From c11caa31a19ad20c44fbf5d246f4782bdd878608 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 23 Dec 2015 01:51:12 +0100 Subject: [PATCH] V1.0.0 Release Candidate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation OTA Web serveur sur le système de fichier SPIFFS Interface Utilisateur pour la Configuration --- .gitignore | 4 + examples/ESP8266_WifInfo/ESP8266_WifInfo.ino | 585 ------------- examples/ESP8266_WifInfo/config.h | 75 -- examples/ESP8266_WifInfo/route.cpp | 562 ------------ .../ESP8266_WifInfo.h => Wifinfo/Wifinfo.h} | 40 +- examples/Wifinfo/Wifinfo.ino | 766 +++++++++++++++++ .../{ESP8266_WifInfo => Wifinfo}/config.cpp | 62 +- examples/Wifinfo/config.h | 148 ++++ examples/Wifinfo/data/css/wifinfo.css.gz | Bin 0 -> 21983 bytes examples/Wifinfo/data/favicon.ico | Bin 0 -> 1150 bytes examples/Wifinfo/data/fonts/glyphicons.woff | Bin 0 -> 23424 bytes examples/Wifinfo/data/fonts/glyphicons.woff2 | Bin 0 -> 18028 bytes examples/Wifinfo/data/index.htm.gz | Bin 0 -> 8215 bytes examples/Wifinfo/data/js/wifinfo.js.gz | Bin 0 -> 56176 bytes examples/Wifinfo/webclient.cpp | 241 ++++++ examples/Wifinfo/webclient.h | 38 + examples/Wifinfo/webserver.cpp | 812 ++++++++++++++++++ .../route.h => Wifinfo/webserver.h} | 16 +- 18 files changed, 2094 insertions(+), 1255 deletions(-) delete mode 100644 examples/ESP8266_WifInfo/ESP8266_WifInfo.ino delete mode 100644 examples/ESP8266_WifInfo/config.h delete mode 100644 examples/ESP8266_WifInfo/route.cpp rename examples/{ESP8266_WifInfo/ESP8266_WifInfo.h => Wifinfo/Wifinfo.h} (80%) create mode 100644 examples/Wifinfo/Wifinfo.ino rename examples/{ESP8266_WifInfo => Wifinfo}/config.cpp (65%) create mode 100644 examples/Wifinfo/config.h create mode 100644 examples/Wifinfo/data/css/wifinfo.css.gz create mode 100644 examples/Wifinfo/data/favicon.ico create mode 100644 examples/Wifinfo/data/fonts/glyphicons.woff create mode 100644 examples/Wifinfo/data/fonts/glyphicons.woff2 create mode 100644 examples/Wifinfo/data/index.htm.gz create mode 100644 examples/Wifinfo/data/js/wifinfo.js.gz create mode 100644 examples/Wifinfo/webclient.cpp create mode 100644 examples/Wifinfo/webclient.h create mode 100644 examples/Wifinfo/webserver.cpp rename examples/{ESP8266_WifInfo/route.h => Wifinfo/webserver.h} (81%) diff --git a/.gitignore b/.gitignore index 96374c4..25f47fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ +node_modules/ +bootstrap-3.3.5-dist/ +font-awesome-4.5.0/ +webdev/ # Windows Installer files *.cab diff --git a/examples/ESP8266_WifInfo/ESP8266_WifInfo.ino b/examples/ESP8266_WifInfo/ESP8266_WifInfo.ino deleted file mode 100644 index 5dbc353..0000000 --- a/examples/ESP8266_WifInfo/ESP8266_WifInfo.ino +++ /dev/null @@ -1,585 +0,0 @@ -// ********************************************************************************** -// ESP8266 Teleinfo WEB Server -// ********************************************************************************** -// Creative Commons Attrib Share-Alike License -// You are free to use/extend this library but please abide with the CC-BY-SA license: -// Attribution-NonCommercial-ShareAlike 4.0 International License -// http://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// For any explanation about teleinfo ou use , see my blog -// http://hallard.me/category/tinfo -// -// This program works with the Wifinfo board -// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo -// -// Written by Charles-Henri Hallard (http://hallard.me) -// -// History : V1.00 2015-06-14 - First release -// -// All text above must be included in any redistribution. -// -// ********************************************************************************** -// Include Arduino header -#include -#include -#include -#include -#include -#include -#include -//#include -//#include -#include -#include - -// Global project file -#include "ESP8266_WifInfo.h" - -//WiFiManager wifi(0); -ESP8266WebServer server(80); -// Udp listener and telnet server -WiFiUDP OTA; -// Teleinfo -TInfo tinfo; -// RGB Loed -NeoPixelBus rgb_led = NeoPixelBus(1, RGB_LED_PIN, NEO_RGB | NEO_KHZ800); - -// define whole brigtness level for RGBLED -uint8_t rgb_brightness = 127; -// LED Blink timers -Ticker rgb_ticker; -Ticker blu_ticker; -Ticker red_ticker; -Ticker Every_1_Sec; - -volatile boolean task_1_sec = false; -unsigned long seconds = 0; - -// sysinfo data -_sysinfo sysinfo; - -/* ====================================================================== -Function: UpdateSysinfo -Purpose : update sysinfo variables -Input : true if first call - true if needed to print on serial debug -Output : - -Comments: - -====================================================================== */ -void UpdateSysinfo(boolean first_call, boolean show_debug) -{ - char buff[64]; - int32_t adc; - int sec = seconds; - int min = sec / 60; - int hr = min / 60; - - sprintf( buff, "%02d:%02d:%02d", hr, min % 60, sec % 60); - sysinfo.sys_uptime = buff; - - sprintf( buff, "%d KB", ESP.getFreeHeap()/1024 ); - sysinfo.sys_free_ram = buff; - - adc = ( (1000 * analogRead(A0)) / 1024); - sprintf( buff, "%d mV", adc); - sysinfo.sys_analog = buff; - - // Values not subject to change during running sketch - if (first_call) { - sprintf( buff, "%d KB", ESP.getFlashChipRealSize()/1024 ); - sysinfo.sys_flash_real_size = buff; - sprintf( buff, "%d KB", ESP.getSketchSize()/1024 ); - sysinfo.sys_firmware_size = buff; - sprintf( buff, "%d KB", ESP.getFreeSketchSpace()/1024 ); - sysinfo.sys_firmware_free = buff; - sprintf( buff, "%d MHZ", ESP.getFlashChipSpeed()/1000 ); - sysinfo.sys_flash_speed = buff; - } - - if (show_debug) { - Debug(F("Firmware : ")); Debugln(__DATE__ " " __TIME__); - Debug(F("Flash real id: ")); Serial1.printf("0x%08X\r\n", ESP.getFlashChipId()); - Debug(F("Flash RSize : ")); Debugln(sysinfo.sys_flash_real_size); - Debug(F("CPU Speed : ")); Debugln(sysinfo.sys_flash_speed); - Debug(F("Sketch size : ")); Debugln(sysinfo.sys_firmware_size); - Debug(F("Free size : ")); Debugln(sysinfo.sys_firmware_free); - Debug(F("Free RAM : ")); Debugln(sysinfo.sys_free_ram); - Debug(F("OTA port : ")); Debugln(config.ota_port); - Debug(F("Analog Read : ")); Debugln(sysinfo.sys_analog); - Debug(F("Saved Config : ")); Debugln(sysinfo.sys_eep_config); - } -} - -/* ====================================================================== -Function: Task_1_Sec -Purpose : update our second ticker -Input : - -Output : - -Comments: - -====================================================================== */ -void Task_1_Sec() -{ - task_1_sec = true; - seconds++; -} - -/* ====================================================================== -Function: LedOff -Purpose : callback called after led blink delay -Input : led (defined in term of PIN) -Output : - -Comments: - -====================================================================== */ -void LedOff(int led) -{ - #ifdef BLU_LED_PIN - if (led==BLU_LED_PIN) - LedBluOFF(); - #endif - if (led==RED_LED_PIN) - LedRedOFF(); - if (led==RGB_LED_PIN) - LedRGBOFF(); -} - -/* ====================================================================== -Function: ADPSCallback -Purpose : called by library when we detected a ADPS on any phased -Input : phase number - 0 for ADPS (monophase) - 1 for ADIR1 triphase - 2 for ADIR2 triphase - 3 for ADIR3 triphase -Output : - -Comments: should have been initialised in the main sketch with a - tinfo.attachADPSCallback(ADPSCallback()) -====================================================================== */ -void ADPSCallback(uint8_t phase) -{ - // Monophasé - if (phase == 0 ) { - Debugln(F("ADPS")); - } else { - Debug(F("ADPS Phase ")); - Debugln('0' + phase); - } -} - -/* ====================================================================== -Function: DataCallback -Purpose : callback when we detected new or modified data received -Input : linked list pointer on the concerned data - value current state being TINFO_VALUE_ADDED/TINFO_VALUE_UPDATED -Output : - -Comments: - -====================================================================== */ -void DataCallback(ValueList * me, uint8_t flags) -{ - - // This is for simulating ADPS during my tests - // =========================================== - /* - static uint8_t test = 0; - // Each new/updated values - if (++test >= 20) { - test=0; - uint8_t anotherflag = TINFO_FLAGS_NONE; - ValueList * anotherme = tinfo.addCustomValue("ADPS", "46", &anotherflag); - - // Do our job (mainly debug) - DataCallback(anotherme, anotherflag); - } - Debugf("%02d:",test); - */ - // =========================================== - - - // Do whatever you want there - Debug(me->name); - Debug('='); - Debug(me->value); - - if ( flags & TINFO_FLAGS_NOTHING ) Debug(F(" Nothing")); - if ( flags & TINFO_FLAGS_ADDED ) Debug(F(" Added")); - if ( flags & TINFO_FLAGS_UPDATED ) Debug(F(" Updated")); - if ( flags & TINFO_FLAGS_EXIST ) Debug(F(" Exist")); - if ( flags & TINFO_FLAGS_ALERT ) Debug(F(" Alert")); - - Debugln(); -} - -/* ====================================================================== -Function: NewFrame -Purpose : callback when we received a complete teleinfo frame -Input : linked list pointer on the concerned data -Output : - -Comments: - -====================================================================== */ -void NewFrame(ValueList * me) -{ - char buff[32]; - - // Light the RGB LED - LedRGBON(COLOR_GREEN); - - sprintf( buff, "New Frame (%ld Bytes free)", ESP.getFreeHeap() ); - Debugln(buff); - - // led off after delay - rgb_ticker.once_ms( (uint32_t) BLINK_LED_MS, LedOff, (int) RGB_LED_PIN); -} - -/* ====================================================================== -Function: NewFrame -Purpose : callback when we received a complete teleinfo frame -Input : linked list pointer on the concerned data -Output : - -Comments: it's called only if one data in the frame is different than - the previous frame -====================================================================== */ -void UpdatedFrame(ValueList * me) -{ - char buff[32]; - - // Light the RGB LED (purple) - LedRGBON(COLOR_MAGENTA); - - sprintf( buff, "Updated Frame (%ld Bytes free)", ESP.getFreeHeap() ); - Debugln(buff); - - // led off after delay - rgb_ticker.once_ms(BLINK_LED_MS, LedOff, RGB_LED_PIN); -} - -/* ====================================================================== -Function: CheckOTAUpdate -Purpose : Check if we need to update the firmware over the Air -Input : - -Output : - -Comments: If upgraded, no return, perform update and reboot ESP -====================================================================== */ -void CheckOTAUpdate(void) -{ - bool spiffs = false; - - //OTA detection - if (OTA.parsePacket()) { - IPAddress remote = OTA.remoteIP(); - int cmd = OTA.parseInt(); - int port = OTA.parseInt(); - int size = OTA.parseInt(); - - LedRGBON(COLOR_MAGENTA); - - DebugF("OTA received command "); - Debugln(cmd); - if (cmd == U_SPIFFS) { - spiffs = true; - DebugF("Get SPIFFS image"); - } - - - DebugF("Update Start: ip:"); - Debug(remote); - Debugf(", port:%d, size:%dKB\n", port, size/1024); - uint32_t startTime = millis(); - - WiFiUDP::stopAll(); - - if(!Update.begin(size, cmd)) { - DebugF("Update Begin Error"); - return; - } - - WiFiClient ota_client; - - if (ota_client.connect(remote, port)) { - - uint32_t written; - while(!Update.isFinished()) { - written = Update.write(ota_client); - if(written > 0) - { - LedRGBOFF(); - ota_client.print(written, DEC); - LedRGBON(COLOR_MAGENTA); - } - } - LedRGBOFF(); - Serial.setDebugOutput(false); - - if(Update.end()) { - ota_client.println("OK"); - Debugf("Update Success: %u\nRebooting...\n", millis() - startTime); - ESP.restart(); - } else { - Update.printError(ota_client); - Update.printError(Serial); - } - } else { - Debugf("Connect Failed: %u\n", millis() - startTime); - } - - // Be sure to re enable it - OTA.begin(DEFAULT_OTA_PORT); - } -} - -/* ====================================================================== -Function: WifiSoftAP -Purpose : Change Wifi mode to Soft AP -Input : - -Output : state of the wifi status -Comments: - -====================================================================== */ -int WifiSoftAP() -{ -} - -/* ====================================================================== -Function: WifiHandleConn -Purpose : Handle Wifi connection / reconnection and OTA updates -Input : setup true if we're called 1st Time from setup -Output : state of the wifi status -Comments: - -====================================================================== */ -int WifiHandleConn(boolean setup = false) -{ - int ret ; - - if (setup) - { - // Check WiFi connection mode, at startup - // try to connect to AP - if (WiFi.getMode()!=WIFI_STA) { - WiFi.mode(WIFI_STA); - delay(10); - } - - // Get Wifi Status - ret = WiFi.status(); - - // Try to get 1st connexion - if ( ret != WL_CONNECTED ) { - - // Orange we're not connected anymore - LedRGBON(COLOR_ORANGE); - - DebugF("Connecting to: "); - Debug(DEFAULT_WIFI_SSID); - Debug(F("...")); - - WiFi.begin(DEFAULT_WIFI_SSID, DEFAULT_WIFI_PASS); - - ret = WiFi.waitForConnectResult(); - if ( ret != WL_CONNECTED) { - LedRGBON(COLOR_RED); - DebuglnF("Connection failed!"); - } else { - LedRGBON(COLOR_GREEN); - DebuglnF("Connected"); - DebugF("IP address : "); Debugln(WiFi.localIP()); - DebugF("MAC address : "); Debugln(WiFi.macAddress()); - - // just in case your sketch sucks, keep update OTA Available - // Trust me, when coding and testing it happens, this could save - // the need to connect FTDI to reflash - // Usefull just after 1st connexion when called from setup() before - // launching potentially bugging main() - for (uint8_t i=0; i<= 10; i++) { - LedRGBON(COLOR_MAGENTA); - delay(100); - LedRGBOFF(); - delay(200); - CheckOTAUpdate(); - } - } - } - - // We did not succeded to connect ? - if ( ret != WL_CONNECTED ) { - uint8_t mac[WL_MAC_ADDR_LENGTH]; - char ssid[32]; - - // start Soft AP - DebuglnF("Starting Soft AP mode"); - WiFi.mode(WIFI_AP); - - // Add the last two bytes of the MAC address to AP name - WiFi.softAPmacAddress(mac); - sprintf_P(ssid, PSTR("%s_%02X%02X"), DEFAULT_WIFI_AP_SSID, mac[4], mac[5] ); - - DebugF("SSID : "); Debugln(ssid); - //WiFi.softAP(ssid, DEFAULT_WIFI_AP_PSK); - //DebuglnF("PSK : " DEFAULT_WIFI_AP_PSK); - // No password - WiFi.softAP(ssid); - DebugF("IP : "); - Debugln(WiFi.softAPIP()); - } - - // Advertise US for Arduino IDE - // not very usefull on Windows (IDE does not always sees us) - MDNS.begin(DEFAULT_HOSTNAME); - MDNS.addService("arduino", "tcp", DEFAULT_OTA_PORT); - - // Setup OTA feature - OTA.begin(DEFAULT_OTA_PORT); - } - - // Handle OTA - CheckOTAUpdate(); - - return ret; -} - -/* ====================================================================== -Function: setup -Purpose : Setup I/O and other one time startup stuff -Input : - -Output : - -Comments: - -====================================================================== */ -void setup() -{ - boolean reset_config = true; - - // Set WiFi to station mode and disconnect from an AP if it was previously connected - //WiFi.mode(WIFI_STA); - //WiFi.disconnect(); - //delay(100); - - // Init the RGB Led, and set it off - rgb_led.Begin(); - LedRGBOFF(); - - // Init the serial 1 - // Teleinfo is connected to RXD2 (GPIO13) to - // avoid conflict when flashing, this is why - // we swap RXD1/RXD1 to RXD2/TXD2 - // Note that TXD2 is not used teleinfo is receive only - Serial.begin(1200, SERIAL_7E1); - Serial.swap(); - - // Our Debug Serial TXD0 - // note this serial can only transmit, just - // enough for debugging purpose - Serial1.begin(115200); - - Debugln(F("==============")); - Debugln(F("Wifinfo V1.0.1")); - Debugln(); - Debugflush(); - - // Clear our global flags - config.config = 0; - - // Our configuration is stored into EEPROM - EEPROM.begin(sizeof(_Config)); - - // Read Configuration from EEP - if (readConfig()) { - // in case of saved default config it's not good - if (config.ssid[0]!='*' && config.pass[0]!='*' ) { - sysinfo.sys_eep_config = PSTR("Good CRC, config OK"); - reset_config = false; - } else { - sysinfo.sys_eep_config = PSTR("Good CRC, not set!"); - } - } - - if (reset_config ) { - // Error, enable default configuration - strcpy_P(config.ssid, PSTR(DEFAULT_WIFI_SSID)); - strcpy_P(config.pass, PSTR(DEFAULT_WIFI_PASS)); - strcpy_P(config.host, PSTR(DEFAULT_HOSTNAME)); - config.ota_port = DEFAULT_OTA_PORT ; - - // Indicate the error in global flags - config.config |= CFG_BAD_CRC; - sysinfo.sys_eep_config = PSTR("Reset to default"); - - // save back - saveConfig(); - } - - Debugln(sysinfo.sys_eep_config); - - // Init teleinfo - tinfo.init(); - - // Attach the callback we need - // set all as an example - tinfo.attachADPS(ADPSCallback); - tinfo.attachData(DataCallback); - tinfo.attachNewFrame(NewFrame); - tinfo.attachUpdatedFrame(UpdatedFrame); - - // We'll drive our onboard LED - // old TXD1, not used anymore, has been swapped - pinMode(RED_LED_PIN, OUTPUT); - LedRedOFF(); - - // start Wifi connect or soft AP - WifiHandleConn(true); - - // Update sysinfor variable and print them - UpdateSysinfo(true, true); - - server.on("/", handleRoot); - server.on("/json", sendJSON); - server.on("/tinfojsontbl", tinfoJSONTable); - server.on("/sysjsontbl", sysJSONTable); - server.onNotFound(handleNotFound); - server.begin(); - - Debugln(F("HTTP server started")); - - //webSocket.begin(); - //webSocket.onEvent(webSocketEvent); - - // Light off the RGB LED - LedRGBOFF(); - - // control watchdog - //ESP.wdtEnable(); - //ESP.wdtDisable() - //ESP.wdtFeed(); - - // Update sysinfo every second - Every_1_Sec.attach(1, Task_1_Sec); -} - -/* ====================================================================== -Function: loop -Purpose : infinite loop main code -Input : - -Output : - -Comments: - -====================================================================== */ -void loop() -{ - char c; - - // Do all related network stuff - server.handleClient(); - - //webSocket.loop(); - - // 1 second task job ? - if (task_1_sec) { - UpdateSysinfo(false, false); - task_1_sec = false; - } - - // Handle teleinfo serial - if ( Serial.available() ) { - // Read Serial and process to tinfo - c = Serial.read(); - //Serial1.print(c); - tinfo.process(c); - } - - delay(10); -} - diff --git a/examples/ESP8266_WifInfo/config.h b/examples/ESP8266_WifInfo/config.h deleted file mode 100644 index 067dd03..0000000 --- a/examples/ESP8266_WifInfo/config.h +++ /dev/null @@ -1,75 +0,0 @@ -// ********************************************************************************** -// ESP8266 Teleinfo WEB Server configuration Include file -// ********************************************************************************** -// Creative Commons Attrib Share-Alike License -// You are free to use/extend this library but please abide with the CC-BY-SA license: -// Attribution-NonCommercial-ShareAlike 4.0 International License -// http://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// For any explanation about teleinfo ou use , see my blog -// http://hallard.me/category/tinfo -// -// This program works with the Wifinfo board -// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo -// -// Written by Charles-Henri Hallard (http://hallard.me) -// -// History : V1.00 2015-06-14 - First release -// -// All text above must be included in any redistribution. -// -// ********************************************************************************** -#ifndef __CONFIG_H__ -#define __CONFIG_H__ - -// Include main project include file -#include "ESP8266_WifInfo.h" - -#define CFG_MAX_SSID_SIZE 32 -#define CFG_MAX_PASS_SIZE 32 -#define CFG_MAX_HOSTNAME 16 - -// Mettez ici vos identifiant de connexion à -// votre réseau WIFI -#define DEFAULT_WIFI_SSID "************" -#define DEFAULT_WIFI_PASS "************" -#define DEFAULT_HOSTNAME "WifInfo-esp01" - -// En mode acces point -#define DEFAULT_WIFI_AP_SSID "WifInfo" -#define DEFAULT_WIFI_AP_PSK "WifInfoPSK" - -// Port pour l'OTA -#define DEFAULT_OTA_PORT 8266 - -// Bit definition for different configuration modes -#define CFG_LCD 0x0001 // Enable display -#define CFG_DEBUG 0x0002 // Enable serial debug -#define CFG_BAD_CRC 0x8000 // Bad CRC when reading configuration - -// Config saved into eeprom -// 128 bytes total including CRC -typedef struct -{ - char ssid[CFG_MAX_SSID_SIZE]; /* SSID */ - char pass[CFG_MAX_PASS_SIZE]; /* Password */ - char host[CFG_MAX_HOSTNAME]; /* Password */ - uint32_t config; /* Bit field register */ - uint16_t ota_port; /* OTA port */ - uint8_t filler[40]; /* in case adding data in config avoiding loosing current conf by bad crc*/ - uint16_t crc; -} _Config; - - -// Exported variables/object instancied in main sketch -// =================================================== -extern _Config config; - -// Declared exported function from route.cpp -// =================================================== -bool readConfig (void); -bool saveConfig (void); - - -#endif - diff --git a/examples/ESP8266_WifInfo/route.cpp b/examples/ESP8266_WifInfo/route.cpp deleted file mode 100644 index 97a4059..0000000 --- a/examples/ESP8266_WifInfo/route.cpp +++ /dev/null @@ -1,562 +0,0 @@ -// ********************************************************************************** -// ESP8266 Teleinfo WEB Server, route web function -// ********************************************************************************** -// Creative Commons Attrib Share-Alike License -// You are free to use/extend this library but please abide with the CC-BY-SA license: -// Attribution-NonCommercial-ShareAlike 4.0 International License -// http://creativecommons.org/licenses/by-nc-sa/4.0/ -// -// For any explanation about teleinfo ou use, see my blog -// http://hallard.me/category/tinfo -// -// This program works with the Wifinfo board -// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo -// -// Written by Charles-Henri Hallard (http://hallard.me) -// -// History : V1.00 2015-06-14 - First release -// -// All text above must be included in any redistribution. -// -// ********************************************************************************** - -// Include Arduino header -#include "route.h" - -/* ====================================================================== -Function: handleRoot -Purpose : handle main page /, display information -Input : - -Output : - -Comments: - -====================================================================== */ -void handleRoot(void) -{ - String response=""; - - // Just to debug where we are - Debug(F("Serving / page...")); - - LedBluON(); - - // start HTML Code - response += F("" - "" - "" - "" - "" - // Our custom style - "" - "" - "" - "Wifinfo" - ""); - - response += F("
" - // Onglets - ""); - - // Contenu des onglets - response += F("
"); - - // tab teleinfo - response += F( "
" - "

Données de Téléinformation

" - "
Charge courante : " - "
" - "
" - "Attente des données" - "
" - "
" - "
" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
EtiquetteValeurChecksumFlags
" - "
"); // tab pane - // tab Systeme - response += F( "
" - "

Données du système

" - "" - "" - "" - "" - "" - "" - "" - "
DonnéeValeur
" - "
"); // tab pane - - // tab Configuration - response += F( "
" - "

Configuration du module WifInfo

" - "

Cette partie reste à faire, des volontaires motivés ?

" - "
"); // tab pane - - response += F("
"); // tab content - -/* - response += F("")); -*/ - - response += F("
\r\n"); // Container - response += F("\r\n"); - response += F("\r\n"); - response += F("\r\n"); - - response += F("" "\r\n"); - - response += F(""); - - response += F("\r\n"); - - // Just to debug buffer free size - Debug(F("sending ")); - Debug(response.length()); - Debug(F(" bytes...")); - - // Send response to client - server.send ( 200, "text/html", response ); - - Debug(F("OK!")); - - LedBluOFF(); -} - -/* ====================================================================== -Function: formatNumberJSON -Purpose : check if data value is full number and send correct JSON format -Input : String where to add response - char * value to check -Output : - -Comments: 00150 => 150 - ADCO => "ADCO" - 1 => 1 -====================================================================== */ -void formatNumberJSON( String &response, char * value) -{ - // we have at least something ? - if (value && strlen(value)) - { - boolean isNumber = true; - uint8_t c; - char * p = value; - - // just to be sure - if (strlen(p)<=16) { - // check if value is number - while (*p && isNumber) { - if ( *p < '0' || *p > '9' ) - isNumber = false; - p++; - } - - // this will add "" on not number values - if (!isNumber) { - response += '\"' ; - response += value ; - response += F("\":") ; - } else { - // this will remove leading zero on numbers - p = value; - while (*p=='0' && *(p+1) ) - p++; - response += p ; - } - } else { - Debugln(F("formatNumberJSON error!")); - } - } -} - - -/* ====================================================================== -Function: tinfoJSONTable -Purpose : dump all teleinfo values in JSON table format for browser -Input : linked list pointer on the concerned data - true to dump all values, false for only modified ones -Output : - -Comments: - -====================================================================== */ -void tinfoJSONTable(void) -{ - ValueList * me = tinfo.getList(); - String response = ""; - - // Just to debug where we are - Debug(F("Serving /tinfoJSONTable page...\r\n")); - - // Got at least one ? - if (me) { - uint8_t index=0; - - boolean first_item = true; - // Json start - response += F("[\r\n"); - - // Loop thru the node - while (me->next) { - - // we're there - ESP.wdtFeed(); - - // go to next node - me = me->next; - - // First item do not add , separator - if (first_item) - first_item = false; - else - response += F(",\r\n"); - - Debug(F("(")) ; - Debug(++index) ; - Debug(F(") ")) ; - - if (me->name) Debug(me->name) ; - else Debug(F("NULL")) ; - - Debug(F("=")) ; - - if (me->value) Debug(me->value) ; - else Debug(F("NULL")) ; - - Debug(F(" '")) ; - Debug(me->checksum) ; - Debug(F("' ")); - - // Flags management - if ( me->flags) { - Debug(F("Flags:0x")); - Debugf("%02X => ", me->flags); - if ( me->flags & TINFO_FLAGS_EXIST) - Debug(F("Exist ")) ; - if ( me->flags & TINFO_FLAGS_UPDATED) - Debug(F("Updated ")) ; - if ( me->flags & TINFO_FLAGS_ADDED) - Debug(F("New ")) ; - } - - Debugln() ; - - response += F("{\"na\":\""); - response += me->name ; - response += F("\", \"va\":\"") ; - response += me->value; - response += F("\", \"ck\":\"") ; - if (me->checksum == '"' || me->checksum == '\\' || me->checksum == '/') - response += '\\'; - response += (char) me->checksum; - response += F("\", \"fl\":"); - response += me->flags ; - response += '}' ; - - } - // Json end - response += F("\r\n]"); - - } else { - Debugln(F("sending 404...")); - server.send ( 404, "text/plain", "No data" ); - } - Debug(F("sending...")); - server.send ( 200, "text/json", response ); - Debugln(F("OK!")); -} - -/* ====================================================================== -Function: sysJSONTable -Purpose : dump all sysinfo values in JSON table format for browser -Input : linked list pointer on the concerned data - true to dump all values, false for only modified ones -Output : - -Comments: - -====================================================================== */ -void sysJSONTable() -{ - String response = ""; - - // Just to debug where we are - Debug(F("Serving /sysJSONTable page...")); - - // Json start - response += F("[\r\n"); - - response += "{\"na\":\"Uptime\",\"va\":\""; - response += sysinfo.sys_uptime; - response += "\"},\r\n"; - - response += "{\"na\":\"Compile le\",\"va\":\"" __DATE__ " " __TIME__ "\"},\r\n"; - response += "{\"na\":\"Free Ram\",\"va\":\""; - response += sysinfo.sys_free_ram; - response += "\"},\r\n"; - - response += "{\"na\":\"Flash Real Size\",\"va\":\""; - response += sysinfo.sys_flash_real_size ; - response += "\"},\r\n"; - - response += "{\"na\":\"Firmware Size\",\"va\":\""; - response += sysinfo.sys_firmware_size; - response += "\"},\r\n"; - - response += "{\"na\":\"Free Size\",\"va\":\""; - response += sysinfo.sys_firmware_free; - response += "\"},\r\n"; - - response += "{\"na\":\"Wifi SSID\",\"va\":\""; - response += config.ssid; - response += "\"},\r\n"; - - response += "{\"na\":\"OTA Network Port\",\"va\":"; - response += config.ota_port ; - response += "},\r\n"; - - response += "{\"na\":\"Wifi RSSI\",\"va\":\""; - response += WiFi.RSSI() ; - response += " dB\"}\r\n"; - - response += "{\"na\":\"Analog\",\"va\":\""; - response += sysinfo.sys_analog; - response += " dB\"}\r\n"; - - // Json end - response += F("]\r\n"); - - Debug(F("sending...")); - server.send ( 200, "text/json", response ); - Debugln(F("Ok!")); -} - -/* ====================================================================== -Function: sendJSON -Purpose : dump all values in JSON -Input : linked list pointer on the concerned data - true to dump all values, false for only modified ones -Output : - -Comments: - -====================================================================== */ -void sendJSON(void) -{ - ValueList * me = tinfo.getList(); - String response = ""; - - // Got at least one ? - if (me) { - // Json start - response += F("{\"_UPTIME\":"); - response += seconds; - - // Loop thru the node - while (me->next) { - - // we're there - ESP.wdtFeed(); - - // go to next node - me = me->next; - - response += F(",\"") ; - response += me->name ; - response += F("\":") ; - formatNumberJSON(response, me->value); - } - // Json end - response += F("}\r\n") ; - - } else { - server.send ( 404, "text/plain", "No data" ); - } - server.send ( 200, "text/json", response ); -} - -/* ====================================================================== -Function: handleNotFound -Purpose : default WEB routing when URI is not found -Input : linked list pointer on the concerned data - true to dump all values, false for only modified ones -Output : - -Comments: We search is we have a name that match to this URI, if one we - return it's pair name/value in json -====================================================================== */ -void handleNotFound(void) -{ - String response = ""; - - // We check for an known label - ValueList * me = tinfo.getList(); - const char * uri; - boolean found = false; - - // Led on - LedBluON(); - - // convert uri to char * for compare - uri = server.uri().c_str(); - - Debugf("handleNotFound(%s)\r\n", uri); - - // Got at least one and consistent URI ? - if (me && uri && *uri=='/' && *++uri ) { - - // Loop thru the linked list of values - while (me->next && !found) { - - // we're there - ESP.wdtFeed(); - - // go to next node - me = me->next; - - //Debugf("compare to '%s' ", me->name); - // Do we have this one ? - if (stricmp (me->name, uri) == 0 ) - { - // no need to continue - found = true; - - // Add to respone - response += F("{\"") ; - response += me->name ; - response += F("\":") ; - formatNumberJSON(response, me->value); - response += F("}\r\n"); - } - } - } - - // Got it, send json - if (found) { - server.send ( 200, "text/json", response ); - } else { - // send error message in plain text - String message = "File Not Found\n\n"; - message += "URI: "; - message += server.uri(); - message += "\nMethod: "; - message += ( server.method() == HTTP_GET ) ? "GET" : "POST"; - message += "\nArguments: "; - message += server.args(); - message += "\n"; - - for ( uint8_t i = 0; i < server.args(); i++ ) { - message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n"; - } - - server.send ( 404, "text/plain", message ); - } - - // Led off - LedBluOFF(); -} - diff --git a/examples/ESP8266_WifInfo/ESP8266_WifInfo.h b/examples/Wifinfo/Wifinfo.h similarity index 80% rename from examples/ESP8266_WifInfo/ESP8266_WifInfo.h rename to examples/Wifinfo/Wifinfo.h index b64df11..3836de5 100644 --- a/examples/ESP8266_WifInfo/ESP8266_WifInfo.h +++ b/examples/Wifinfo/Wifinfo.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -34,23 +35,33 @@ //#include #include #include +#include -#include "route.h" +extern "C" { +#include "user_interface.h" +} + +#include "webserver.h" +#include "webclient.h" #include "config.h" #define DEBUG +#define DEBUG_SERIAL Serial1 +#define DEBUG_SERIAL1 + +#define WIFINFO_VERSION "1.0.0" // I prefix debug macro to be sure to use specific for THIS library // debugging, this should not interfere with main sketch or other // libraries #ifdef DEBUG -#define Debug(x) Serial1.print(x) -#define Debugln(x) Serial1.println(x) -#define DebugF(x) Serial1.print(F(x)) -#define DebuglnF(x) Serial1.println(F(x)) -#define Debugf(...) Serial1.printf(__VA_ARGS__) -#define Debugflush Serial1.flush +#define Debug(x) DEBUG_SERIAL.print(x) +#define Debugln(x) DEBUG_SERIAL.println(x) +#define DebugF(x) DEBUG_SERIAL.print(F(x)) +#define DebuglnF(x) DEBUG_SERIAL.println(F(x)) +#define Debugf(...) DEBUG_SERIAL.printf(__VA_ARGS__) +#define Debugflush DEBUG_SERIAL.flush #else #define Debug(x) {} #define Debugln(x) {} @@ -92,13 +103,6 @@ typedef struct { String sys_uptime; - String sys_free_ram; - String sys_flash_real_size; - String sys_flash_speed; - String sys_firmware_size; - String sys_firmware_free; - String sys_analog; - String sys_eep_config; } _sysinfo; // Exported variables/object instancied in main sketch @@ -110,7 +114,15 @@ extern NeoPixelBus rgb_led ; extern uint8_t rgb_brightness; extern unsigned long seconds; extern _sysinfo sysinfo; +extern Ticker Tick_emoncms; +extern Ticker Tick_jeedom; +// Exported function located in main sketch +// =================================================== +void ResetConfig(void); +void Task_emoncms(); +void Task_jeedom(); + #endif diff --git a/examples/Wifinfo/Wifinfo.ino b/examples/Wifinfo/Wifinfo.ino new file mode 100644 index 0000000..b368ebf --- /dev/null +++ b/examples/Wifinfo/Wifinfo.ino @@ -0,0 +1,766 @@ +// ********************************************************************************** +// ESP8266 Teleinfo WEB Server +// ********************************************************************************** +// Creative Commons Attrib Share-Alike License +// You are free to use/extend this library but please abide with the CC-BY-SA license: +// Attribution-NonCommercial-ShareAlike 4.0 International License +// http://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// For any explanation about teleinfo ou use , see my blog +// http://hallard.me/category/tinfo +// +// This program works with the Wifinfo board +// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo +// +// Written by Charles-Henri Hallard (http://hallard.me) +// +// History : V1.00 2015-06-14 - First release +// +// All text above must be included in any redistribution. +// +// ********************************************************************************** +// Include Arduino header +#include +#include +#include +#include +#include +#include +#include +#include +#include +//#include +//#include +#include +#include +#include + +// Global project file +#include "Wifinfo.h" + +//WiFiManager wifi(0); +ESP8266WebServer server(80); + +bool ota_blink; + +// Teleinfo +TInfo tinfo; + +// RGB Loed +NeoPixelBus rgb_led = NeoPixelBus(1, RGB_LED_PIN, NEO_RGB | NEO_KHZ800); + +// define whole brigtness level for RGBLED +uint8_t rgb_brightness = 127; +// LED Blink timers +Ticker rgb_ticker; +Ticker blu_ticker; +Ticker red_ticker; +Ticker Every_1_Sec; +Ticker Tick_emoncms; +Ticker Tick_jeedom; + +volatile boolean task_1_sec = false; +volatile boolean task_emoncms = false; +volatile boolean task_jeedom = false; +unsigned long seconds = 0; + +// sysinfo data +_sysinfo sysinfo; + +/* ====================================================================== +Function: UpdateSysinfo +Purpose : update sysinfo variables +Input : true if first call + true if needed to print on serial debug +Output : - +Comments: - +====================================================================== */ +void UpdateSysinfo(boolean first_call, boolean show_debug) +{ + char buff[64]; + int32_t adc; + int sec = seconds; + int min = sec / 60; + int hr = min / 60; + + sprintf_P( buff, PSTR("%02d:%02d:%02d"), hr, min % 60, sec % 60); + sysinfo.sys_uptime = buff; +} + +/* ====================================================================== +Function: Task_1_Sec +Purpose : update our second ticker +Input : - +Output : - +Comments: - +====================================================================== */ +void Task_1_Sec() +{ + task_1_sec = true; + seconds++; +} + +/* ====================================================================== +Function: Task_emoncms +Purpose : callback of emoncms ticker +Input : +Output : - +Comments: Like an Interrupt, need to be short, we set flag for main loop +====================================================================== */ +void Task_emoncms() +{ + task_emoncms = true; +} + +/* ====================================================================== +Function: Task_jeedom +Purpose : callback of jeedom ticker +Input : +Output : - +Comments: Like an Interrupt, need to be short, we set flag for main loop +====================================================================== */ +void Task_jeedom() +{ + task_jeedom = true; +} + +/* ====================================================================== +Function: LedOff +Purpose : callback called after led blink delay +Input : led (defined in term of PIN) +Output : - +Comments: - +====================================================================== */ +void LedOff(int led) +{ + #ifdef BLU_LED_PIN + if (led==BLU_LED_PIN) + LedBluOFF(); + #endif + if (led==RED_LED_PIN) + LedRedOFF(); + if (led==RGB_LED_PIN) + LedRGBOFF(); +} + +/* ====================================================================== +Function: ADPSCallback +Purpose : called by library when we detected a ADPS on any phased +Input : phase number + 0 for ADPS (monophase) + 1 for ADIR1 triphase + 2 for ADIR2 triphase + 3 for ADIR3 triphase +Output : - +Comments: should have been initialised in the main sketch with a + tinfo.attachADPSCallback(ADPSCallback()) +====================================================================== */ +void ADPSCallback(uint8_t phase) +{ + // Monophasé + if (phase == 0 ) { + Debugln(F("ADPS")); + } else { + Debug(F("ADPS Phase ")); + Debugln('0' + phase); + } +} + +/* ====================================================================== +Function: DataCallback +Purpose : callback when we detected new or modified data received +Input : linked list pointer on the concerned data + value current state being TINFO_VALUE_ADDED/TINFO_VALUE_UPDATED +Output : - +Comments: - +====================================================================== */ +void DataCallback(ValueList * me, uint8_t flags) +{ + + // This is for simulating ADPS during my tests + // =========================================== + /* + static uint8_t test = 0; + // Each new/updated values + if (++test >= 20) { + test=0; + uint8_t anotherflag = TINFO_FLAGS_NONE; + ValueList * anotherme = tinfo.addCustomValue("ADPS", "46", &anotherflag); + + // Do our job (mainly debug) + DataCallback(anotherme, anotherflag); + } + Debugf("%02d:",test); + */ + // =========================================== + +/* + // Do whatever you want there + Debug(me->name); + Debug('='); + Debug(me->value); + + if ( flags & TINFO_FLAGS_NOTHING ) Debug(F(" Nothing")); + if ( flags & TINFO_FLAGS_ADDED ) Debug(F(" Added")); + if ( flags & TINFO_FLAGS_UPDATED ) Debug(F(" Updated")); + if ( flags & TINFO_FLAGS_EXIST ) Debug(F(" Exist")); + if ( flags & TINFO_FLAGS_ALERT ) Debug(F(" Alert")); + + Debugln(); +*/ +} + +/* ====================================================================== +Function: NewFrame +Purpose : callback when we received a complete teleinfo frame +Input : linked list pointer on the concerned data +Output : - +Comments: - +====================================================================== */ +void NewFrame(ValueList * me) +{ + char buff[32]; + + // Light the RGB LED + if ( config.config & CFG_RGB_LED) { + LedRGBON(COLOR_GREEN); + + // led off after delay + rgb_ticker.once_ms( (uint32_t) BLINK_LED_MS, LedOff, (int) RGB_LED_PIN); + } + + sprintf_P( buff, PSTR("New Frame (%ld Bytes free)"), ESP.getFreeHeap() ); + Debugln(buff); +} + +/* ====================================================================== +Function: NewFrame +Purpose : callback when we received a complete teleinfo frame +Input : linked list pointer on the concerned data +Output : - +Comments: it's called only if one data in the frame is different than + the previous frame +====================================================================== */ +void UpdatedFrame(ValueList * me) +{ + char buff[32]; + + // Light the RGB LED (purple) + if ( config.config & CFG_RGB_LED) { + LedRGBON(COLOR_MAGENTA); + + // led off after delay + rgb_ticker.once_ms(BLINK_LED_MS, LedOff, RGB_LED_PIN); + } + + sprintf_P( buff, PSTR("Updated Frame (%ld Bytes free)"), ESP.getFreeHeap() ); + Debugln(buff); + +/* + // Got at least one ? + if (me) { + WiFiUDP myudp; + IPAddress ip = WiFi.localIP(); + + // start UDP server + myudp.begin(1201); + ip[3] = 255; + + // transmit broadcast package + myudp.beginPacket(ip, 1201); + + // start of frame + myudp.write(TINFO_STX); + + // Loop thru the node + while (me->next) { + me = me->next; + // prepare line and write it + sprintf_P( buff, PSTR("%s %s %c\n"),me->name, me->value, me->checksum ); + myudp.write( buff); + } + + // End of frame + myudp.write(TINFO_ETX); + myudp.endPacket(); + myudp.flush(); + + } +*/ +} + + +/* ====================================================================== +Function: ResetConfig +Purpose : Set configuration to default values +Input : - +Output : - +Comments: - +====================================================================== */ +void ResetConfig(void) +{ + // enable default configuration + memset(&config, 0, sizeof(_Config)); + + // Set default Hostname + sprintf_P(config.host, PSTR("WifInfo_%06X"), ESP.getChipId()); + strcpy_P(config.ota_auth, PSTR(DEFAULT_OTA_AUTH)); + config.ota_port = DEFAULT_OTA_PORT ; + + // Add other init default config here + + // Emoncms + strcpy_P(config.emoncms.host, CFG_EMON_DEFAULT_HOST); + config.emoncms.port = CFG_EMON_DEFAULT_PORT; + strcpy_P(config.emoncms.url, CFG_EMON_DEFAULT_URL); + config.emoncms.apikey[0] = '\0'; + config.emoncms.node = 0; + config.emoncms.freq = 0; + + // Jeedom + strcpy_P(config.jeedom.host, CFG_JDOM_DEFAULT_HOST); + config.jeedom.port = CFG_JDOM_DEFAULT_PORT; + strcpy_P(config.jeedom.url, CFG_JDOM_DEFAULT_URL); + strcpy_P(config.jeedom.adco, CFG_JDOM_DEFAULT_ADCO); + config.jeedom.apikey[0] = '\0'; + config.jeedom.freq = 0; + + + config.config |= CFG_RGB_LED; + + // save back + saveConfig(); +} + +/* ====================================================================== +Function: WifiHandleConn +Purpose : Handle Wifi connection / reconnection and OTA updates +Input : setup true if we're called 1st Time from setup +Output : state of the wifi status +Comments: - +====================================================================== */ +int WifiHandleConn(boolean setup = false) +{ + int ret = WiFi.status(); + + if (setup) { + + DebuglnF("========== SDK Saved parameters Start"); + WiFi.printDiag(DEBUG_SERIAL); + DebuglnF("========== SDK Saved parameters End"); + Debugflush(); + + // no correct SSID + if (!*config.ssid) { + DebugF("no Wifi SSID in config, trying to get SDK ones..."); + + // Let's see of SDK one is okay + if ( WiFi.SSID() == "" ) { + DebuglnF("Not found may be blank chip!"); + } else { + *config.psk = '\0'; + + // Copy SDK SSID + strcpy(config.ssid, WiFi.SSID().c_str()); + + // Copy SDK password if any + if (WiFi.psk() != "") + strcpy(config.psk, WiFi.psk().c_str()); + + DebuglnF("found one!"); + + // save back new config + saveConfig(); + } + } + + // correct SSID + if (*config.ssid) { + uint8_t timeout ; + + DebugF("Connecting to: "); + Debug(config.ssid); + Debugflush(); + + // Do wa have a PSK ? + if (*config.psk) { + // protected network + Debug(F(" with key '")); + Debug(config.psk); + Debug(F("'...")); + Debugflush(); + WiFi.begin(config.ssid, config.psk); + } else { + // Open network + Debug(F("unsecure AP")); + Debugflush(); + WiFi.begin(config.ssid); + } + + timeout = 25; // 25 * 200 ms = 5 sec time out + // 200 ms loop + while ( ((ret = WiFi.status()) != WL_CONNECTED) && timeout ) + { + // Orange LED + LedRGBON(COLOR_ORANGE); + delay(50); + LedRGBOFF(); + delay(150); + --timeout; + } + } + + + // connected ? disable AP, client mode only + if (ret == WL_CONNECTED) + { + DebuglnF("connected!"); + WiFi.mode(WIFI_STA); + + DebugF("IP address : "); Debugln(WiFi.localIP()); + DebugF("MAC address : "); Debugln(WiFi.macAddress()); + + // not connected ? start AP + } else { + char ap_ssid[32]; + DebuglnF("Error!"); + Debugflush(); + + // SSID = hostname + strcpy(ap_ssid, config.host ); + DebugF("Switching to AP "); + Debugln(ap_ssid); + Debugflush(); + + // protected network + if (*config.ap_psk) { + DebugF(" with key '"); + Debug(config.ap_psk); + DebuglnF("'"); + WiFi.softAP(ap_ssid, config.ap_psk); + // Open network + } else { + DebuglnF(" with no password"); + WiFi.softAP(ap_ssid); + } + WiFi.mode(WIFI_AP_STA); + + DebugF("IP address : "); Debugln(WiFi.softAPIP()); + DebugF("MAC address : "); Debugln(WiFi.softAPmacAddress()); + } + + // Set OTA parameters + ArduinoOTA.setPort(config.ota_port); + ArduinoOTA.setHostname(config.host); + ArduinoOTA.setPassword(config.ota_auth); + ArduinoOTA.begin(); + + // just in case your sketch sucks, keep update OTA Available + // Trust me, when coding and testing it happens, this could save + // the need to connect FTDI to reflash + // Usefull just after 1st connexion when called from setup() before + // launching potentially buggy main() + for (uint8_t i=0; i<= 10; i++) { + LedRGBON(COLOR_MAGENTA); + delay(100); + LedRGBOFF(); + delay(200); + ArduinoOTA.handle(); + } + + } // if setup + + return WiFi.status(); +} + +/* ====================================================================== +Function: setup +Purpose : Setup I/O and other one time startup stuff +Input : - +Output : - +Comments: - +====================================================================== */ +void setup() +{ + char buff[32]; + boolean reset_config = true; + + // Set CPU speed to 160MHz + system_update_cpu_freq(160); + + //WiFi.disconnect(false); + + // Set WiFi to station mode and disconnect from an AP if it was previously connected + //WiFi.mode(WIFI_AP_STA); + //WiFi.disconnect(); + //delay(1000); + + // Init the RGB Led, and set it off + rgb_led.Begin(); + LedRGBOFF(); + + // Init the serial 1, Our Debug Serial TXD0 + // note this serial can only transmit, just + // enough for debugging purpose + DEBUG_SERIAL.begin(115200); + Debugln(F("\r\n\r\n==============")); + Debug(F("WifInfo V")); + Debugln(F(WIFINFO_VERSION)); + Debugln(); + Debugflush(); + + // Clear our global flags + config.config = 0; + + // Our configuration is stored into EEPROM + //EEPROM.begin(sizeof(_Config)); + EEPROM.begin(1024); + + DebugF("Config size="); Debug(sizeof(_Config)); + DebugF(" (emoncms="); Debug(sizeof(_emoncms)); + DebugF(" jeedom="); Debug(sizeof(_jeedom)); + Debugln(')'); + Debugflush(); + + // Check File system init + if (!SPIFFS.begin()) + { + // Serious problem + DebuglnF("SPIFFS Mount failed"); + } else { + + DebuglnF("SPIFFS Mount succesfull"); + + Dir dir = SPIFFS.openDir("/"); + while (dir.next()) { + String fileName = dir.fileName(); + size_t fileSize = dir.fileSize(); + Debugf("FS File: %s, size: %d\n", fileName.c_str(), fileSize); + } + DebuglnF(""); + } + + // Read Configuration from EEP + if (readConfig()) { + DebuglnF("Good CRC, not set!"); + } else { + // Reset Configuration + ResetConfig(); + + // save back + saveConfig(); + + // Indicate the error in global flags + config.config |= CFG_BAD_CRC; + + DebuglnF("Reset to default"); + } + + // We'll drive our onboard LED + // old TXD1, not used anymore, has been swapped + pinMode(RED_LED_PIN, OUTPUT); + LedRedOFF(); + + // start Wifi connect or soft AP + WifiHandleConn(true); + + // OTA callbacks + ArduinoOTA.onStart([]() { + LedRGBON(COLOR_MAGENTA); + DebuglnF("Update Started"); + ota_blink = true; + }); + + ArduinoOTA.onEnd([]() { + LedRGBOFF(); + DebuglnF("Update finished restarting"); + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + if (ota_blink) { + LedRGBON(COLOR_MAGENTA); + } else { + LedRGBOFF(); + } + ota_blink = !ota_blink; + //Serial.printf("Progress: %u%%\n", (progress / (total / 100))); + }); + + ArduinoOTA.onError([](ota_error_t error) { + LedRGBON(COLOR_RED); + Debugf("Update Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) DebuglnF("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) DebuglnF("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) DebuglnF("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) DebuglnF("Receive Failed"); + else if (error == OTA_END_ERROR) DebuglnF("End Failed"); + ESP.restart(); + }); + + // Update sysinfo variable and print them + UpdateSysinfo(true, true); + + server.on("/", handleRoot); + server.on("/config_form.json", handleFormConfig); + server.on("/json", sendJSON); + server.on("/tinfo.json", tinfoJSONTable); + server.on("/system.json", sysJSONTable); + server.on("/config.json", confJSONTable); + server.on("/spiffs.json", spiffsJSONTable); + server.on("/wifiscan.json", wifiScanJSON); + server.on("/factory_reset", handleFactoryReset); + server.on("/reset", handleReset); + + // handler for the hearbeat + server.on("/hb.htm", HTTP_GET, [&](){ + server.sendHeader("Connection", "close"); + server.sendHeader("Access-Control-Allow-Origin", "*"); + server.send(200, "text/html", R"(OK)"); + }); + + // handler for the /update form POST (once file upload finishes) + server.on("/update", HTTP_POST, + // handler once file upload finishes + [&]() { + server.sendHeader("Connection", "close"); + server.sendHeader("Access-Control-Allow-Origin", "*"); + server.send(200, "text/plain", (Update.hasError())?"FAIL":"OK"); + ESP.restart(); + }, + // handler for upload, get's the sketch bytes, + // and writes them through the Update object + [&]() { + HTTPUpload& upload = server.upload(); + + if(upload.status == UPLOAD_FILE_START) { + uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + WiFiUDP::stopAll(); + Debugf("Update: %s\n", upload.filename.c_str()); + LedRGBON(COLOR_MAGENTA); + ota_blink = true; + + //start with max available size + if(!Update.begin(maxSketchSpace)) + Update.printError(Serial1); + + } else if(upload.status == UPLOAD_FILE_WRITE) { + if (ota_blink) { + LedRGBON(COLOR_MAGENTA); + } else { + LedRGBOFF(); + } + ota_blink = !ota_blink; + Debug("."); + if(Update.write(upload.buf, upload.currentSize) != upload.currentSize) + Update.printError(Serial1); + + } else if(upload.status == UPLOAD_FILE_END) { + //true to set the size to the current progress + if(Update.end(true)) + Debugf("Update Success: %u\nRebooting...\n", upload.totalSize); + else + Update.printError(Serial1); + + LedRGBOFF(); + + } else if(upload.status == UPLOAD_FILE_ABORTED) { + Update.end(); + LedRGBOFF(); + DebuglnF("Update was aborted"); + } + delay(0); + } + ); + + // All other not known + server.onNotFound(handleNotFound); + + // serves all SPIFFS Web file with 24hr max-age control + // to avoid multiple requests to ESP + server.serveStatic("/font", SPIFFS, "/font","max-age=86400"); + server.serveStatic("/js", SPIFFS, "/js" ,"max-age=86400"); + server.serveStatic("/css", SPIFFS, "/css" ,"max-age=86400"); + server.begin(); + + // Display configuration + showConfig(); + + Debugln(F("HTTP server started")); + + // Teleinfo is connected to RXD2 (GPIO13) to + // avoid conflict when flashing, this is why + // we swap RXD1/RXD1 to RXD2/TXD2 + // Note that TXD2 is not used teleinfo is receive only + #ifdef DEBUG_SERIAL1 + Serial.begin(1200, SERIAL_7E1); + Serial.swap(); + #endif + + // Init teleinfo + tinfo.init(); + + // Attach the callback we need + // set all as an example + tinfo.attachADPS(ADPSCallback); + tinfo.attachData(DataCallback); + tinfo.attachNewFrame(NewFrame); + tinfo.attachUpdatedFrame(UpdatedFrame); + + //webSocket.begin(); + //webSocket.onEvent(webSocketEvent); + + // Light off the RGB LED + LedRGBOFF(); + + // Update sysinfo every second + Every_1_Sec.attach(1, Task_1_Sec); + + // Emoncms Update if needed + if (config.emoncms.freq) + Tick_emoncms.attach(config.emoncms.freq, Task_emoncms); + + // Jeedom Update if needed + if (config.jeedom.freq) + Tick_jeedom.attach(config.jeedom.freq, Task_jeedom); +} + +/* ====================================================================== +Function: loop +Purpose : infinite loop main code +Input : - +Output : - +Comments: - +====================================================================== */ +void loop() +{ + char c; + + // Do all related network stuff + server.handleClient(); + ArduinoOTA.handle(); + + //webSocket.loop(); + + // Only once task per loop, let system do it own task + if (task_1_sec) { + UpdateSysinfo(false, false); + task_1_sec = false; + } else if (task_emoncms) { + emoncmsPost(); + task_emoncms=false; + } else if (task_jeedom) { + jeedomPost(); + task_jeedom=false; + } + + // Handle teleinfo serial + if ( Serial.available() ) { + // Read Serial and process to tinfo + c = Serial.read(); + //Serial1.print(c); + tinfo.process(c); + } + + //delay(10); +} + diff --git a/examples/ESP8266_WifInfo/config.cpp b/examples/Wifinfo/config.cpp similarity index 65% rename from examples/ESP8266_WifInfo/config.cpp rename to examples/Wifinfo/config.cpp index aec3c09..c8f1fa3 100644 --- a/examples/ESP8266_WifInfo/config.cpp +++ b/examples/Wifinfo/config.cpp @@ -58,11 +58,10 @@ void eepromDump(uint8_t bytesPerRow) Debugln(); // loop thru EEP address - for (i = 0; i <= sizeof(_Config); i++) { + for (i = 0; i < sizeof(_Config); i++) { // First byte of the row ? if (j==0) { // Display Address - Debug(buf); Debugf("%04X : ", i); } @@ -81,18 +80,18 @@ void eepromDump(uint8_t bytesPerRow) /* ====================================================================== Function: readConfig Purpose : fill config structure with data located into eeprom -Input : - +Input : true if we need to clear actual struc in case of error Output : true if config found and crc ok, false otherwise Comments: - ====================================================================== */ -bool readConfig (void) +bool readConfig (bool clear_on_error) { uint16_t crc = ~0; uint8_t * pconfig = (uint8_t *) &config ; uint8_t data ; // For whole size of config structure - for (uint8_t i = 0; i < sizeof(_Config); ++i) { + for (uint16_t i = 0; i < sizeof(_Config); ++i) { // read data data = EEPROM.read(i); @@ -105,8 +104,9 @@ bool readConfig (void) // CRC Error ? if (crc != 0) { - // Clear config - memset(&config, 0, sizeof( _Config )); + // Clear config if wanted + if (clear_on_error) + memset(&config, 0, sizeof( _Config )); return false; } @@ -125,6 +125,8 @@ bool saveConfig (void) uint8_t * pconfig ; bool ret_code; + //eepromDump(32); + // Init pointer pconfig = (uint8_t *) &config ; @@ -132,28 +134,32 @@ bool saveConfig (void) config.crc = ~0; // For whole size of config structure, pre-calculate CRC - for (uint8_t i = 0; i < sizeof (_Config) - 2; ++i) + for (uint16_t i = 0; i < sizeof (_Config) - 2; ++i) config.crc = crc16Update(config.crc, *pconfig++); // Re init pointer pconfig = (uint8_t *) &config ; - // For whole size of config structure, write to EEP - for (byte i = 0; i < sizeof(_Config); ++i) - EEPROM.write(i, *pconfig++); + // For whole size of config structure, write to EEP + for (uint16_t i = 0; i < sizeof(_Config); ++i) + EEPROM.write(i, *pconfig++); // Physically save EEPROM.commit(); - // Read Again to see if saved ok - ret_code = readConfig(); + // Read Again to see if saved ok, but do + // not clear if error this avoid clearing + // default config and breaks OTA + ret_code = readConfig(false); - Debug(F("Write config")); + Debug(F("Write config ")); if (ret_code) Debugln(F("OK!")); else Debugln(F("Error!")); + + //eepromDump(32); // return result return (ret_code); @@ -168,6 +174,32 @@ Comments: - ====================================================================== */ void showConfig() { - + DebuglnF("===== Wifi"); + DebugF("ssid :"); Debugln(config.ssid); + DebugF("psk :"); Debugln(config.psk); + DebugF("host :"); Debugln(config.host); + DebugF("ap_psk :"); Debugln(config.ap_psk); + DebugF("OTA auth :"); Debugln(config.ota_auth); + DebugF("OTA port :"); Debugln(config.ota_port); + DebugF("Config :"); + if (config.config & CFG_RGB_LED) DebugF(" RGB"); + if (config.config & CFG_DEBUG) DebugF(" DEBUG"); + if (config.config & CFG_LCD) DebugF(" LCD"); + + DebuglnF("\r\n===== Emoncms"); + DebugF("host :"); Debugln(config.emoncms.host); + DebugF("port :"); Debugln(config.emoncms.port); + DebugF("url :"); Debugln(config.emoncms.url); + DebugF("key :"); Debugln(config.emoncms.apikey); + DebugF("node :"); Debugln(config.emoncms.node); + DebugF("freq :"); Debugln(config.emoncms.freq); + + DebuglnF("\r\n===== Jeedom"); + DebugF("host :"); Debugln(config.jeedom.host); + DebugF("port :"); Debugln(config.jeedom.port); + DebugF("url :"); Debugln(config.jeedom.url); + DebugF("key :"); Debugln(config.jeedom.apikey); + DebugF("compteur :"); Debugln(config.jeedom.adco); + DebugF("freq :"); Debugln(config.jeedom.freq); } diff --git a/examples/Wifinfo/config.h b/examples/Wifinfo/config.h new file mode 100644 index 0000000..6f9b833 --- /dev/null +++ b/examples/Wifinfo/config.h @@ -0,0 +1,148 @@ +// ********************************************************************************** +// ESP8266 Teleinfo WEB Server configuration Include file +// ********************************************************************************** +// Creative Commons Attrib Share-Alike License +// You are free to use/extend this library but please abide with the CC-BY-SA license: +// Attribution-NonCommercial-ShareAlike 4.0 International License +// http://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// For any explanation about teleinfo ou use , see my blog +// http://hallard.me/category/tinfo +// +// This program works with the Wifinfo board +// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo +// +// Written by Charles-Henri Hallard (http://hallard.me) +// +// History : V1.00 2015-06-14 - First release +// +// All text above must be included in any redistribution. +// +// ********************************************************************************** +#ifndef __CONFIG_H__ +#define __CONFIG_H__ + +// Include main project include file +#include "Wifinfo.h" + +#define CFG_SSID_SIZE 32 +#define CFG_PSK_SIZE 64 +#define CFG_HOSTNAME_SIZE 16 + +#define CFG_EMON_HOST_SIZE 32 +#define CFG_EMON_APIKEY_SIZE 32 +#define CFG_EMON_URL_SIZE 32 +#define CFG_EMON_DEFAULT_PORT 80 +#define CFG_EMON_DEFAULT_HOST "emoncms.org" +#define CFG_EMON_DEFAULT_URL "/input/post.json" + +#define CFG_JDOM_HOST_SIZE 32 +#define CFG_JDOM_APIKEY_SIZE 32 +#define CFG_JDOM_URL_SIZE 64 +#define CFG_JDOM_ADCO_SIZE 12 +#define CFG_JDOM_DEFAULT_PORT 80 +#define CFG_JDOM_DEFAULT_HOST "jeedom.local" +#define CFG_JDOM_DEFAULT_URL "/jeedom/plugins/teleinfo/core/php/jeeTeleinfo.php" +#define CFG_JDOM_DEFAULT_ADCO "0000111122223333" + + +// Port pour l'OTA +#define DEFAULT_OTA_PORT 8266 +#define DEFAULT_OTA_AUTH "OTA_WifInfo" +//#define DEFAULT_OTA_AUTH "" + +// Bit definition for different configuration modes +#define CFG_LCD 0x0001 // Enable display +#define CFG_DEBUG 0x0002 // Enable serial debug +#define CFG_RGB_LED 0x0004 // Enable RGB LED +#define CFG_BAD_CRC 0x8000 // Bad CRC when reading configuration + +// Web Interface Configuration Form field names +#define CFG_FORM_SSID FPSTR("ssid") +#define CFG_FORM_PSK FPSTR("psk") +#define CFG_FORM_HOST FPSTR("host") +#define CFG_FORM_AP_PSK FPSTR("ap_psk") +#define CFG_FORM_OTA_AUTH FPSTR("ota_auth") +#define CFG_FORM_OTA_PORT FPSTR("ota_port") + +#define CFG_FORM_EMON_HOST FPSTR("emon_host") +#define CFG_FORM_EMON_PORT FPSTR("emon_port") +#define CFG_FORM_EMON_URL FPSTR("emon_url") +#define CFG_FORM_EMON_KEY FPSTR("emon_apikey") +#define CFG_FORM_EMON_NODE FPSTR("emon_node") +#define CFG_FORM_EMON_FREQ FPSTR("emon_freq") + +#define CFG_FORM_JDOM_HOST FPSTR("jdom_host") +#define CFG_FORM_JDOM_PORT FPSTR("jdom_port") +#define CFG_FORM_JDOM_URL FPSTR("jdom_url") +#define CFG_FORM_JDOM_KEY FPSTR("jdom_apikey") +#define CFG_FORM_JDOM_ADCO FPSTR("jdom_adco") +#define CFG_FORM_JDOM_FREQ FPSTR("jdom_freq") + +#define CFG_FORM_IP FPSTR("wifi_ip"); +#define CFG_FORM_GW FPSTR("wifi_gw"); +#define CFG_FORM_MSK FPSTR("wifi_msk"); + +#pragma pack(push) // push current alignment to stack +#pragma pack(1) // set alignment to 1 byte boundary + +// Config for emoncms +// 128 Bytes +typedef struct +{ + char host[CFG_EMON_HOST_SIZE+1]; // FQDN + char apikey[CFG_EMON_APIKEY_SIZE+1]; // Secret + char url[CFG_EMON_URL_SIZE+1]; // Post URL + uint8_t port; // Protocol port (HTTP/HTTPS) + uint8_t node; // optional node + uint32_t freq; // refresh rate + uint8_t filler[23]; // in case adding data in config avoiding loosing current conf by bad crc*/ +} _emoncms; + +// Config for jeedom +// 160 Bytes +typedef struct +{ + char host[CFG_JDOM_HOST_SIZE+1]; // FQDN + char apikey[CFG_JDOM_APIKEY_SIZE+1]; // Secret + char url[CFG_JDOM_URL_SIZE+1]; // Post URL + char adco[CFG_JDOM_ADCO_SIZE+1]; // Identifiant compteur + uint8_t port; // Protocol port (HTTP/HTTPS) + uint32_t freq; // refresh rate + uint8_t filler[11]; // in case adding data in config avoiding loosing current conf by bad crc*/ +} _jeedom; + +// Config saved into eeprom +// 1024 bytes total including CRC +typedef struct +{ + char ssid[CFG_SSID_SIZE+1]; // SSID + char psk[CFG_PSK_SIZE+1]; // Pre shared key + char host[CFG_HOSTNAME_SIZE+1]; // Hostname + char ap_psk[CFG_PSK_SIZE+1]; // Access Point Pre shared key + char ota_auth[CFG_PSK_SIZE+1]; // OTA Authentication password + uint32_t config; // Bit field register + uint16_t ota_port; // OTA port + uint8_t filler[131]; // in case adding data in config avoiding loosing current conf by bad crc + _emoncms emoncms; // Emoncms configuration + _jeedom jeedom; // jeedom configuration + uint8_t filler1[352]; // Another filler in case we need more + uint16_t crc; +} _Config; + + +// Exported variables/object instancied in main sketch +// =================================================== +extern _Config config; + +#pragma pack(pop) + +// Declared exported function from route.cpp +// =================================================== +bool readConfig(bool clear_on_error=true); +bool saveConfig(void); +void showConfig(void); + + +#endif + diff --git a/examples/Wifinfo/data/css/wifinfo.css.gz b/examples/Wifinfo/data/css/wifinfo.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..67b07b845dd8a82860013ef075f239e14dce6a42 GIT binary patch literal 21983 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%0DPV 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 literal 0 HcmV?d00001 diff --git a/examples/Wifinfo/data/favicon.ico b/examples/Wifinfo/data/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..20da666c87707abc0701cecdf209979529953425 GIT binary patch literal 1150 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 G`u_lkUM?>H literal 0 HcmV?d00001 diff --git a/examples/Wifinfo/data/fonts/glyphicons.woff2 b/examples/Wifinfo/data/fonts/glyphicons.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..64539b54c3751a6d9adb44c8e3a45ba5a73b77f0 GIT binary patch literal 18028 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 literal 0 HcmV?d00001 diff --git a/examples/Wifinfo/data/index.htm.gz b/examples/Wifinfo/data/index.htm.gz new file mode 100644 index 0000000000000000000000000000000000000000..a42c9d7daef8c2cb0f3a4fd8fc6627a962787171 GIT binary patch literal 8215 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 JKG>F!006IQ+=u`G literal 0 HcmV?d00001 diff --git a/examples/Wifinfo/data/js/wifinfo.js.gz b/examples/Wifinfo/data/js/wifinfo.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..b975dcfb4870a6624a1d69a97de34b0f003a71f6 GIT binary patch literal 56176 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 literal 0 HcmV?d00001 diff --git a/examples/Wifinfo/webclient.cpp b/examples/Wifinfo/webclient.cpp new file mode 100644 index 0000000..10ec024 --- /dev/null +++ b/examples/Wifinfo/webclient.cpp @@ -0,0 +1,241 @@ +// ********************************************************************************** +// ESP8266 Teleinfo WEB Client, web server function +// ********************************************************************************** +// Creative Commons Attrib Share-Alike License +// You are free to use/extend this library but please abide with the CC-BY-SA license: +// Attribution-NonCommercial-ShareAlike 4.0 International License +// http://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// For any explanation about teleinfo ou use, see my blog +// http://hallard.me/category/tinfo +// +// This program works with the Wifinfo board +// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo +// +// Written by Charles-Henri Hallard (http://hallard.me) +// +// History : V1.00 2015-12-04 - First release +// +// All text above must be included in any redistribution. +// +// ********************************************************************************** + +#include "webclient.h" + +/* ====================================================================== +Function: httpPost +Purpose : Do a http post +Input : hostname + port + url +Output : true if received 200 OK +Comments: - +====================================================================== */ +boolean httpPost(char * host, uint16_t port, char * url) +{ + HTTPClient http; + bool ret = false; + + unsigned long start = millis(); + + // configure traged server and url + http.begin(host, port, url, port==443 ? true : false); + //http.begin("http://emoncms.org/input/post.json?node=20&apikey=2f13e4608d411d20354485f72747de7b&json={PAPP:100}"); + //http.begin("emoncms.org", 80, "/input/post.json?node=20&apikey=2f13e4608d411d20354485f72747de7b&json={}"); //HTTP + + Debugf("http%s://%s:%d%s => ", port==443?"s":"", host, port, url); + + // start connection and send HTTP header + int httpCode = http.GET(); + if(httpCode) { + // HTTP header has been send and Server response header has been handled + Debug(httpCode); + Debug(" "); + // file found at server + if(httpCode == 200) { + String payload = http.getString(); + Debug(payload); + ret = true; + } + } else { + DebugF("failed!"); + } + Debugf(" in %d ms\r\n",millis()-start); + return ret; +} + +/* ====================================================================== +Function: emoncmsPost +Purpose : Do a http post to emoncms +Input : +Output : true if post returned 200 OK +Comments: - +====================================================================== */ +boolean emoncmsPost(void) +{ + boolean ret = false; + + // Some basic checking + if (*config.emoncms.host) { + ValueList * me = tinfo.getList(); + // Got at least one ? + if (me && me->next) { + String url ; + boolean first_item; + + url = *config.emoncms.url ? config.emoncms.url : "/"; + url += "?"; + if (config.emoncms.node>0) { + url+= F("node="); + url+= String(config.emoncms.node); + url+= "&"; + } + + url += F("apikey=") ; + url += config.emoncms.apikey; + url += F("&json={") ; + + first_item = true; + + // Loop thru the node + while (me->next) { + // go to next node + me = me->next; + // First item do not add , separator + if (first_item) + first_item = false; + else + url += ","; + + url += me->name ; + url += ":" ; + + // EMONCMS ne sais traiter que des valeurs numériques, donc ici il faut faire une + // table de mappage, tout à fait arbitraire, mais c"est celle-ci dont je me sers + // depuis mes débuts avec la téléinfo + if (!strcmp(me->name, "OPTARIF")) { + // L'option tarifaire choisie (Groupe "OPTARIF") est codée sur 4 caractères alphanumériques + /* J'ai pris un nombre arbitraire codé dans l'ordre ci-dessous + je mets le 4eme char à 0, trop de possibilités + BASE => Option Base. + HC.. => Option Heures Creuses. + EJP. => Option EJP. + BBRx => Option Tempo + */ + char * p = me->value; + + if (*p=='B'&&*(p+1)=='A'&&*(p+2)=='S') url += "1"; + else if (*p=='H'&&*(p+1)=='C'&&*(p+2)=='.') url += "2"; + else if (*p=='E'&&*(p+1)=='J'&&*(p+2)=='P') url += "3"; + else if (*p=='B'&&*(p+1)=='B'&&*(p+2)=='R') url += "4"; + else url +="0"; + } else if (!strcmp(me->name, "HHPHC")) { + // L'horaire heures pleines/heures creuses (Groupe "HHPHC") est codé par un caractère A à Y + // J'ai choisi de prendre son code ASCII + int code = *me->value; + url += String(code); + } else if (!strcmp(me->name, "PTEC")) { + // La période tarifaire en cours (Groupe "PTEC"), est codée sur 4 caractères + /* J'ai pris un nombre arbitraire codé dans l'ordre ci-dessous + TH.. => Toutes les Heures. + HC.. => Heures Creuses. + HP.. => Heures Pleines. + HN.. => Heures Normales. + PM.. => Heures de Pointe Mobile. + HCJB => Heures Creuses Jours Bleus. + HCJW => Heures Creuses Jours Blancs (White). + HCJR => Heures Creuses Jours Rouges. + HPJB => Heures Pleines Jours Bleus. + HPJW => Heures Pleines Jours Blancs (White). + HPJR => Heures Pleines Jours Rouges. + */ + if (!strcmp(me->value, "TH..")) url += "1"; + else if (!strcmp(me->value, "HC..")) url += "2"; + else if (!strcmp(me->value, "HP..")) url += "3"; + else if (!strcmp(me->value, "HN..")) url += "4"; + else if (!strcmp(me->value, "PM..")) url += "5"; + else if (!strcmp(me->value, "HCJB")) url += "6"; + else if (!strcmp(me->value, "HCJW")) url += "7"; + else if (!strcmp(me->value, "HCJR")) url += "8"; + else if (!strcmp(me->value, "HPJB")) url += "9"; + else if (!strcmp(me->value, "HPJW")) url += "10"; + else if (!strcmp(me->value, "HPJR")) url += "11"; + else url +="0"; + } else { + url += me->value; + } + } // While me + + // Json end + url += "}"; + + ret = httpPost( config.emoncms.host, config.emoncms.port, (char *) url.c_str()) ; + } // if me + } // if host + return ret; +} + +/* ====================================================================== +Function: jeedomPost +Purpose : Do a http post to jeedom server +Input : +Output : true if post returned 200 OK +Comments: - +====================================================================== */ +boolean jeedomPost(void) +{ + boolean ret = false; + + // Some basic checking + if (*config.jeedom.host) { + ValueList * me = tinfo.getList(); + // Got at least one ? + if (me && me->next) { + String url ; + boolean skip_item; + + url = *config.jeedom.url ? config.jeedom.url : "/"; + url += "?"; + + // Config identifiant forcée ? + if (*config.jeedom.adco) { + url+= F("ADCO="); + url+= config.jeedom.adco; + url+= "&"; + } + + url += F("api=") ; + url += config.jeedom.apikey; + url += F("&") ; + + // Loop thru the node + while (me->next) { + // go to next node + me = me->next; + skip_item = false; + + // Si ADCO déjà renseigné, on le remet pas + if (!strcmp(me->name, "ADCO")) { + if (*config.jeedom.adco) + skip_item = true; + } + + // Si Item virtuel, on le met pas + if (*me->name =='_') + skip_item = true; + + // On doit ajouter l'item ? + if (!skip_item) { + url += me->name ; + url += "=" ; + url += me->value; + url += "&" ; + } + } // While me + + ret = httpPost( config.jeedom.host, config.jeedom.port, (char *) url.c_str()) ; + } // if me + } // if host + return ret; +} + diff --git a/examples/Wifinfo/webclient.h b/examples/Wifinfo/webclient.h new file mode 100644 index 0000000..7a42e0b --- /dev/null +++ b/examples/Wifinfo/webclient.h @@ -0,0 +1,38 @@ +// ********************************************************************************** +// ESP8266 Teleinfo WEB Client routing Include file +// ********************************************************************************** +// Creative Commons Attrib Share-Alike License +// You are free to use/extend this library but please abide with the CC-BY-SA license: +// Attribution-NonCommercial-ShareAlike 4.0 International License +// http://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// For any explanation about teleinfo ou use , see my blog +// http://hallard.me/category/tinfo +// +// This program works with the Wifinfo board +// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo +// +// Written by Charles-Henri Hallard (http://hallard.me) +// +// History : V1.00 2015-12-04 - First release +// +// All text above must be included in any redistribution. +// +// ********************************************************************************** + +#ifndef WEBCLIENT_H +#define WEBCLIENT_H + +// Include main project include file +#include "Wifinfo.h" + +// Exported variables/object instancied in main sketch +// =================================================== + +// declared exported function from route.cpp +// =================================================== +boolean httpPost(char * host, uint16_t port, char * url); +boolean emoncmsPost(void); +boolean jeedomPost(void); + +#endif diff --git a/examples/Wifinfo/webserver.cpp b/examples/Wifinfo/webserver.cpp new file mode 100644 index 0000000..984a09c --- /dev/null +++ b/examples/Wifinfo/webserver.cpp @@ -0,0 +1,812 @@ +// ********************************************************************************** +// ESP8266 Teleinfo WEB Server, route web function +// ********************************************************************************** +// Creative Commons Attrib Share-Alike License +// You are free to use/extend this library but please abide with the CC-BY-SA license: +// Attribution-NonCommercial-ShareAlike 4.0 International License +// http://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// For any explanation about teleinfo ou use, see my blog +// http://hallard.me/category/tinfo +// +// This program works with the Wifinfo board +// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo +// +// Written by Charles-Henri Hallard (http://hallard.me) +// +// History : V1.00 2015-06-14 - First release +// +// All text above must be included in any redistribution. +// +// ********************************************************************************** + +// Include Arduino header +#include "webserver.h" + +// Optimize string space in flash, avoid duplication +const char FP_JSON_START[] PROGMEM = "{\r\n"; +const char FP_JSON_END[] PROGMEM = "\r\n}\r\n"; +const char FP_QCQ[] PROGMEM = "\":\""; +const char FP_QCNL[] PROGMEM = "\",\r\n\""; +const char FP_RESTART[] PROGMEM = "OK, Redémarrage en cours\r\n"; +const char FP_NL[] PROGMEM = "\r\n"; + +/* ====================================================================== +Function: formatSize +Purpose : format a asize to human readable format +Input : size +Output : formated string +Comments: - +====================================================================== */ +String formatSize(size_t bytes) +{ + if (bytes < 1024){ + return String(bytes) + F(" Byte"); + } else if(bytes < (1024 * 1024)){ + return String(bytes/1024.0) + F(" KB"); + } else if(bytes < (1024 * 1024 * 1024)){ + return String(bytes/1024.0/1024.0) + F(" MB"); + } else { + return String(bytes/1024.0/1024.0/1024.0) + F(" GB"); + } +} + +/* ====================================================================== +Function: getContentType +Purpose : return correct mime content type depending on file extension +Input : - +Output : Mime content type +Comments: - +====================================================================== */ +String getContentType(String filename) { + if(filename.endsWith(".htm")) return F("text/html"); + else if(filename.endsWith(".html")) return F("text/html"); + else if(filename.endsWith(".css")) return F("text/css"); + else if(filename.endsWith(".json")) return F("text/json"); + else if(filename.endsWith(".js")) return F("application/javascript"); + else if(filename.endsWith(".png")) return F("image/png"); + else if(filename.endsWith(".gif")) return F("image/gif"); + else if(filename.endsWith(".jpg")) return F("image/jpeg"); + else if(filename.endsWith(".ico")) return F("image/x-icon"); + else if(filename.endsWith(".xml")) return F("text/xml"); + else if(filename.endsWith(".pdf")) return F("application/x-pdf"); + else if(filename.endsWith(".zip")) return F("application/x-zip"); + else if(filename.endsWith(".gz")) return F("application/x-gzip"); + else if(filename.endsWith(".otf")) return F("application/x-font-opentype"); + else if(filename.endsWith(".eot")) return F("application/vnd.ms-fontobject"); + else if(filename.endsWith(".svg")) return F("image/svg+xml"); + else if(filename.endsWith(".woff")) return F("application/x-font-woff"); + else if(filename.endsWith(".woff2")) return F("application/x-font-woff2"); + else if(filename.endsWith(".ttf")) return F("application/x-font-ttf"); + return "text/plain"; +} + +/* ====================================================================== +Function: handleFileRead +Purpose : return content of a file stored on SPIFFS file system +Input : file path +Output : true if file found and sent +Comments: - +====================================================================== */ +bool handleFileRead(String path) { + if ( path.endsWith("/") ) + path += "index.htm"; + + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + + DebugF("handleFileRead "); + Debug(path); + + if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + if( SPIFFS.exists(pathWithGz) ){ + path += ".gz"; + DebugF(".gz"); + } + + DebuglnF(" found on FS"); + + File file = SPIFFS.open(path, "r"); + size_t sent = server.streamFile(file, contentType); + file.close(); + return true; + } + + Debugln(""); + + server.send(404, "text/plain", "File Not Found"); + return false; +} + +/* ====================================================================== +Function: handleFormConfig +Purpose : handle main configuration page +Input : - +Output : - +Comments: - +====================================================================== */ +void handleFormConfig(void) +{ + String response=""; + int ret ; + + LedBluON(); + + // We validated config ? + if (server.hasArg("save")) + { + int itemp; + DebuglnF("===== Posted configuration"); + + // WifInfo + strncpy(config.ssid , server.arg("ssid").c_str(), CFG_SSID_SIZE ); + strncpy(config.psk , server.arg("psk").c_str(), CFG_PSK_SIZE ); + strncpy(config.host , server.arg("host").c_str(), CFG_HOSTNAME_SIZE ); + strncpy(config.ap_psk , server.arg("ap_psk").c_str(), CFG_PSK_SIZE ); + strncpy(config.ota_auth,server.arg("ota_auth").c_str(), CFG_PSK_SIZE ); + itemp = server.arg("ota_port").toInt(); + config.ota_port = (itemp>=0 && itemp<=65535) ? itemp : DEFAULT_OTA_PORT ; + + // Emoncms + strncpy(config.emoncms.host, server.arg("emon_host").c_str(), CFG_EMON_HOST_SIZE ); + strncpy(config.emoncms.url, server.arg("emon_url").c_str(), CFG_EMON_URL_SIZE ); + strncpy(config.emoncms.apikey, server.arg("emon_apikey").c_str(),CFG_EMON_APIKEY_SIZE ); + itemp = server.arg("emon_node").toInt(); + config.emoncms.node = (itemp>=0 && itemp<=255) ? itemp : 0 ; + itemp = server.arg("emon_port").toInt(); + config.emoncms.port = (itemp>=0 && itemp<=65535) ? itemp : CFG_EMON_DEFAULT_PORT ; + itemp = server.arg("emon_freq").toInt(); + if (itemp>0 && itemp<=86400){ + // Emoncms Update if needed + Tick_emoncms.detach(); + Tick_emoncms.attach(itemp, Task_emoncms); + } else { + itemp = 0 ; + } + config.emoncms.freq = itemp; + + // jeedom + strncpy(config.jeedom.host, server.arg("jdom_host").c_str(), CFG_JDOM_HOST_SIZE ); + strncpy(config.jeedom.url, server.arg("jdom_url").c_str(), CFG_JDOM_URL_SIZE ); + strncpy(config.jeedom.apikey, server.arg("jdom_apikey").c_str(),CFG_JDOM_APIKEY_SIZE ); + strncpy(config.jeedom.adco, server.arg("jdom_adco").c_str(),CFG_JDOM_ADCO_SIZE ); + itemp = server.arg("jdom_port").toInt(); + config.jeedom.port = (itemp>=0 && itemp<=65535) ? itemp : CFG_JDOM_DEFAULT_PORT ; + itemp = server.arg("jdom_freq").toInt(); + if (itemp>0 && itemp<=86400){ + // Emoncms Update if needed + Tick_jeedom.detach(); + Tick_jeedom.attach(itemp, Task_jeedom); + } else { + itemp = 0 ; + } + config.jeedom.freq = itemp; + + if ( saveConfig() ) { + ret = 200; + response = PSTR("OK"); + } else { + ret = 412; + response = PSTR("Unable to save configuration"); + } + + showConfig(); + } + else + { + ret = 400; + response = PSTR("Missing Form Field"); + } + + DebugF("Sending response "); + Debug(ret); + Debug(":"); + Debugln(response); + server.send ( ret, "text/plain", response); + LedBluOFF(); +} + +/* ====================================================================== +Function: handleRoot +Purpose : handle main page / +Input : - +Output : - +Comments: - +====================================================================== */ +void handleRoot(void) +{ + LedBluON(); + handleFileRead("/"); + LedBluOFF(); +} + +/* ====================================================================== +Function: formatNumberJSON +Purpose : check if data value is full number and send correct JSON format +Input : String where to add response + char * value to check +Output : - +Comments: 00150 => 150 + ADCO => "ADCO" + 1 => 1 +====================================================================== */ +void formatNumberJSON( String &response, char * value) +{ + // we have at least something ? + if (value && strlen(value)) + { + boolean isNumber = true; + uint8_t c; + char * p = value; + + // just to be sure + if (strlen(p)<=16) { + // check if value is number + while (*p && isNumber) { + if ( *p < '0' || *p > '9' ) + isNumber = false; + p++; + } + + // this will add "" on not number values + if (!isNumber) { + response += '\"' ; + response += value ; + response += F("\"") ; + } else { + // this will remove leading zero on numbers + p = value; + while (*p=='0' && *(p+1) ) + p++; + response += p ; + } + } else { + Debugln(F("formatNumberJSON error!")); + } + } +} + + +/* ====================================================================== +Function: tinfoJSONTable +Purpose : dump all teleinfo values in JSON table format for browser +Input : linked list pointer on the concerned data + true to dump all values, false for only modified ones +Output : - +Comments: - +====================================================================== */ +void tinfoJSONTable(void) +{ + ValueList * me = tinfo.getList(); + String response = ""; + + // Just to debug where we are + Debug(F("Serving /tinfo page...\r\n")); + + // Got at least one ? + if (me) { + uint8_t index=0; + + boolean first_item = true; + // Json start + response += F("[\r\n"); + + // Loop thru the node + while (me->next) { + + // we're there + ESP.wdtFeed(); + + // go to next node + me = me->next; + + // First item do not add , separator + if (first_item) + first_item = false; + else + response += F(",\r\n"); + +/* + Debug(F("(")) ; + Debug(++index) ; + Debug(F(") ")) ; + + if (me->name) Debug(me->name) ; + else Debug(F("NULL")) ; + + Debug(F("=")) ; + + if (me->value) Debug(me->value) ; + else Debug(F("NULL")) ; + + Debug(F(" '")) ; + Debug(me->checksum) ; + Debug(F("' ")); + + // Flags management + if ( me->flags) { + Debug(F("Flags:0x")); + Debugf("%02X => ", me->flags); + if ( me->flags & TINFO_FLAGS_EXIST) + Debug(F("Exist ")) ; + if ( me->flags & TINFO_FLAGS_UPDATED) + Debug(F("Updated ")) ; + if ( me->flags & TINFO_FLAGS_ADDED) + Debug(F("New ")) ; + } + + Debugln() ; +*/ + response += F("{\"na\":\""); + response += me->name ; + response += F("\", \"va\":\"") ; + response += me->value; + response += F("\", \"ck\":\"") ; + if (me->checksum == '"' || me->checksum == '\\' || me->checksum == '/') + response += '\\'; + response += (char) me->checksum; + response += F("\", \"fl\":"); + response += me->flags ; + response += '}' ; + + } + // Json end + response += F("\r\n]"); + + } else { + Debugln(F("sending 404...")); + server.send ( 404, "text/plain", "No data" ); + } + Debug(F("sending...")); + server.send ( 200, "text/json", response ); + Debugln(F("OK!")); +} + + + + + +/* ====================================================================== +Function: getSysJSONData +Purpose : Return JSON string containing system data +Input : Response String +Output : - +Comments: - +====================================================================== */ +void getSysJSONData(String & response) +{ + response = ""; + char buffer[32]; + int32_t adc = ( 1000 * analogRead(A0) / 1024 ); + + // Json start + response += F("[\r\n"); + + response += "{\"na\":\"Uptime\",\"va\":\""; + response += sysinfo.sys_uptime; + response += "\"},\r\n"; + + response += "{\"na\":\"WifInfo Version\",\"va\":\"" WIFINFO_VERSION "\"},\r\n"; + + response += "{\"na\":\"Compile le\",\"va\":\"" __DATE__ " " __TIME__ "\"},\r\n"; + + response += "{\"na\":\"SDK Version\",\"va\":\""; + response += system_get_sdk_version() ; + response += "\"},\r\n"; + + response += "{\"na\":\"Chip ID\",\"va\":\""; + sprintf_P(buffer, "0x%0X",system_get_chip_id() ); + response += buffer ; + response += "\"},\r\n"; + + response += "{\"na\":\"Boot Version\",\"va\":\""; + sprintf_P(buffer, "0x%0X",system_get_boot_version() ); + response += buffer ; + response += "\"},\r\n"; + + response += "{\"na\":\"Flash Real Size\",\"va\":\""; + response += formatSize(ESP.getFlashChipRealSize()) ; + response += "\"},\r\n"; + + response += "{\"na\":\"Firmware Size\",\"va\":\""; + response += formatSize(ESP.getSketchSize()) ; + response += "\"},\r\n"; + + response += "{\"na\":\"Free Size\",\"va\":\""; + response += formatSize(ESP.getFreeSketchSpace()) ; + response += "\"},\r\n"; + + response += "{\"na\":\"Analog\",\"va\":\""; + adc = ( (1000 * analogRead(A0)) / 1024); + sprintf_P( buffer, PSTR("%d mV"), adc); + response += buffer ; + response += "\"},\r\n"; + + FSInfo info; + SPIFFS.info(info); + + response += "{\"na\":\"SPIFFS Total\",\"va\":\""; + response += formatSize(info.totalBytes) ; + response += "\"},\r\n"; + + response += "{\"na\":\"SPIFFS Used\",\"va\":\""; + response += formatSize(info.usedBytes) ; + response += "\"},\r\n"; + + response += "{\"na\":\"SPIFFS Occupation\",\"va\":\""; + sprintf_P(buffer, "%d%%",100*info.usedBytes/info.totalBytes); + response += buffer ; + response += "\"},\r\n"; + + // Free mem should be last one + response += "{\"na\":\"Free Ram\",\"va\":\""; + response += formatSize(system_get_free_heap_size()) ; + response += "\"}\r\n"; // Last don't have comma at end + + // Json end + response += F("]\r\n"); +} + +/* ====================================================================== +Function: sysJSONTable +Purpose : dump all sysinfo values in JSON table format for browser +Input : - +Output : - +Comments: - +====================================================================== */ +void sysJSONTable() +{ + String response = ""; + + getSysJSONData(response); + + // Just to debug where we are + Debug(F("Serving /system page...")); + server.send ( 200, "text/json", response ); + Debugln(F("Ok!")); +} + + + +/* ====================================================================== +Function: getConfigJSONData +Purpose : Return JSON string containing configuration data +Input : Response String +Output : - +Comments: - +====================================================================== */ +void getConfJSONData(String & r) +{ + // Json start + r = FPSTR(FP_JSON_START); + + r+="\""; + r+=CFG_FORM_SSID; r+=FPSTR(FP_QCQ); r+=config.ssid; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_PSK; r+=FPSTR(FP_QCQ); r+=config.psk; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_HOST; r+=FPSTR(FP_QCQ); r+=config.host; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_AP_PSK; r+=FPSTR(FP_QCQ); r+=config.ap_psk; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_HOST; r+=FPSTR(FP_QCQ); r+=config.emoncms.host; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_PORT; r+=FPSTR(FP_QCQ); r+=config.emoncms.port; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_URL; r+=FPSTR(FP_QCQ); r+=config.emoncms.url; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_KEY; r+=FPSTR(FP_QCQ); r+=config.emoncms.apikey; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_NODE; r+=FPSTR(FP_QCQ); r+=config.emoncms.node; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_EMON_FREQ; r+=FPSTR(FP_QCQ); r+=config.emoncms.freq; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_OTA_AUTH; r+=FPSTR(FP_QCQ); r+=config.ota_auth; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_OTA_PORT; r+=FPSTR(FP_QCQ); r+=config.ota_port; r+= FPSTR(FP_QCNL); + + r+=CFG_FORM_JDOM_HOST; r+=FPSTR(FP_QCQ); r+=config.jeedom.host; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_JDOM_PORT; r+=FPSTR(FP_QCQ); r+=config.jeedom.port; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_JDOM_URL; r+=FPSTR(FP_QCQ); r+=config.jeedom.url; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_JDOM_KEY; r+=FPSTR(FP_QCQ); r+=config.jeedom.apikey; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_JDOM_ADCO; r+=FPSTR(FP_QCQ); r+=config.jeedom.adco; r+= FPSTR(FP_QCNL); + r+=CFG_FORM_JDOM_FREQ; r+=FPSTR(FP_QCQ); r+=config.jeedom.freq; + + r+= F("\""); + // Json end + r += FPSTR(FP_JSON_END); + +} + +/* ====================================================================== +Function: confJSONTable +Purpose : dump all config values in JSON table format for browser +Input : - +Output : - +Comments: - +====================================================================== */ +void confJSONTable() +{ + String response = ""; + getConfJSONData(response); + // Just to debug where we are + Debug(F("Serving /config page...")); + server.send ( 200, "text/json", response ); + Debugln(F("Ok!")); +} + +/* ====================================================================== +Function: getSpiffsJSONData +Purpose : Return JSON string containing list of SPIFFS files +Input : Response String +Output : - +Comments: - +====================================================================== */ +void getSpiffsJSONData(String & response) +{ + char buffer[32]; + bool first_item = true; + + // Json start + response = FPSTR(FP_JSON_START); + + // Files Array + response += F("\"files\":[\r\n"); + + // Loop trough all files + Dir dir = SPIFFS.openDir("/"); + while (dir.next()) { + String fileName = dir.fileName(); + size_t fileSize = dir.fileSize(); + if (first_item) + first_item=false; + else + response += ","; + + response += F("{\"na\":\""); + response += fileName.c_str(); + response += F("\",\"va\":\""); + response += fileSize; + response += F("\"}\r\n"); + } + response += F("],\r\n"); + + + // SPIFFS File system array + response += F("\"spiffs\":[\r\n{"); + + // Get SPIFFS File system informations + FSInfo info; + SPIFFS.info(info); + response += F("\"Total\":"); + response += info.totalBytes ; + response += F(", \"Used\":"); + response += info.usedBytes ; + response += F(", \"ram\":"); + response += system_get_free_heap_size() ; + response += F("}\r\n]"); + + // Json end + response += FPSTR(FP_JSON_END); +} + +/* ====================================================================== +Function: spiffsJSONTable +Purpose : dump all spiffs system in JSON table format for browser +Input : - +Output : - +Comments: - +====================================================================== */ +void spiffsJSONTable() +{ + String response = ""; + getSpiffsJSONData(response); + server.send ( 200, "text/json", response ); +} + +/* ====================================================================== +Function: sendJSON +Purpose : dump all values in JSON +Input : linked list pointer on the concerned data + true to dump all values, false for only modified ones +Output : - +Comments: - +====================================================================== */ +void sendJSON(void) +{ + ValueList * me = tinfo.getList(); + String response = ""; + + // Got at least one ? + if (me) { + // Json start + response += FPSTR(FP_JSON_START); + response += F("\"_UPTIME\":"); + response += seconds; + + // Loop thru the node + while (me->next) { + // go to next node + me = me->next; + response += F(",\"") ; + response += me->name ; + response += F("\":") ; + formatNumberJSON(response, me->value); + } + // Json end + response += FPSTR(FP_JSON_END) ; + + } else { + server.send ( 404, "text/plain", "No data" ); + } + server.send ( 200, "text/json", response ); +} + + +/* ====================================================================== +Function: wifiScanJSON +Purpose : scan Wifi Access Point and return JSON code +Input : - +Output : - +Comments: - +====================================================================== */ +void wifiScanJSON(void) +{ + String response = ""; + bool first = true; + + // Just to debug where we are + Debug(F("Serving /wifiscan page...")); + + int n = WiFi.scanNetworks(); + + // Json start + response += F("[\r\n"); + + for (uint8_t i = 0; i < n; ++i) + { + int8_t rssi = WiFi.RSSI(i); + + uint8_t percent; + + // dBm to Quality + if(rssi<=-100) percent = 0; + else if (rssi>=-50) percent = 100; + else percent = 2 * (rssi + 100); + + if (first) + first = false; + else + response += F(","); + + response += F("{\"ssid\":\""); + response += WiFi.SSID(i); + response += F("\",\"rssi\":") ; + response += rssi; + response += FPSTR(FP_JSON_END); + } + + // Json end + response += FPSTR("]\r\n"); + + Debug(F("sending...")); + server.send ( 200, "text/json", response ); + Debugln(F("Ok!")); +} + + +/* ====================================================================== +Function: handleFactoryReset +Purpose : reset the module to factory settingd +Input : - +Output : - +Comments: - +====================================================================== */ +void handleFactoryReset(void) +{ + // Just to debug where we are + Debug(F("Serving /factory_reset page...")); + ResetConfig(); + ESP.eraseConfig(); + Debug(F("sending...")); + server.send ( 200, "text/plain", FPSTR(FP_RESTART) ); + Debugln(F("Ok!")); + delay(1000); + ESP.restart(); + while (true) + delay(1); +} + +/* ====================================================================== +Function: handleReset +Purpose : reset the module +Input : - +Output : - +Comments: - +====================================================================== */ +void handleReset(void) +{ + // Just to debug where we are + Debug(F("Serving /reset page...")); + Debug(F("sending...")); + server.send ( 200, "text/plain", FPSTR(FP_RESTART) ); + Debugln(F("Ok!")); + delay(1000); + ESP.restart(); + while (true) + delay(1); + +} + + +/* ====================================================================== +Function: handleNotFound +Purpose : default WEB routing when URI is not found +Input : - +Output : - +Comments: - +====================================================================== */ +void handleNotFound(void) +{ + String response = ""; + boolean found = false; + + // Led on + LedBluON(); + + // try to return SPIFFS file + found = handleFileRead(server.uri()); + + // Try Teleinfo ETIQUETTE + if (!found) { + // We check for an known label + ValueList * me = tinfo.getList(); + const char * uri; + // convert uri to char * for compare + uri = server.uri().c_str(); + + Debugf("handleNotFound(%s)\r\n", uri); + + // Got at least one and consistent URI ? + if (me && uri && *uri=='/' && *++uri ) { + + // Loop thru the linked list of values + while (me->next && !found) { + + // go to next node + me = me->next; + + //Debugf("compare to '%s' ", me->name); + // Do we have this one ? + if (stricmp (me->name, uri) == 0 ) + { + // no need to continue + found = true; + + // Add to respone + response += F("{\"") ; + response += me->name ; + response += F("\":") ; + formatNumberJSON(response, me->value); + response += F("}\r\n"); + } + } + } + + // Got it, send json + if (found) + server.send ( 200, "text/json", response ); + } + + // All trys failed + if (!found) { + // send error message in plain text + String message = F("File Not Found\n\n"); + message += F("URI: "); + message += server.uri(); + message += F("\nMethod: "); + message += ( server.method() == HTTP_GET ) ? "GET" : "POST"; + message += F("\nArguments: "); + message += server.args(); + message += FPSTR(FP_NL); + + for ( uint8_t i = 0; i < server.args(); i++ ) { + message += " " + server.argName ( i ) + ": " + server.arg ( i ) + FPSTR(FP_NL); + } + + server.send ( 404, "text/plain", message ); + } + + // Led off + LedBluOFF(); +} + diff --git a/examples/ESP8266_WifInfo/route.h b/examples/Wifinfo/webserver.h similarity index 81% rename from examples/ESP8266_WifInfo/route.h rename to examples/Wifinfo/webserver.h index 015ca66..81326b7 100644 --- a/examples/ESP8266_WifInfo/route.h +++ b/examples/Wifinfo/webserver.h @@ -20,11 +20,11 @@ // // ********************************************************************************** -#ifndef ROUTE_H -#define ROUTE_H +#ifndef WEBSERVER_H +#define WEBSERVER_H // Include main project include file -#include "ESP8266_WifInfo.h" +#include "Wifinfo.h" // Web response max size #define RESPONSE_BUFFER_SIZE 4096 @@ -38,10 +38,18 @@ extern uint16_t response_idx; // =================================================== void handleTest(void); void handleRoot(void); +void handleFormConfig(void) ; void handleNotFound(void); void tinfoJSONTable(void); +void getSysJSONData(String & r); void sysJSONTable(void); +void getConfJSONData(String & r); +void confJSONTable(void); +void getSpiffsJSONData(String & r); +void spiffsJSONTable(void); void sendJSON(void); - +void wifiScanJSON(void); +void handleFactoryReset(void); +void handleReset(void); #endif