Browse Source

Merge branch 'dev' into rules-rpn

rules-rpn
Xose Pérez 5 years ago
parent
commit
62b80bb9ea
113 changed files with 7986 additions and 4596 deletions
  1. +1
    -0
      .gitattributes
  2. +3
    -1
      .travis.yml
  3. +13
    -15
      code/.gitignore
  4. +9
    -3
      code/build.sh
  5. +1
    -1
      code/debug.sh
  6. +0
    -19
      code/eagle.flash.1m0m1s.ld
  7. +0
    -19
      code/eagle.flash.1m0m2s.ld
  8. +0
    -19
      code/eagle.flash.512k0m1s.ld
  9. +6
    -5
      code/espurna/alexa.ino
  10. +49
    -22
      code/espurna/api.ino
  11. +2
    -2
      code/espurna/button.ino
  12. +1
    -0
      code/espurna/config/all.h
  13. +8
    -0
      code/espurna/config/arduino.h
  14. +97
    -0
      code/espurna/config/buildtime.h
  15. +58
    -0
      code/espurna/config/defaults.h
  16. +17
    -2
      code/espurna/config/dependencies.h
  17. +29
    -0
      code/espurna/config/deprecated.h
  18. +277
    -52
      code/espurna/config/general.h
  19. +328
    -41
      code/espurna/config/hardware.h
  20. +29
    -0
      code/espurna/config/progmem.h
  21. +276
    -44
      code/espurna/config/prototypes.h
  22. +1
    -1
      code/espurna/config/rtcmem.h
  23. +278
    -17
      code/espurna/config/sensors.h
  24. +41
    -0
      code/espurna/config/types.h
  25. +55
    -18
      code/espurna/crash.ino
  26. +18
    -50
      code/espurna/debug.ino
  27. +44
    -30
      code/espurna/domoticz.ino
  28. +7
    -0
      code/espurna/eeprom.ino
  29. +8
    -9
      code/espurna/encoder.ino
  30. +32
    -5
      code/espurna/espurna.ino
  31. +198
    -154
      code/espurna/homeassistant.ino
  32. +9
    -4
      code/espurna/influxdb.ino
  33. +2
    -1
      code/espurna/ir.ino
  34. +13
    -8
      code/espurna/led.ino
  35. +47
    -0
      code/espurna/libs/DebugSend.h
  36. +228
    -0
      code/espurna/libs/Encoder.h
  37. +112
    -0
      code/espurna/libs/HeapStats.h
  38. +247
    -0
      code/espurna/libs/SecureClientHelpers.h
  39. +68
    -0
      code/espurna/libs/URL.h
  40. +26
    -0
      code/espurna/light.h
  41. +278
    -177
      code/espurna/light.ino
  42. +4
    -3
      code/espurna/lightfox.ino
  43. +1
    -0
      code/espurna/mdns.ino
  44. +12
    -0
      code/espurna/migrate.ino
  45. +395
    -203
      code/espurna/mqtt.ino
  46. +18
    -9
      code/espurna/nofuss.ino
  47. +16
    -11
      code/espurna/ntp.ino
  48. +0
    -295
      code/espurna/ota.ino
  49. +101
    -0
      code/espurna/ota_arduinoota.ino
  50. +227
    -0
      code/espurna/ota_asynctcp.ino
  51. +264
    -0
      code/espurna/ota_httpupdate.ino
  52. +184
    -118
      code/espurna/relay.ino
  53. +24
    -47
      code/espurna/rfbridge.ino
  54. +6
    -5
      code/espurna/rfm69.ino
  55. +46
    -10
      code/espurna/rtcmem.ino
  56. +11
    -5
      code/espurna/scheduler.ino
  57. +326
    -80
      code/espurna/sensor.ino
  58. +249
    -0
      code/espurna/sensors/ADE7953Sensor.h
  59. +2
    -3
      code/espurna/sensors/AnalogSensor.h
  60. +43
    -18
      code/espurna/sensors/EventSensor.h
  61. +4
    -4
      code/espurna/sensors/GeigerSensor.h
  62. +2
    -2
      code/espurna/sensors/HLW8012Sensor.h
  63. +25
    -9
      code/espurna/sensors/PMSX003Sensor.h
  64. +11
    -1
      code/espurna/sensors/PulseMeterSensor.h
  65. +2
    -2
      code/espurna/sensors/SenseAirSensor.h
  66. +28
    -0
      code/espurna/settings.ino
  67. +42
    -0
      code/espurna/static/digicert_evroot_pem.h
  68. +94
    -0
      code/espurna/static/letsencrypt_isrgroot_pem.h
  69. +5
    -3
      code/espurna/system.ino
  70. +136
    -56
      code/espurna/telnet.ino
  71. +167
    -22
      code/espurna/terminal.ino
  72. +16
    -15
      code/espurna/thermostat.ino
  73. +157
    -77
      code/espurna/thinkspeak.ino
  74. +61
    -65
      code/espurna/utils.ino
  75. +79
    -23
      code/espurna/web.ino
  76. +224
    -103
      code/espurna/wifi.ino
  77. +368
    -131
      code/espurna/ws.ino
  78. +90
    -0
      code/extra_script_pre.py
  79. +32
    -6
      code/extra_scripts.py
  80. +176
    -99
      code/html/custom.js
  81. +7
    -5
      code/html/index.html
  82. +0
    -4
      code/html/vendor/jquery-3.2.1.min.js
  83. +2
    -0
      code/html/vendor/jquery-3.4.1.slim.min.js
  84. +0
    -160
      code/html/vendor/jquery.wheelcolorpicker-3.0.3.css
  85. +0
    -13
      code/html/vendor/jquery.wheelcolorpicker-3.0.3.min.js
  86. +161
    -0
      code/html/vendor/jquery.wheelcolorpicker-3.0.8.css
  87. +6
    -0
      code/html/vendor/jquery.wheelcolorpicker-3.0.8.min.js
  88. +1
    -0
      code/libraries/README
  89. +11
    -7
      code/memanalyzer.py
  90. +8
    -4
      code/ota.py
  91. +425
    -2236
      code/platformio.ini
  92. +1
    -1
      code/symbols.sh
  93. +39
    -0
      dist/arduino_ide/2.3.0/boards.local.txt
  94. +24
    -0
      dist/arduino_ide/README.md
  95. +39
    -0
      dist/arduino_ide/latest/boards.local.txt
  96. +187
    -0
      dist/boards_local_txt.py
  97. +8
    -0
      dist/ld/README.md
  98. +30
    -0
      dist/ld/latest/eagle.flash.1m0m1s.ld
  99. +30
    -0
      dist/ld/latest/eagle.flash.1m0m2s.ld
  100. +30
    -0
      dist/ld/latest/eagle.flash.2m1m4s.ld

+ 1
- 0
.gitattributes View File

@ -1,2 +1,3 @@
*.gz.h -diff -merge *.gz.h -diff -merge
*.gz.h linguist-generated=true *.gz.h linguist-generated=true
*.ini text eol=lf

+ 3
- 1
.travis.yml View File

@ -7,15 +7,16 @@ cache:
directories: directories:
- "~/.npm" - "~/.npm"
- "~/.platformio" - "~/.platformio"
- "$TRAVIS_BUILD_DIR/code/.piolibdeps"
install: install:
- pip install -U platformio - pip install -U platformio
- pio platform update -p
- npm install -g npm@latest - npm install -g npm@latest
- cd code && npm ci && cd .. - cd code && npm ci && cd ..
env: env:
global: global:
- BUILDER_TOTAL_THREADS=4 - BUILDER_TOTAL_THREADS=4
- ESPURNA_PIO_PATCH_ISSUE_1610=y - ESPURNA_PIO_PATCH_ISSUE_1610=y
- ESPURNA_PIO_SHARED_LIBRARIES=y
script: script:
- cd code && ./build.sh -p && cd .. - cd code && ./build.sh -p && cd ..
stages: stages:
@ -28,6 +29,7 @@ jobs:
script: cd code && ./build.sh travis01 script: cd code && ./build.sh travis01
- script: cd code && ./build.sh travis02 - script: cd code && ./build.sh travis02
- script: cd code && ./build.sh travis03 - script: cd code && ./build.sh travis03
- script: cd code && ./build.sh travis04
- stage: Release - stage: Release
env: BUILDER_THREAD=0 env: BUILDER_THREAD=0
- env: BUILDER_THREAD=1 - env: BUILDER_THREAD=1


+ 13
- 15
code/.gitignore View File

@ -1,15 +1,13 @@
.clang_complete
core_version.h
custom.h
.DS_Store
.gcc-flags.json
.pioenvs
.piolibdeps
.python-version
.travis.yml
.vscode
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.pioenvs
.piolibdeps
.clang_complete
core_version.h
custom.h
.DS_Store
.gcc-flags.json
.python-version
.travis.yml
.vscode
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.pio
libraries/

+ 9
- 3
code/build.sh View File

@ -112,19 +112,25 @@ build_webui() {
echo "--------------------------------------------------------------" echo "--------------------------------------------------------------"
echo "Building web interface..." echo "Building web interface..."
node node_modules/gulp/bin/gulp.js || exit node node_modules/gulp/bin/gulp.js || exit
# TODO: do something if webui files are different
# for now, just print in travis log
if ${TRAVIS:-false}; then
git --no-pager diff --stat
fi
} }
build_environments() { build_environments() {
echo "--------------------------------------------------------------" echo "--------------------------------------------------------------"
echo "Building firmware images..." echo "Building firmware images..."
mkdir -p ../firmware/espurna-$version
mkdir -p $destination/espurna-$version
for environment in $environments; do for environment in $environments; do
echo -n "* espurna-$version-$environment.bin --- " echo -n "* espurna-$version-$environment.bin --- "
platformio run --silent --environment $environment || exit 1 platformio run --silent --environment $environment || exit 1
stat_bytes .pioenvs/$environment/firmware.bin
stat_bytes .pio/build/$environment/firmware.bin
[[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \ [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \
mv .pioenvs/$environment/firmware.bin $destination/espurna-$version/espurna-$version-$environment.bin
mv .pio/build/$environment/firmware.bin $destination/espurna-$version/espurna-$version-$environment.bin
done done
echo "--------------------------------------------------------------" echo "--------------------------------------------------------------"
} }


+ 1
- 1
code/debug.sh View File

@ -49,7 +49,7 @@ done
# check environment folder # check environment folder
if [ ! -f $ELF ]; then if [ ! -f $ELF ]; then
ELF=.pioenvs/$ENVIRONMENT/firmware.elf
ELF=.pio/build/$ENVIRONMENT/firmware.elf
fi fi
if [ ! -f $ELF ]; then if [ ! -f $ELF ]; then
echo "Could not find ELF file for the selected environment: $ELF" echo "Could not find ELF file for the selected environment: $ELF"


+ 0
- 19
code/eagle.flash.1m0m1s.ld View File

@ -1,19 +0,0 @@
/* Flash Split for 1M chips, no SPIFFS, 1 sector for EEPROM */
/* sketch 999KB */
/* eeprom 4KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xf9ff0
}
PROVIDE ( _SPIFFS_start = 0x402FB000 );
PROVIDE ( _SPIFFS_end = 0x402FB000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 0
- 19
code/eagle.flash.1m0m2s.ld View File

@ -1,19 +0,0 @@
/* Flash Split for 1M chips, no SPIFFS, 2 sectors for EEPROM */
/* sketch 995KB */
/* eeprom 8KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xf8ff0
}
PROVIDE ( _SPIFFS_start = 0x402FA000 );
PROVIDE ( _SPIFFS_end = 0x402FA000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 0
- 19
code/eagle.flash.512k0m1s.ld View File

@ -1,19 +0,0 @@
/* Flash Split for 512K chips, no SPIFFS, 1 sector for EEPROM */
/* sketch 487KB */
/* eeprom 4KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0x79ff0
}
PROVIDE ( _SPIFFS_start = 0x4027B000 );
PROVIDE ( _SPIFFS_end = 0x4027B000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 6
- 5
code/espurna/alexa.ino View File

@ -23,12 +23,11 @@ static std::queue<alexa_queue_element_t> _alexa_queue;
// ALEXA // ALEXA
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
bool _alexaWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _alexaWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "alexa", 5) == 0); return (strncmp(key, "alexa", 5) == 0);
} }
void _alexaWebSocketOnSend(JsonObject& root) {
root["alexaVisible"] = 1;
void _alexaWebSocketOnConnected(JsonObject& root) {
root["alexaEnabled"] = alexaEnabled(); root["alexaEnabled"] = alexaEnabled();
root["alexaName"] = getSetting("alexaName"); root["alexaName"] = getSetting("alexaName");
} }
@ -123,8 +122,10 @@ void alexaSetup() {
#if WEB_SUPPORT #if WEB_SUPPORT
webBodyRegister(_alexaBodyCallback); webBodyRegister(_alexaBodyCallback);
webRequestRegister(_alexaRequestCallback); webRequestRegister(_alexaRequestCallback);
wsOnSendRegister(_alexaWebSocketOnSend);
wsOnReceiveRegister(_alexaWebSocketOnReceive);
wsRegister()
.onVisible([](JsonObject& root) { root["alexaVisible"] = 1; })
.onConnected(_alexaWebSocketOnConnected)
.onKeyCheck(_alexaWebSocketOnKeyCheck);
#endif #endif
// Register wifi callback // Register wifi callback


+ 49
- 22
code/espurna/api.ino View File

@ -22,12 +22,11 @@ std::vector<web_api_t> _apis;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
bool _apiWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _apiWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "api", 3) == 0); return (strncmp(key, "api", 3) == 0);
} }
void _apiWebSocketOnSend(JsonObject& root) {
root["apiVisible"] = 1;
void _apiWebSocketOnConnected(JsonObject& root) {
root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1; root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1;
root["apiKey"] = getSetting("apiKey"); root["apiKey"] = getSetting("apiKey");
root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1; root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1;
@ -76,6 +75,47 @@ bool _asJson(AsyncWebServerRequest *request) {
return asJson; return asJson;
} }
void _onAPIsText(AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("text/plain");
String output;
output.reserve(48);
for (unsigned int i=0; i < _apis.size(); i++) {
output = "";
output += _apis[i].key;
output += " -> ";
output += "/api/";
output += _apis[i].key;
output += '\n';
response->write(output.c_str());
}
request->send(response);
}
constexpr const size_t API_JSON_BUFFER_SIZE = 1024;
void _onAPIsJson(AsyncWebServerRequest *request) {
DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE);
JsonObject& root = jsonBuffer.createObject();
constexpr const int BUFFER_SIZE = 48;
for (unsigned int i=0; i < _apis.size(); i++) {
char buffer[BUFFER_SIZE] = {0};
int res = snprintf(buffer, sizeof(buffer), "/api/%s", _apis[i].key);
if ((res < 0) || (res > (BUFFER_SIZE - 1))) {
request->send(500);
return;
}
root[_apis[i].key] = buffer;
}
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
}
void _onAPIs(AsyncWebServerRequest *request) { void _onAPIs(AsyncWebServerRequest *request) {
webLog(request); webLog(request);
@ -83,26 +123,11 @@ void _onAPIs(AsyncWebServerRequest *request) {
bool asJson = _asJson(request); bool asJson = _asJson(request);
char buffer[40];
String output; String output;
if (asJson) { if (asJson) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
for (unsigned int i=0; i < _apis.size(); i++) {
snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), _apis[i].key);
root[_apis[i].key] = String(buffer);
}
root.printTo(output);
jsonBuffer.clear();
request->send(200, "application/json", output);
_onAPIsJson(request);
} else { } else {
for (unsigned int i=0; i < _apis.size(); i++) {
snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), _apis[i].key);
output += _apis[i].key + String(" -> ") + String(buffer) + String("\n");
}
request->send(200, "text/plain", output);
_onAPIsText(request);
} }
} }
@ -220,8 +245,10 @@ void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f
void apiSetup() { void apiSetup() {
_apiConfigure(); _apiConfigure();
wsOnSendRegister(_apiWebSocketOnSend);
wsOnReceiveRegister(_apiWebSocketOnReceive);
wsRegister()
.onVisible([](JsonObject& root) { root["apiVisible"] = 1; })
.onConnected(_apiWebSocketOnConnected)
.onKeyCheck(_apiWebSocketOnKeyCheck);
webRequestRegister(_apiRequestCallback); webRequestRegister(_apiRequestCallback);
espurnaRegisterReload(_apiConfigure); espurnaRegisterReload(_apiConfigure);
} }


+ 2
- 2
code/espurna/button.ino View File

@ -36,7 +36,7 @@ void buttonMQTT(unsigned char id, uint8_t event) {
#if WEB_SUPPORT #if WEB_SUPPORT
bool _buttonWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "btn", 3) == 0); return (strncmp(key, "btn", 3) == 0);
} }
@ -243,7 +243,7 @@ void buttonSetup() {
// Websocket Callbacks // Websocket Callbacks
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnReceiveRegister(_buttonWebSocketOnReceive);
wsRegister().onKeyCheck(_buttonWebSocketOnKeyCheck);
#endif #endif
// Register loop // Register loop


+ 1
- 0
code/espurna/config/all.h View File

@ -28,6 +28,7 @@
#include "arduino.h" #include "arduino.h"
#include "hardware.h" #include "hardware.h"
#include "defaults.h" #include "defaults.h"
#include "buildtime.h"
#include "deprecated.h" #include "deprecated.h"
#include "general.h" #include "general.h"
#include "dependencies.h" #include "dependencies.h"


+ 8
- 0
code/espurna/config/arduino.h View File

@ -11,6 +11,8 @@
//#define ALLNET_4DUINO_IOT_WLAN_RELAIS //#define ALLNET_4DUINO_IOT_WLAN_RELAIS
//#define ALLTERCO_SHELLY1 //#define ALLTERCO_SHELLY1
//#define ALLTERCO_SHELLY2 //#define ALLTERCO_SHELLY2
//#define ALLTERCO_SHELLY1PM
//#define ALLTERCO_SHELLY25
//#define ARILUX_AL_LC01 //#define ARILUX_AL_LC01
//#define ARILUX_AL_LC02 //#define ARILUX_AL_LC02
//#define ARILUX_AL_LC02_V14 //#define ARILUX_AL_LC02_V14
@ -72,6 +74,7 @@
//#define ITEAD_SONOFF_RF //#define ITEAD_SONOFF_RF
//#define ITEAD_SONOFF_RFBRIDGE //#define ITEAD_SONOFF_RFBRIDGE
//#define ITEAD_SONOFF_S31 //#define ITEAD_SONOFF_S31
//#define ITEAD_SONOFF_S31_LITE
//#define ITEAD_SONOFF_SV //#define ITEAD_SONOFF_SV
//#define ITEAD_SONOFF_T1_1CH //#define ITEAD_SONOFF_T1_1CH
//#define ITEAD_SONOFF_T1_2CH //#define ITEAD_SONOFF_T1_2CH
@ -81,6 +84,7 @@
//#define IWOOLE_LED_TABLE_LAMP //#define IWOOLE_LED_TABLE_LAMP
//#define JANGOE_WIFI_RELAY_NC //#define JANGOE_WIFI_RELAY_NC
//#define JANGOE_WIFI_RELAY_NO //#define JANGOE_WIFI_RELAY_NO
//#define JINVOO_VALVE_SM_AW713
//#define JORGEGARCIA_WIFI_RELAYS //#define JORGEGARCIA_WIFI_RELAYS
//#define KMC_70011 //#define KMC_70011
//#define LINGAN_SWA1 //#define LINGAN_SWA1
@ -94,10 +98,12 @@
//#define MAGICHOME_LED_CONTROLLER_20 //#define MAGICHOME_LED_CONTROLLER_20
//#define MAGICHOME_ZJ_WFMN_A_11 //#define MAGICHOME_ZJ_WFMN_A_11
//#define MAGICHOME_ZJ_WFMN_B_11 //#define MAGICHOME_ZJ_WFMN_B_11
//#define MAGICHOME_ZJ_ESPM_5CH_B_13
//#define MANCAVEMADE_ESPLIVE //#define MANCAVEMADE_ESPLIVE
//#define MAXCIO_WDE004 //#define MAXCIO_WDE004
//#define MAXCIO_WUS002S //#define MAXCIO_WUS002S
//#define NEO_COOLCAM_NAS_WR01W //#define NEO_COOLCAM_NAS_WR01W
//#define NEXETE_A19
//#define NODEMCU_BASIC //#define NODEMCU_BASIC
//#define NODEMCU_LOLIN //#define NODEMCU_LOLIN
//#define OPENENERGYMONITOR_MQTT_RELAY //#define OPENENERGYMONITOR_MQTT_RELAY
@ -124,6 +130,7 @@
//#define WION_50055 //#define WION_50055
//#define WORKCHOICE_ECOPLUG //#define WORKCHOICE_ECOPLUG
//#define XENON_SM_PW702U //#define XENON_SM_PW702U
//#define ISELECTOR_SM_PW702
//#define XIAOMI_SMART_DESK_LAMP //#define XIAOMI_SMART_DESK_LAMP
//#define YIDIAN_XSSSA05 //#define YIDIAN_XSSSA05
//#define YJZK_SWITCH_1CH //#define YJZK_SWITCH_1CH
@ -210,3 +217,4 @@
//#define V9261F_SUPPORT 1 //#define V9261F_SUPPORT 1
//#define VEML6075_SUPPORT 1 //#define VEML6075_SUPPORT 1
//#define VL53L1X_SUPPORT 1 //#define VL53L1X_SUPPORT 1
//#define ADE7953_SUPPORT 1

+ 97
- 0
code/espurna/config/buildtime.h View File

@ -0,0 +1,97 @@
/*
*
* Created: 29.03.2018
*
* Authors:
*
* Assembled from the code released on Stackoverflow by:
* Dennis (instructable.com/member/nqtronix) | https://stackoverflow.com/questions/23032002/c-c-how-to-get-integer-unix-timestamp-of-build-time-not-string
* and
* Alexis Wilke | https://stackoverflow.com/questions/10538444/do-you-know-of-a-c-macro-to-compute-unix-time-and-date
*
* Assembled by Jean Rabault
*
* UNIX_TIMESTAMP gives the UNIX timestamp (unsigned long integer of seconds since 1st Jan 1970) of compilation from macros using the compiler defined __TIME__ macro.
* This should include Gregorian calendar leap days, in particular the 29ths of February, 100 and 400 years modulo leaps.
*
* Careful: __TIME__ is the local time of the computer, NOT the UTC time in general!
*
*/
#ifndef COMPILE_TIME_H_
#define COMPILE_TIME_H_
// Some definitions for calculation
#define SEC_PER_MIN 60UL
#define SEC_PER_HOUR 3600UL
#define SEC_PER_DAY 86400UL
#define SEC_PER_YEAR (SEC_PER_DAY*365)
// extracts 1..4 characters from a string and interprets it as a decimal value
#define CONV_STR2DEC_1(str, i) (str[i]>'0'?str[i]-'0':0)
#define CONV_STR2DEC_2(str, i) (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0')
#define CONV_STR2DEC_3(str, i) (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0')
#define CONV_STR2DEC_4(str, i) (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0')
// Custom "glue logic" to convert the month name to a usable number
#define GET_MONTH(str, i) (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 : \
str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 : \
str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 : \
str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 : \
str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 : \
str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 : \
str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 : \
str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 : \
str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 : \
str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 : \
str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 : \
str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0)
// extract the information from the time string given by __TIME__ and __DATE__
#define __TIME_SECOND__ (CONV_STR2DEC_2(__TIME__, 6))
#define __TIME_MINUTE__ (CONV_STR2DEC_2(__TIME__, 3))
#define __TIME_HOUR__ (CONV_STR2DEC_2(__TIME__, 0))
#define __TIME_DAY__ (CONV_STR2DEC_2(__DATE__, 4))
#define __TIME_MONTH__ (GET_MONTH(__DATE__, 0))
#define __TIME_YEAR__ (CONV_STR2DEC_4(__DATE__, 7))
// Days in February
#define _UNIX_TIMESTAMP_FDAY(year) \
(((year) % 400) == 0UL ? 29UL : \
(((year) % 100) == 0UL ? 28UL : \
(((year) % 4) == 0UL ? 29UL : \
28UL)))
// Days in the year
#define _UNIX_TIMESTAMP_YDAY(year, month, day) \
( \
/* January */ day \
/* February */ + (month >= 2 ? 31UL : 0UL) \
/* March */ + (month >= 3 ? _UNIX_TIMESTAMP_FDAY(year) : 0UL) \
/* April */ + (month >= 4 ? 31UL : 0UL) \
/* May */ + (month >= 5 ? 30UL : 0UL) \
/* June */ + (month >= 6 ? 31UL : 0UL) \
/* July */ + (month >= 7 ? 30UL : 0UL) \
/* August */ + (month >= 8 ? 31UL : 0UL) \
/* September */+ (month >= 9 ? 31UL : 0UL) \
/* October */ + (month >= 10 ? 30UL : 0UL) \
/* November */ + (month >= 11 ? 31UL : 0UL) \
/* December */ + (month >= 12 ? 30UL : 0UL) \
)
// get the UNIX timestamp from a digits representation
#define _UNIX_TIMESTAMP(year, month, day, hour, minute, second) \
( /* time */ second \
+ minute * SEC_PER_MIN \
+ hour * SEC_PER_HOUR \
+ /* year day (month + day) */ (_UNIX_TIMESTAMP_YDAY(year, month, day) - 1) * SEC_PER_DAY \
+ /* year */ (year - 1970UL) * SEC_PER_YEAR \
+ ((year - 1969UL) / 4UL) * SEC_PER_DAY \
- ((year - 1901UL) / 100UL) * SEC_PER_DAY \
+ ((year - 1601UL) / 400UL) * SEC_PER_DAY \
)
// the UNIX timestamp
#define __UNIX_TIMESTAMP__ (_UNIX_TIMESTAMP(__TIME_YEAR__, __TIME_MONTH__, __TIME_DAY__, __TIME_HOUR__, __TIME_MINUTE__, __TIME_SECOND__))
#endif

+ 58
- 0
code/espurna/config/defaults.h View File

@ -552,6 +552,64 @@
#define LED8_RELAY 8 #define LED8_RELAY 8
#endif #endif
// -----------------------------------------------------------------------------
// Digital Inputs
// -----------------------------------------------------------------------------
#ifndef DIGITAL1_PIN
#define DIGITAL1_PIN GPIO_NONE
#endif
#ifndef DIGITAL2_PIN
#define DIGITAL2_PIN GPIO_NONE
#endif
#ifndef DIGITAL3_PIN
#define DIGITAL3_PIN GPIO_NONE
#endif
#ifndef DIGITAL4_PIN
#define DIGITAL4_PIN GPIO_NONE
#endif
#ifndef DIGITAL5_PIN
#define DIGITAL5_PIN GPIO_NONE
#endif
#ifndef DIGITAL6_PIN
#define DIGITAL6_PIN GPIO_NONE
#endif
#ifndef DIGITAL7_PIN
#define DIGITAL7_PIN GPIO_NONE
#endif
#ifndef DIGITAL8_PIN
#define DIGITAL8_PIN GPIO_NONE
#endif
// -----------------------------------------------------------------------------
// Events
// -----------------------------------------------------------------------------
#ifndef EVENTS1_PIN
#define EVENTS1_PIN GPIO_NONE
#endif
#ifndef EVENTS2_PIN
#define EVENTS2_PIN GPIO_NONE
#endif
#ifndef EVENTS3_PIN
#define EVENTS3_PIN GPIO_NONE
#endif
#ifndef EVENTS4_PIN
#define EVENTS4_PIN GPIO_NONE
#endif
#ifndef EVENTS5_PIN
#define EVENTS5_PIN GPIO_NONE
#endif
#ifndef EVENTS6_PIN
#define EVENTS6_PIN GPIO_NONE
#endif
#ifndef EVENTS7_PIN
#define EVENTS7_PIN GPIO_NONE
#endif
#ifndef EVENTS8_PIN
#define EVENTS8_PIN GPIO_NONE
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// General // General
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------


+ 17
- 2
code/espurna/config/dependencies.h View File

@ -5,6 +5,11 @@
// Configuration settings are in the general.h file // Configuration settings are in the general.h file
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
#if defined(ASYNC_TCP_SSL_ENABLED) && SECURE_CLIENT == SECURE_CLIENT_NONE
#undef SECURE_CLIENT
#define SECURE_CLIENT SECURE_CLIENT_AXTLS
#endif
#if DEBUG_TELNET_SUPPORT #if DEBUG_TELNET_SUPPORT
#undef TELNET_SUPPORT #undef TELNET_SUPPORT
#define TELNET_SUPPORT 1 #define TELNET_SUPPORT 1
@ -60,10 +65,10 @@
#define MQTT_SUPPORT 1 // If Home Assistant enabled enable MQTT #define MQTT_SUPPORT 1 // If Home Assistant enabled enable MQTT
#endif #endif
#ifndef ASYNC_TCP_SSL_ENABLED
#if SECURE_CLIENT != SECURE_CLIENT_AXTLS
#if THINGSPEAK_USE_SSL && THINGSPEAK_USE_ASYNC #if THINGSPEAK_USE_SSL && THINGSPEAK_USE_ASYNC
#undef THINGSPEAK_SUPPORT #undef THINGSPEAK_SUPPORT
#define THINGSPEAK_SUPPORT 0 // Thingspeak in ASYNC mode requires ASYNC_TCP_SSL_ENABLED
#define THINGSPEAK_SUPPORT 0 // Thingspeak in ASYNC mode requires SECURE_CLIENT_AXTLS
#endif #endif
#endif #endif
@ -76,3 +81,13 @@
#undef NTP_SUPPORT #undef NTP_SUPPORT
#define NTP_SUPPORT 1 // Scheduler needs NTP #define NTP_SUPPORT 1 // Scheduler needs NTP
#endif #endif
#if (SECURE_CLIENT == SECURE_CLIENT_BEARSSL)
#undef OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE
#define OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE 0 // Use new HTTPUpdate API with BearSSL
#endif
#if LWIP_VERSION_MAJOR != 1
#undef MDNS_CLIENT_SUPPORT
#define MDNS_CLIENT_SUPPORT 0 // default resolver already handles this
#endif

+ 29
- 0
code/espurna/config/deprecated.h View File

@ -13,3 +13,32 @@
#warning RF_PIN is deprecated! Please use RFB_RX_PIN instead #warning RF_PIN is deprecated! Please use RFB_RX_PIN instead
#define RFB_RX_PIN RF_PIN #define RFB_RX_PIN RF_PIN
#endif #endif
// 1.13.6 allow multiple digitals
#ifdef DIGITAL_PIN
#warning DIGITAL_PIN is deprecated! Please use DIGITAL1_PIN instead
#define DIGITAL1_PIN DIGITAL_PIN
#endif
// 1.13.6 allow multiple events
#ifdef EVENTS_PIN
#warning EVENTS_PIN is deprecated! Please use EVENTS1_PIN instead
#define EVENTS1_PIN EVENTS_PIN
#endif
// 1.13.6 unifies mqtt payload options
#ifdef HOMEASSISTANT_PAYLOAD_ON
#warning HOMEASSISTANT_PAYLOAD_ON is deprecated! Global RELAY_MQTT_ON is used instead
#endif
#ifdef HOMEASSISTANT_PAYLOAD_OFF
#warning HOMEASSISTANT_PAYLOAD_OFF is deprecated! Global RELAY_MQTT_OFF is used instead
#endif
#ifdef HOMEASSISTANT_PAYLOAD_AVAILABLE
#warning HOMEASSISTANT_PAYLOAD_AVAILABLE is deprecated! Global MQTT_STATUS_ONLINE is used instead
#endif
#ifdef HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE
#warning HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE is deprecated! Global MQTT_STATUS_OFFLINE is used instead
#endif

+ 277
- 52
code/espurna/config/general.h View File

@ -118,8 +118,17 @@
#define TELNET_AUTHENTICATION 1 // Request password to start telnet session by default #define TELNET_AUTHENTICATION 1 // Request password to start telnet session by default
#endif #endif
#ifndef TELNET_PORT
#define TELNET_PORT 23 // Port to listen to telnet clients #define TELNET_PORT 23 // Port to listen to telnet clients
#endif
#ifndef TELNET_MAX_CLIENTS
#define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients #define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients
#endif
#ifndef TELNET_SERVER
#define TELNET_SERVER TELNET_SERVER_ASYNC // Can be either TELNET_SERVER_ASYNC (using ESPAsyncTCP) or TELNET_SERVER_WIFISERVER (using WiFiServer)
#endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TERMINAL // TERMINAL
@ -335,6 +344,10 @@
#define ENCODER_SUPPORT 0 #define ENCODER_SUPPORT 0
#endif #endif
#ifndef ENCODER_MINIMUM_DELTA
#define ENCODER_MINIMUM_DELTA 1
#endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// LED // LED
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -391,12 +404,17 @@
#define RELAY_REPORT_STATUS 1 #define RELAY_REPORT_STATUS 1
#endif #endif
// Configure the MQTT payload for ON/OFF
// Configure the MQTT payload for ON, OFF and TOGGLE
#ifndef RELAY_MQTT_OFF
#define RELAY_MQTT_OFF "0"
#endif
#ifndef RELAY_MQTT_ON #ifndef RELAY_MQTT_ON
#define RELAY_MQTT_ON "1" #define RELAY_MQTT_ON "1"
#endif #endif
#ifndef RELAY_MQTT_OFF
#define RELAY_MQTT_OFF "0"
#ifndef RELAY_MQTT_TOGGLE
#define RELAY_MQTT_TOGGLE "2"
#endif #endif
// TODO Only single EEPROM address is used to store state, which is 1 byte // TODO Only single EEPROM address is used to store state, which is 1 byte
@ -485,6 +503,54 @@
#define WIFI2_DNS "" #define WIFI2_DNS ""
#endif #endif
#ifndef WIFI3_SSID
#define WIFI3_SSID ""
#endif
#ifndef WIFI3_PASS
#define WIFI3_PASS ""
#endif
#ifndef WIFI3_IP
#define WIFI3_IP ""
#endif
#ifndef WIFI3_GW
#define WIFI3_GW ""
#endif
#ifndef WIFI3_MASK
#define WIFI3_MASK ""
#endif
#ifndef WIFI3_DNS
#define WIFI3_DNS ""
#endif
#ifndef WIFI4_SSID
#define WIFI4_SSID ""
#endif
#ifndef WIFI4_PASS
#define WIFI4_PASS ""
#endif
#ifndef WIFI4_IP
#define WIFI4_IP ""
#endif
#ifndef WIFI4_GW
#define WIFI4_GW ""
#endif
#ifndef WIFI4_MASK
#define WIFI4_MASK ""
#endif
#ifndef WIFI4_DNS
#define WIFI4_DNS ""
#endif
#ifndef WIFI_RSSI_1M #ifndef WIFI_RSSI_1M
#define WIFI_RSSI_1M -30 // Calibrate it with your router reading the RSSI at 1m #define WIFI_RSSI_1M -30 // Calibrate it with your router reading the RSSI at 1m
#endif #endif
@ -493,6 +559,34 @@
#define WIFI_PROPAGATION_CONST 4 // This is typically something between 2.7 to 4.3 (free space is 2) #define WIFI_PROPAGATION_CONST 4 // This is typically something between 2.7 to 4.3 (free space is 2)
#endif #endif
// ref: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html#config-lwip-esp-gratuitous-arp
// ref: https://github.com/xoseperez/espurna/pull/1877#issuecomment-525612546
//
// Broadcast gratuitous ARP periodically to update ARP tables on the AP and all devices on the same network.
// Helps to solve compatibility issues when ESP fails to timely reply to ARP requests, causing the device's ARP table entry to expire.
#ifndef WIFI_GRATUITOUS_ARP_SUPPORT
#define WIFI_GRATUITOUS_ARP_SUPPORT 1
#endif
// Interval is randomized on each boot in range from ..._MIN to ..._MAX (ms)
#ifndef WIFI_GRATUITOUS_ARP_INTERVAL_MIN
#define WIFI_GRATUITOUS_ARP_INTERVAL_MIN 15000
#endif
#ifndef WIFI_GRATUITOUS_ARP_INTERVAL_MAX
#define WIFI_GRATUITOUS_ARP_INTERVAL_MAX 30000
#endif
// ref: https://github.com/esp8266/Arduino/issues/6471
// ref: https://github.com/esp8266/Arduino/issues/6366
//
// Issue #6366 turned out to be high tx power causing weird behavior. Lowering tx power achieved stability.
#ifndef WIFI_OUTPUT_POWER_DBM
#define WIFI_OUTPUT_POWER_DBM 20.0
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WEB // WEB
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -506,7 +600,7 @@
#endif #endif
// This is not working at the moment!! // This is not working at the moment!!
// Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0
// Requires SECURE_CLIENT = SECURE_CLIENT_AXTLS and ESP8266 Arduino Core 2.4.0
#ifndef WEB_SSL_ENABLED #ifndef WEB_SSL_ENABLED
#define WEB_SSL_ENABLED 0 // Use HTTPS web interface #define WEB_SSL_ENABLED 0 // Use HTTPS web interface
#endif #endif
@ -574,7 +668,7 @@
#endif #endif
#ifndef API_BUFFER_SIZE #ifndef API_BUFFER_SIZE
#define API_BUFFER_SIZE 15 // Size of the buffer for HTTP GET API responses
#define API_BUFFER_SIZE 64 // Size of the buffer for HTTP GET API responses
#endif #endif
#ifndef API_REAL_TIME_VALUES #ifndef API_REAL_TIME_VALUES
@ -621,19 +715,99 @@
#define SPIFFS_SUPPORT 0 // Do not add support for SPIFFS by default #define SPIFFS_SUPPORT 0 // Do not add support for SPIFFS by default
#endif #endif
// -----------------------------------------------------------------------------
// SSL Client ** EXPERIMENTAL **
// -----------------------------------------------------------------------------
#ifndef SECURE_CLIENT
#define SECURE_CLIENT SECURE_CLIENT_NONE // What variant of WiFiClient to use
// SECURE_CLIENT_NONE - No secure client support (default)
// SECURE_CLIENT_AXTLS - axTLS client secure support (All Core versions, ONLY TLS 1.1)
// SECURE_CLIENT_BEARSSL - BearSSL client secure support (starting with 2.5.0, TLS 1.2)
//
// axTLS marked for derecation since Arduino Core 2.4.2 and **will** be removed in the future
#endif
// Security check that is performed when the connection is established:
// SECURE_CLIENT_CHECK_CA - Use Trust Anchor / Root Certificate
// Supported only by the SECURE_CLIENT_BEARSSL
// (See respective ..._SECURE_CLIENT_INCLUDE_CA options per-module)
// SECURE_CLIENT_CHECK_FINGERPRINT - Check certificate fingerprint
// SECURE_CLIENT_CHECK_NONE - Allow insecure connections
#ifndef SECURE_CLIENT_CHECK
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
#define SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK_CA
#else
#define SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK_FINGERPRINT
#endif
#endif // SECURE_CLIENT_CHECK
// Support Maximum Fragment Length Negotiation TLS extension
// "...negotiate a smaller maximum fragment length due to memory limitations or bandwidth limitations."
// - https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/bearssl-client-secure-class.html#mfln-or-maximum-fragment-length-negotiation-saving-ram
// - https://tools.ietf.org/html/rfc6066#section-4
#ifndef SECURE_CLIENT_MFLN
#define SECURE_CLIENT_MFLN 0 // The only possible values are: 512, 1024, 2048 and 4096
// Set to 0 to disable (default)
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// OTA // OTA
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#ifndef OTA_PORT #ifndef OTA_PORT
#define OTA_PORT 8266 // OTA port
#define OTA_PORT 8266 // Port for ArduinoOTA
#endif #endif
#ifndef OTA_MQTT_SUPPORT #ifndef OTA_MQTT_SUPPORT
#define OTA_MQTT_SUPPORT 0 // No support by default
#define OTA_MQTT_SUPPORT 0 // Listen for HTTP(s) URLs at '<root topic>/ota'. Depends on OTA_CLIENT
#endif
#ifndef OTA_ARDUINOOTA_SUPPORT
#define OTA_ARDUINOOTA_SUPPORT 1 // Support ArduinoOTA by default (4.2Kb)
// Implicitly depends on ESP8266mDNS library, thus increasing firmware size
#endif
#ifndef OTA_CLIENT
#define OTA_CLIENT OTA_CLIENT_ASYNCTCP // Terminal / MQTT OTA support
// OTA_CLIENT_ASYNCTCP (ESPAsyncTCP library)
// OTA_CLIENT_HTTPUPDATE (Arduino Core library)
#endif
#ifndef OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE
#define OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE 1 // Use old HTTPUpdate API by default
#endif
#define OTA_GITHUB_FP "CA:06:F5:6B:25:8B:7A:0D:4F:2B:05:47:09:39:47:86:51:15:19:84"
#ifndef OTA_FINGERPRINT
#define OTA_FINGERPRINT OTA_GITHUB_FP
#endif
#ifndef OTA_SECURE_CLIENT_CHECK
#define OTA_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK
#endif
#ifndef OTA_SECURE_CLIENT_MFLN
#define OTA_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN
#endif
#ifndef OTA_SECURE_CLIENT_INCLUDE_CA
#define OTA_SECURE_CLIENT_INCLUDE_CA 0 // Use user-provided CA. Only PROGMEM PEM option is supported.
// TODO: eventually should be replaced with pre-parsed structs, read directly from flash
// (ref: https://github.com/earlephilhower/bearssl-esp8266/pull/14)
//
// When enabled, current implementation includes "static/ota_client_trusted_root_ca.h" with
// const char _ota_client_trusted_root_ca[] PROGMEM = "...PEM data...";
// By default, using DigiCert root in "static/digicert_evroot_pem.h" (for https://github.com)
#endif #endif
#define OTA_GITHUB_FP "D7:9F:07:61:10:B3:92:93:E3:49:AC:89:84:5B:03:80:C1:9E:2F:8B"
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// NOFUSS // NOFUSS
@ -698,26 +872,37 @@
#endif #endif
#ifndef MQTT_USE_ASYNC
#define MQTT_USE_ASYNC 1 // Use AysncMQTTClient (1) or PubSubClient (0)
#ifndef MQTT_LIBRARY
#define MQTT_LIBRARY MQTT_LIBRARY_ASYNCMQTTCLIENT // MQTT_LIBRARY_ASYNCMQTTCLIENT (default, https://github.com/marvinroger/async-mqtt-client)
// MQTT_LIBRARY_PUBSUBCLIENT (https://github.com/knolleary/pubsubclient)
// MQTT_LIBRARY_ARDUINOMQTT (https://github.com/256dpi/arduino-mqtt)
#endif #endif
// -----------------------------------------------------------------------------
// MQTT OVER SSL // MQTT OVER SSL
// Using MQTT over SSL works pretty well but generates problems with the web interface.
// It could be a good idea to use it in conjuntion with WEB_SUPPORT=0.
// Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0.
// -----------------------------------------------------------------------------
//
// Requires SECURE_CLIENT set to SECURE_CLIENT_AXTLS or SECURE_CLIENT_BEARSSL
// It is recommended to use MQTT_LIBRARY_ARDUINOMQTT or MQTT_LIBRARY_PUBSUBCLIENT
// It is recommended to use SECURE_CLIENT_BEARSSL
// It is recommended to use ESP8266 Arduino Core >= 2.5.2 with SECURE_CLIENT_BEARSSL
//
// Current version of MQTT_LIBRARY_ASYNCMQTTCLIENT only supports SECURE_CLIENT_AXTLS
// //
// You can use SSL with MQTT_USE_ASYNC=1 (AsyncMqttClient library)
// but you might experience hiccups on the web interface, so my recommendation is:
// WEB_SUPPORT=0
// It is recommended to use WEB_SUPPORT=0 with either SECURE_CLIENT option, as there are miscellaneous problems when using them simultaneously
// (although, things might've improved, and I'd encourage to check whether this is true or not)
// //
// If you use SSL with MQTT_USE_ASYNC=0 (PubSubClient library)
// you will have to disable all the modules that use ESPAsyncTCP, that is:
// ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0, THINGSPEAK_SUPPORT=0 and WEB_SUPPORT=0
// When using MQTT_LIBRARY_PUBSUBCLIENT or MQTT_LIBRARY_ARDUINOMQTT, you will have to disable every module that uses ESPAsyncTCP:
// ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0, THINGSPEAK_SUPPORT=0, DEBUG_TELNET_SUPPORT=0 and WEB_SUPPORT=0
// Or, use "sync" versions instead (note that not every module has this option):
// THINGSPEAK_USE_ASYNC=0, TELNET_SERVER=TELNET_SERVER_WIFISERVER
// //
// You will need the fingerprint for your MQTT server, example for CloudMQTT:
// $ echo -n | openssl s_client -connect m11.cloudmqtt.com:24055 > cloudmqtt.pem
// $ openssl x509 -noout -in cloudmqtt.pem -fingerprint -sha1
// See SECURE_CLIENT_CHECK for all possible connection verification options.
//
// The simpliest way to verify SSL connection is to use fingerprinting.
// For example, to get Google's MQTT server certificate fingerprint, run the following command:
// $ echo -n | openssl s_client -connect mqtt.googleapis.com:8883 2>&1 | openssl x509 -noout -fingerprint -sha1 | cut -d\= -f2
// Note that fingerprint will change when certificate changes e.g. LetsEncrypt renewals or when the CSR updates
#ifndef MQTT_SSL_ENABLED #ifndef MQTT_SSL_ENABLED
#define MQTT_SSL_ENABLED 0 // By default MQTT over SSL will not be enabled #define MQTT_SSL_ENABLED 0 // By default MQTT over SSL will not be enabled
@ -727,6 +912,20 @@
#define MQTT_SSL_FINGERPRINT "" // SSL fingerprint of the server #define MQTT_SSL_FINGERPRINT "" // SSL fingerprint of the server
#endif #endif
#ifndef MQTT_SECURE_CLIENT_CHECK
#define MQTT_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK // Use global verification setting by default
#endif
#ifndef MQTT_SECURE_CLIENT_MFLN
#define MQTT_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN // Use global MFLN setting by default
#endif
#ifndef MQTT_SECURE_CLIENT_INCLUDE_CA
#define MQTT_SECURE_CLIENT_INCLUDE_CA 0 // Use user-provided CA. Only PROGMEM PEM option is supported.
// When enabled, current implementation includes "static/mqtt_client_trusted_root_ca.h" with
// const char _mqtt_client_trusted_root_ca[] PROGMEM = "...PEM data...";
// By default, using LetsEncrypt X3 root in "static/letsencrypt_isrgroot_pem.h"
#endif
#ifndef MQTT_ENABLED #ifndef MQTT_ENABLED
#define MQTT_ENABLED 0 // Do not enable MQTT connection by default #define MQTT_ENABLED 0 // Do not enable MQTT connection by default
@ -846,7 +1045,9 @@
#define MQTT_TOPIC_TIMESTAMP "timestamp" #define MQTT_TOPIC_TIMESTAMP "timestamp"
#define MQTT_TOPIC_FREEHEAP "freeheap" #define MQTT_TOPIC_FREEHEAP "freeheap"
#define MQTT_TOPIC_VCC "vcc" #define MQTT_TOPIC_VCC "vcc"
#ifndef MQTT_TOPIC_STATUS
#define MQTT_TOPIC_STATUS "status" #define MQTT_TOPIC_STATUS "status"
#endif
#define MQTT_TOPIC_MAC "mac" #define MQTT_TOPIC_MAC "mac"
#define MQTT_TOPIC_RSSI "rssi" #define MQTT_TOPIC_RSSI "rssi"
#define MQTT_TOPIC_MESSAGE_ID "id" #define MQTT_TOPIC_MESSAGE_ID "id"
@ -890,13 +1091,16 @@
#define MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX "notify_temp_range_max" #define MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX "notify_temp_range_max"
#ifndef MQTT_STATUS_ONLINE
#define MQTT_STATUS_ONLINE "1" // Value for the device ON message #define MQTT_STATUS_ONLINE "1" // Value for the device ON message
#endif
#ifndef MQTT_STATUS_OFFLINE
#define MQTT_STATUS_OFFLINE "0" // Value for the device OFF message (will) #define MQTT_STATUS_OFFLINE "0" // Value for the device OFF message (will)
#endif
#define MQTT_ACTION_RESET "reboot" // RESET MQTT topic particle #define MQTT_ACTION_RESET "reboot" // RESET MQTT topic particle
#define MQTT_MESSAGE_ID_SHIFT 1000 // Store MQTT message id into EEPROM every these many
// Custom get and set postfixes // Custom get and set postfixes
// Use something like "/status" or "/set", with leading slash // Use something like "/status" or "/set", with leading slash
// Since 1.9.0 the default value is "" for getter and "/set" for setter // Since 1.9.0 the default value is "" for getter and "/set" for setter
@ -956,15 +1160,18 @@
#define LIGHT_SAVE_DELAY 5 // Persist color after 5 seconds to avoid wearing out #define LIGHT_SAVE_DELAY 5 // Persist color after 5 seconds to avoid wearing out
#endif #endif
#ifndef LIGHT_MIN_PWM
#define LIGHT_MIN_PWM 0
#endif
#ifndef LIGHT_MAX_PWM #ifndef LIGHT_MAX_PWM
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
#define LIGHT_MAX_PWM 255 #define LIGHT_MAX_PWM 255
#endif
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#elif LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz #define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz
#else
#define LIGHT_MAX_PWM 0
#endif #endif
#endif // LIGHT_MAX_PWM #endif // LIGHT_MAX_PWM
@ -973,16 +1180,45 @@
#define LIGHT_LIMIT_PWM LIGHT_MAX_PWM // Limit PWM to this value (prevent 100% power) #define LIGHT_LIMIT_PWM LIGHT_MAX_PWM // Limit PWM to this value (prevent 100% power)
#endif #endif
#ifndef LIGHT_MIN_VALUE
#define LIGHT_MIN_VALUE 0 // Minimum light value
#endif
#ifndef LIGHT_MAX_VALUE #ifndef LIGHT_MAX_VALUE
#define LIGHT_MAX_VALUE 255 // Maximum light value #define LIGHT_MAX_VALUE 255 // Maximum light value
#endif #endif
#ifndef LIGHT_MIN_BRIGHTNESS
#define LIGHT_MIN_BRIGHTNESS 0 // Minimum brightness value
#endif
#ifndef LIGHT_MAX_BRIGHTNESS #ifndef LIGHT_MAX_BRIGHTNESS
#define LIGHT_MAX_BRIGHTNESS 255 // Maximun brightness value
#define LIGHT_MAX_BRIGHTNESS 255 // Maximum brightness value
#endif #endif
#define LIGHT_MIN_MIREDS 153 // Default to the Philips Hue value that HA also use.
#define LIGHT_MAX_MIREDS 500 // https://developers.meethue.com/documentation/core-concepts
// Default mireds & kelvin to the Philips Hue limits
// https://developers.meethue.com/documentation/core-concepts
//
// Home Assistant also uses these, see Light::min_mireds, Light::max_mireds
// https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/light/__init__.py
// Used when LIGHT_USE_WHITE AND LIGHT_USE_CCT is 1 - (1000000/Kelvin = MiReds)
// Warning! Don't change this yet, NOT FULLY IMPLEMENTED!
#ifndef LIGHT_COLDWHITE_MIRED
#define LIGHT_COLDWHITE_MIRED 153 // Coldwhite Strip, Value must be __BELOW__ W2!! (Default: 6535 Kelvin/153 MiRed)
#endif
#ifndef LIGHT_WARMWHITE_MIRED
#define LIGHT_WARMWHITE_MIRED 500 // Warmwhite Strip, Value must be __ABOVE__ W1!! (Default: 2000 Kelvin/500 MiRed)
#endif
#ifndef LIGHT_COLDWHITE_KELVIN
#define LIGHT_COLDWHITE_KELVIN 6536
#endif
#ifndef LIGHT_WARMWHITE_KELVIN
#define LIGHT_WARMWHITE_KELVIN 2000
#endif
#ifndef LIGHT_STEP #ifndef LIGHT_STEP
#define LIGHT_STEP 32 // Step size #define LIGHT_STEP 32 // Step size
@ -1000,11 +1236,6 @@
#define LIGHT_USE_CCT 0 // Use the 5th channel as Coldwhite LEDs, LIGHT_USE_WHITE must be 1. #define LIGHT_USE_CCT 0 // Use the 5th channel as Coldwhite LEDs, LIGHT_USE_WHITE must be 1.
#endif #endif
// Used when LIGHT_USE_WHITE AND LIGHT_USE_CCT is 1 - (1000000/Kelvin = MiReds)
// Warning! Don't change this yet, NOT FULLY IMPLEMENTED!
#define LIGHT_COLDWHITE_MIRED 153 // Coldwhite Strip, Value must be __BELOW__ W2!! (Default: 6535 Kelvin/153 MiRed)
#define LIGHT_WARMWHITE_MIRED 500 // Warmwhite Strip, Value must be __ABOVE__ W1!! (Default: 2000 Kelvin/500 MiRed)
#ifndef LIGHT_USE_GAMMA #ifndef LIGHT_USE_GAMMA
#define LIGHT_USE_GAMMA 0 // Use gamma correction for color channels #define LIGHT_USE_GAMMA 0 // Use gamma correction for color channels
#endif #endif
@ -1044,9 +1275,17 @@
#define DOMOTICZ_SUPPORT MQTT_SUPPORT // Build with domoticz (if MQTT) support (1.72Kb) #define DOMOTICZ_SUPPORT MQTT_SUPPORT // Build with domoticz (if MQTT) support (1.72Kb)
#endif #endif
#ifndef DOMOTICZ_ENABLED
#define DOMOTICZ_ENABLED 0 // Disable domoticz by default #define DOMOTICZ_ENABLED 0 // Disable domoticz by default
#endif
#ifndef DOMOTICZ_IN_TOPIC
#define DOMOTICZ_IN_TOPIC "domoticz/in" // Default subscription topic #define DOMOTICZ_IN_TOPIC "domoticz/in" // Default subscription topic
#endif
#ifndef DOMOTICZ_OUT_TOPIC
#define DOMOTICZ_OUT_TOPIC "domoticz/out" // Default publication topic #define DOMOTICZ_OUT_TOPIC "domoticz/out" // Default publication topic
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// HOME ASSISTANT // HOME ASSISTANT
@ -1064,22 +1303,6 @@
#define HOMEASSISTANT_PREFIX "homeassistant" // Default MQTT prefix #define HOMEASSISTANT_PREFIX "homeassistant" // Default MQTT prefix
#endif #endif
#ifndef HOMEASSISTANT_PAYLOAD_ON
#define HOMEASSISTANT_PAYLOAD_ON "1" // Payload for ON and available messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_OFF
#define HOMEASSISTANT_PAYLOAD_OFF "0" // Payload for OFF and unavailable messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_AVAILABLE
#define HOMEASSISTANT_PAYLOAD_AVAILABLE "1" // Payload for available messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE
#define HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE "0" // Payload for available messages
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// INFLUXDB // INFLUXDB
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -1139,7 +1362,7 @@
// THINGSPEAK OVER SSL // THINGSPEAK OVER SSL
// Using THINGSPEAK over SSL works well but generates problems with the web interface, // Using THINGSPEAK over SSL works well but generates problems with the web interface,
// so you should compile it with WEB_SUPPORT to 0. // so you should compile it with WEB_SUPPORT to 0.
// When THINGSPEAK_USE_ASYNC is 1, requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0.
// When THINGSPEAK_USE_ASYNC is 1, requires SECURE_CLIENT = SECURE_CLIENT_AXTLS and ESP8266 Arduino Core >= 2.4.0.
#define THINGSPEAK_USE_SSL 0 // Use secure connection #define THINGSPEAK_USE_SSL 0 // Use secure connection
#define THINGSPEAK_FINGERPRINT "78 60 18 44 81 35 BF DF 77 84 D4 0A 22 0D 9B 4E 6C DC 57 2C" #define THINGSPEAK_FINGERPRINT "78 60 18 44 81 35 BF DF 77 84 D4 0A 22 0D 9B 4E 6C DC 57 2C"
@ -1278,6 +1501,8 @@
#endif #endif
// Enable RCSwitch support // Enable RCSwitch support
// Originally implemented for SONOFF BASIC
// https://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff/
// Also possible to use with SONOFF RF BRIDGE, thanks to @wildwiz // Also possible to use with SONOFF RF BRIDGE, thanks to @wildwiz
// https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge---Direct-Hack // https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge---Direct-Hack
#ifndef RFB_DIRECT #ifndef RFB_DIRECT


+ 328
- 41
code/espurna/config/hardware.h View File

@ -20,11 +20,19 @@
// //
// Besides, other hardware specific information should be stated here // Besides, other hardware specific information should be stated here
// -----------------------------------------------------------------------------
// Custom hardware
// -----------------------------------------------------------------------------
#if defined(MANUFACTURER) and defined(DEVICE)
// user has defined custom hardware, no need to check anything else
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ESPurna Core // ESPurna Core
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if defined(ESPURNA_CORE)
#elif defined(ESPURNA_CORE)
// This is a special device targeted to generate a light-weight binary image // This is a special device targeted to generate a light-weight binary image
// meant to be able to do two-step-updates: // meant to be able to do two-step-updates:
@ -900,6 +908,25 @@
#define CSE7766_SUPPORT 1 #define CSE7766_SUPPORT 1
#define CSE7766_PIN 1 #define CSE7766_PIN 1
#elif defined(ITEAD_SONOFF_S31_LITE)
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_S31_LITE"
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
#elif defined(ITEAD_SONOFF_IFAN02) #elif defined(ITEAD_SONOFF_IFAN02)
// Info // Info
@ -1297,6 +1324,37 @@
#define RFB_DIRECT 1 #define RFB_DIRECT 1
#define RFB_RX_PIN 4 #define RFB_RX_PIN 4
#elif defined(MAGICHOME_ZJ_ESPM_5CH_B_13)
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_ESPM_5CH_B_13"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// LEDs
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
// Light
#define LIGHT_CHANNELS 5
#define LIGHT_CH1_PIN 14 // RED
#define LIGHT_CH2_PIN 12 // GREEN
#define LIGHT_CH3_PIN 13 // BLUE
#define LIGHT_CH4_PIN 5 // COLD WHITE
#define LIGHT_CH5_PIN 15 // WARM WHITE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0
#define LIGHT_CH5_INVERSE 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// HUACANXING H801 & H802 // HUACANXING H801 & H802
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -1689,6 +1747,11 @@
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1 #define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Light // Light
#define LIGHT_CHANNELS 5 #define LIGHT_CHANNELS 5
#define LIGHT_CH1_PIN 14 // RED #define LIGHT_CH1_PIN 14 // RED
@ -1765,6 +1828,31 @@
#define LED1_PIN 4 #define LED1_PIN 4
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// ISELECTOR SM-PW702
// -----------------------------------------------------------------------------
#elif defined(ISELECTOR_SM_PW702)
// Info
#define MANUFACTURER "ISELECTOR"
#define DEVICE "SM_PW702"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 4 //BLUE
#define LED1_PIN_INVERSE 0
#define LED2_PIN 5 //RED
#define LED2_PIN_INVERSE 1
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// AUTHOMETION LYT8266 // AUTHOMETION LYT8266
// https://authometion.com/shop/en/home/13-lyt8266.html // https://authometion.com/shop/en/home/13-lyt8266.html
@ -2079,6 +2167,9 @@
#define HLW8012_CURRENT_R 0.002 // Current resistor #define HLW8012_CURRENT_R 0.002 // Current resistor
#define HLW8012_VOLTAGE_R_UP ( 2 * 1000000 ) // Upstream voltage resistor #define HLW8012_VOLTAGE_R_UP ( 2 * 1000000 ) // Upstream voltage resistor
// LED1 on RX pin
#define DEBUG_SERIAL_SUPPORT 1
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Maxcio W-DE004 // Maxcio W-DE004
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -2438,8 +2529,9 @@
#ifndef DIGITAL_SUPPORT #ifndef DIGITAL_SUPPORT
#define DIGITAL_SUPPORT 1 #define DIGITAL_SUPPORT 1
#endif #endif
#define DIGITAL_PIN 16
#define DIGITAL_PIN_MODE INPUT
#define DIGITAL1_PIN 16
#define DIGITAL1_PIN_MODE INPUT
#define DIGITAL1_DEFAULT_STATE 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Heltec Touch Relay // Heltec Touch Relay
@ -2497,11 +2589,11 @@
#define LED1_PIN 1 #define LED1_PIN 1
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// Allnet 4duino ESP8266-UP-Relais
// http://www.allnet.de/de/allnet-brand/produkte/neuheiten/p/allnet-4duino-iot-wlan-relais-unterputz-esp8266-up-relais/
// https://shop.allnet.de/fileadmin/transfer/products/148814.pdf
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Allnet 4duino ESP8266-UP-Relais
// http://www.allnet.de/de/allnet-brand/produkte/neuheiten/p/allnet-4duino-iot-wlan-relais-unterputz-esp8266-up-relais/
// https://shop.allnet.de/fileadmin/transfer/products/148814.pdf
// -----------------------------------------------------------------------------
#elif defined(ALLNET_4DUINO_IOT_WLAN_RELAIS) #elif defined(ALLNET_4DUINO_IOT_WLAN_RELAIS)
@ -2807,6 +2899,9 @@
#define HLW8012_POWER_RATIO 3414290 #define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING #define HLW8012_INTERRUPT_ON FALLING
// BUTTON1 and LED1 are using Serial pins
#define DEBUG_SERIAL_SUPPORT 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Teckin SP22 v1.4 - v1.6 // Teckin SP22 v1.4 - v1.6
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -2848,6 +2943,9 @@
#define HLW8012_POWER_RATIO 2533110 #define HLW8012_POWER_RATIO 2533110
#define HLW8012_INTERRUPT_ON FALLING #define HLW8012_INTERRUPT_ON FALLING
// BUTTON1 and LED1 are using Serial pins
#define DEBUG_SERIAL_SUPPORT 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Several boards under different names uing a power chip labelled BL0937 or HJL-01 // Several boards under different names uing a power chip labelled BL0937 or HJL-01
// Also model number KS-602S // Also model number KS-602S
@ -2872,6 +2970,9 @@
#define LED1_PIN 1 #define LED1_PIN 1
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1
// LED1 is using TX pin
#define DEBUG_SERIAL_SUPPORT 0
// ---------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------
// Homecube 16A is similar but some pins differ and it also has RGB LEDs // Homecube 16A is similar but some pins differ and it also has RGB LEDs
// https://www.amazon.de/gp/product/B07D7RVF56/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1 // https://www.amazon.de/gp/product/B07D7RVF56/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1
@ -3054,7 +3155,93 @@
#define RELAY1_TYPE RELAY_TYPE_NORMAL #define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_PIN 5 #define RELAY2_PIN 5
#define RELAY2_TYPE RELAY_TYPE_NORMAL #define RELAY2_TYPE RELAY_TYPE_NORMAL
#elif defined(ALLTERCO_SHELLY1PM)
// Info
#define MANUFACTURER "ALLTERCO"
#define DEVICE "SHELLY1PM"
// Buttons
#define BUTTON1_PIN 4
#define BUTTON1_MODE BUTTON_SWITCH
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 2
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_LNGCLICK BUTTON_MODE_RESET
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_FACTORY
// Relays
#define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// Light
#define LED1_PIN 0
#define LED1_PIN_INVERSE 1
// HJL01 / BL0937
#define HLW8012_SUPPORT 1
#define HLW8012_SEL_PIN 12
#define HLW8012_CF1_PIN 13
#define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
//Temperature
#define NTC_SUPPORT 1
#define SENSOR_SUPPORT 1
#define NTC_BETA 3350
#define NTC_R_UP 10000
#define NTC_R_DOWN 0
#define NTC_R0 8000
#elif defined(ALLTERCO_SHELLY25)
// Info
#define MANUFACTURER "ALLTERCO"
#define DEVICE "SHELLY25"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_SWITCH
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 5
#define BUTTON2_MODE BUTTON_SWITCH
#define BUTTON2_RELAY 2
#define BUTTON3_PIN 2
#define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_LNGCLICK BUTTON_MODE_RESET
#define BUTTON3_LNGLNGCLICK BUTTON_MODE_FACTORY
// Relays
#define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_PIN 15
#define RELAY2_TYPE RELAY_TYPE_NORMAL
// Light
#define LED1_PIN 0
#define LED1_PIN_INVERSE 1
//Temperature
#define NTC_SUPPORT 1
#define SENSOR_SUPPORT 1
#define NTC_BETA 3350
#define NTC_R_UP 10000
#define NTC_R_DOWN 0
#define NTC_R0 8000
//Current
#define ADE7953_SUPPORT 1
#define I2C_SDA_PIN 12
#define I2C_SCL_PIN 14
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#elif defined(LOHAS_9W) #elif defined(LOHAS_9W)
@ -3197,6 +3384,32 @@
#define LIGHT_CH3_INVERSE 0 #define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0 #define LIGHT_CH4_INVERSE 0
// -----------------------------------------------------------------------------
// Nexete A19
// https://www.ebay.com/itm/Wifi-Smart-LED-light-Bulb-9W-60W-A19-850LM-RGBW-Dimmable-for-Alexa-Google-Home/283514779201
// -----------------------------------------------------------------------------
#elif defined(NEXETE_A19)
// Info
#define MANUFACTURER "NEXETE"
#define DEVICE "A19"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
#define LIGHT_CH1_PIN 12 // RED
#define LIGHT_CH2_PIN 15 // GREEN
#define LIGHT_CH3_PIN 14 // BLUE
#define LIGHT_CH4_PIN 5 // WHITE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Lombex Lux Nova 2 Tunable White // Lombex Lux Nova 2 Tunable White
// https://www.amazon.com/Lombex-Compatible-Equivalent-Dimmable-2700K-6500K/dp/B07B8K72PR // https://www.amazon.com/Lombex-Compatible-Equivalent-Dimmable-2700K-6500K/dp/B07B8K72PR
@ -3286,7 +3499,7 @@
// Relays // Relays
#define RELAY1_PIN 15 #define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL #define RELAY1_TYPE RELAY_TYPE_NORMAL
// Light RGBW // Light RGBW
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
@ -3449,22 +3662,22 @@
// Teckin SP20 // Teckin SP20
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#elif defined(TECKIN_SP20)
#elif defined(TECKIN_SP20)
// Info
// Info
#define MANUFACTURER "TECKIN" #define MANUFACTURER "TECKIN"
#define DEVICE "SP20" #define DEVICE "SP20"
// Buttons
// Buttons
#define BUTTON1_PIN 13 #define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1 #define BUTTON1_RELAY 1
// Relays
// Relays
#define RELAY1_PIN 4 #define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL #define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
// LEDs
#define LED1_PIN 2 #define LED1_PIN 2
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1
#define LED2_PIN 0 #define LED2_PIN 0
@ -3472,7 +3685,7 @@
#define LED2_MODE LED_MODE_FINDME #define LED2_MODE LED_MODE_FINDME
#define LED2_RELAY 0 #define LED2_RELAY 0
// HJL01 / BL0937
// HJL01 / BL0937
#ifndef HLW8012_SUPPORT #ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1 #define HLW8012_SUPPORT 1
#endif #endif
@ -3480,7 +3693,7 @@
#define HLW8012_CF1_PIN 14 #define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5 #define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740 #define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400 #define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290 #define HLW8012_POWER_RATIO 3414290
@ -3519,32 +3732,32 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#elif defined(PSH_WIFI_PLUG) #elif defined(PSH_WIFI_PLUG)
// Info // Info
#define MANUFACTURER "PSH" #define MANUFACTURER "PSH"
#define DEVICE "WIFI_PLUG" #define DEVICE "WIFI_PLUG"
// Relays // Relays
#define RELAY1_PIN 2 #define RELAY1_PIN 2
#define RELAY1_TYPE RELAY_TYPE_NORMAL #define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs // LEDs
#define LED1_PIN 0 #define LED1_PIN 0
#define LED1_PIN_INVERSE 0 #define LED1_PIN_INVERSE 0
#elif defined(PSH_RGBW_CONTROLLER) #elif defined(PSH_RGBW_CONTROLLER)
// Info // Info
#define MANUFACTURER "PSH" #define MANUFACTURER "PSH"
#define DEVICE "RGBW_CONTROLLER" #define DEVICE "RGBW_CONTROLLER"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1 #define DUMMY_RELAY_COUNT 1
// LEDs // LEDs
#define LED1_PIN 13 #define LED1_PIN 13
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1
// Light // Light
#define LIGHT_CHANNELS 4 #define LIGHT_CHANNELS 4
#define LIGHT_CH1_PIN 5 // RED #define LIGHT_CH1_PIN 5 // RED
@ -3555,9 +3768,9 @@
#define LIGHT_CH2_INVERSE 0 #define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0 #define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0 #define LIGHT_CH4_INVERSE 0
#elif defined(PSH_WIFI_SENSOR) #elif defined(PSH_WIFI_SENSOR)
// Info // Info
#define MANUFACTURER "PSH" #define MANUFACTURER "PSH"
#define DEVICE "WIFI_SENSOR" #define DEVICE "WIFI_SENSOR"
@ -3573,6 +3786,81 @@
#define LDR_ON_GROUND false #define LDR_ON_GROUND false
#define LDR_RESISTOR 10000 #define LDR_RESISTOR 10000
#elif defined(JINVOO_VALVE_SM_AW713)
// Reflashing from original Tuya firmware
// to thirdparty firmware like espurna by:
// https://github.com/ct-Open-Source/tuya-convert
// Info
#define MANUFACTURER "JINVOO"
#define DEVICE "VALVE_SM_AW713"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LED
#define LED1_PIN 5 // 5 red led
#define LED1_PIN_INVERSE 0
#define LED1_RELAY 1
#define LED1_MODE LED_MODE_RELAY
#define LED2_PIN 4 // 4 blue led
#define LED2_PIN_INVERSE 0
#define LED2_RELAY 1
#define LED2_MODE LED_MODE_FINDME_WIFI
// -----------------------------------------------------------------------------
// Etekcity ESW01-USA
// https://www.amazon.com/Etekcity-Voltson-Outlet-Monitoring-Required/dp/B01M3MYIFS
// -----------------------------------------------------------------------------
#elif defined(ETEKCITY_ESW01_USA)
// Info
#define MANUFACTURER "ETEKCITY"
#define DEVICE "ESW01-USA"
// Buttons
#define BUTTON1_PIN 14
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
// Blue
#define LED1_PIN 5
#define LED1_PIN_INVERSE 0
#define LED1_MODE LED_MODE_WIFI
// Yellow
#define LED2_PIN 16
#define LED2_PIN_INVERSE 0
#define LED2_MODE LED_MODE_FOLLOW
#define LED2_RELAY 1
// HLW8012
#ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1
#endif
#define HLW8012_SEL_PIN 15
#define HLW8012_CF1_PIN 12
#define HLW8012_CF_PIN 13
#define HLW8012_SEL_CURRENT HIGH // SEL pin to HIGH to measure current
#define HLW8012_CURRENT_R 0.001 // Current resistor
#define HLW8012_VOLTAGE_R_UP ( 4 * 470000 ) // Upstream voltage resistor
#define HLW8012_VOLTAGE_R_DOWN ( 1000 ) // Downstream voltage resistor
#define HLW8012_INTERRUPT_ON CHANGE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEST boards (do not use!!) // TEST boards (do not use!!)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -3628,6 +3916,7 @@
#define LIGHT_CHANNELS 1 #define LIGHT_CHANNELS 1
#define LIGHT_CH1_PIN 5 #define LIGHT_CH1_PIN 5
#define LIGHT_CH1_INVERSE 0 #define LIGHT_CH1_INVERSE 0
#define ENCODER_SUPPORT 1
// A bit of HLW8012 - pins 6,7,8 // A bit of HLW8012 - pins 6,7,8
#ifndef HLW8012_SUPPORT #ifndef HLW8012_SUPPORT
@ -3655,13 +3944,13 @@
#define MICS2710_SUPPORT 1 #define MICS2710_SUPPORT 1
#define MICS5525_SUPPORT 1 #define MICS5525_SUPPORT 1
// MAX6675 14 11 10
#ifndef MAX6675_SUPPORT
#define MAX6675_SUPPORT 1
#endif
#define MAX6675_CS_PIN 14
#define MAX6675_SO_PIN 11
#define MAX6675_SCK_PIN 10
// MAX6675 14 11 10
#ifndef MAX6675_SUPPORT
#define MAX6675_SUPPORT 1
#endif
#define MAX6675_CS_PIN 14
#define MAX6675_SO_PIN 11
#define MAX6675_SCK_PIN 10
#elif defined(TRAVIS02) #elif defined(TRAVIS02)
@ -3703,7 +3992,7 @@
// A bit of EVENTS - pin 10 // A bit of EVENTS - pin 10
#define EVENTS_SUPPORT 1 #define EVENTS_SUPPORT 1
#define EVENTS_PIN 6
#define EVENTS1_PIN 6
// Sonar // Sonar
#define SONAR_SUPPORT 1 #define SONAR_SUPPORT 1
@ -3735,6 +4024,7 @@
#define INFLUXDB_SUPPORT 1 #define INFLUXDB_SUPPORT 1
#define IR_SUPPORT 1 #define IR_SUPPORT 1
#define RF_SUPPORT 1 #define RF_SUPPORT 1
#define OTA_MQTT_SUPPORT 1
#define RFB_DIRECT 1 #define RFB_DIRECT 1
#define RFB_RX_PIN 4 #define RFB_RX_PIN 4
@ -3766,8 +4056,8 @@
// will not work on real life since they all share GPIO // will not work on real life since they all share GPIO
// but it's OK to test build // but it's OK to test build
#define EMON_ANALOG_SUPPORT 1 #define EMON_ANALOG_SUPPORT 1
#define NTC_SENSOR 1
#define LDR_SENSOR 1
#define NTC_SUPPORT 1
#define LDR_SUPPORT 1
#define PULSEMETER_SUPPORT 1 #define PULSEMETER_SUPPORT 1
@ -3777,12 +4067,9 @@
#define SSDP_SUPPORT 1 #define SSDP_SUPPORT 1
#define RF_SUPPORT 1 #define RF_SUPPORT 1
#endif
#else
// -----------------------------------------------------------------------------
// Check definitions
// -----------------------------------------------------------------------------
#if not defined(MANUFACTURER) || not defined(DEVICE)
#error "UNSUPPORTED HARDWARE!!" #error "UNSUPPORTED HARDWARE!!"
#endif #endif

+ 29
- 0
code/espurna/config/progmem.h View File

@ -135,6 +135,32 @@ PROGMEM const char espurna_modules[] =
#endif #endif
""; "";
PROGMEM const char espurna_ota_modules[] =
#if OTA_ARDUINOOTA_SUPPORT
"ARDUINO "
#endif
#if (OTA_CLIENT == OTA_CLIENT_ASYNCTCP)
"ASYNCTCP "
#endif
#if (OTA_CLIENT == OTA_CLIENT_HTTPUPDATE)
#if (SECURE_CLIENT == SECURE_CLIENT_NONE)
"*HTTPUPDATE "
#endif
#if (SECURE_CLIENT == SECURE_CLIENT_AXTLS)
"*HTTPUPDATE_AXTLS "
#endif
#if (SECURE_CLIENT == SECURE_CLIENT_BEARSSL)
"*HTTPUPDATE_BEARSSL "
#endif
#endif // OTA_CLIENT_HTTPUPDATE
#if OTA_MQTT_SUPPORT
"MQTT "
#endif
#if WEB_SUPPORT
"WEB "
#endif
"";
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// Sensors // Sensors
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
@ -247,6 +273,9 @@ PROGMEM const char espurna_sensors[] =
#if EZOPH_SUPPORT #if EZOPH_SUPPORT
"EZOPH " "EZOPH "
#endif #endif
#if ADE7953_SUPPORT
"ADE7953 "
#endif
""; "";


+ 276
- 44
code/espurna/config/prototypes.h View File

@ -1,7 +1,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <functional> #include <functional>
#include <pgmspace.h>
#include <vector>
#include <memory>
#include <core_version.h> #include <core_version.h>
extern "C" { extern "C" {
@ -10,25 +11,75 @@ extern "C" {
} }
#define UNUSED(x) (void)(x) #define UNUSED(x) (void)(x)
#define INLINE inline __attribute__((always_inline))
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// System // System
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#define LWIP_INTERNAL
#include <ESP8266WiFi.h>
#undef LWIP_INTERNAL
extern "C" {
#include <lwip/opt.h>
#include <lwip/ip.h>
#include <lwip/tcp.h>
#include <lwip/inet.h> // ip_addr_t
#include <lwip/err.h> // ERR_x
#include <lwip/dns.h> // dns_gethostbyname
#include <lwip/ip_addr.h> // ip4/ip6 helpers
#include <lwip/init.h> // LWIP_VERSION_MAJOR
}
uint32_t systemResetReason(); uint32_t systemResetReason();
uint8_t systemStabilityCounter(); uint8_t systemStabilityCounter();
void systemStabilityCounter(uint8_t); void systemStabilityCounter(uint8_t);
// -----------------------------------------------------------------------------
// PROGMEM
// -----------------------------------------------------------------------------
#include <pgmspace.h>
// ref: https://github.com/esp8266/Arduino/blob/master/tools/sdk/libc/xtensa-lx106-elf/include/sys/pgmspace.h
// __STRINGIZE && __STRINGIZE_NX && PROGMEM definitions port
// Do not replace macros unless running version older than 2.5.0
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_1) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_2)
// Quoting esp8266/Arduino comments:
// "Since __section__ is supposed to be only use for global variables,
// there could be conflicts when a static/inlined function has them in the
// same file as a non-static PROGMEM object.
// Ref: https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html
// Place each progmem object into its own named section, avoiding conflicts"
#define __TO_STR_(A) #A
#define __TO_STR(A) __TO_STR_(A)
#undef PROGMEM
#define PROGMEM __attribute__((section( "\".irom.text." __FILE__ "." __TO_STR(__LINE__) "." __TO_STR(__COUNTER__) "\"")))
// "PSTR() macro modified to start on a 32-bit boundary. This adds on average
// 1.5 bytes/string, but in return memcpy_P and strcpy_P will work 4~8x faster"
#undef PSTR
#define PSTR(s) (__extension__({static const char __c[] __attribute__((__aligned__(4))) PROGMEM = (s); &__c[0];}))
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// API // API
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using api_get_callback_f = std::function<void(char *, size_t)>;
using api_put_callback_f = std::function<void(const char *)> ;
#if WEB_SUPPORT #if WEB_SUPPORT
typedef std::function<void(char *, size_t)> api_get_callback_f;
typedef std::function<void(const char *)> api_put_callback_f;
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL); void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL);
#else
#define api_get_callback_f void *
#define api_put_callback_f void *
#endif #endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -41,8 +92,10 @@ void systemStabilityCounter(uint8_t);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Debug // Debug
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void debugSend(const char * format, ...);
void debugSend_P(PGM_P format, ...);
#include "../libs/DebugSend.h"
void debugSendImpl(const char*);
extern "C" { extern "C" {
void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); void custom_crash_callback(struct rst_info*, uint32_t, uint32_t);
} }
@ -66,6 +119,10 @@ extern "C" {
#endif #endif
} }
void infoMemory(const char* , unsigned int, unsigned int);
unsigned int getFreeHeap();
unsigned int getInitialFreeHeap();
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Domoticz // Domoticz
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -89,6 +146,12 @@ bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio); bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio); bool gpioReleaseLock(unsigned char gpio);
// -----------------------------------------------------------------------------
// Homeassistant
// -----------------------------------------------------------------------------
struct ha_config_t;
void haSetup();
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// I2C // I2C
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -114,21 +177,80 @@ int16_t i2c_read_int16(uint8_t address, uint8_t reg);
int16_t i2c_read_int16_le(uint8_t address, uint8_t reg); int16_t i2c_read_int16_le(uint8_t address, uint8_t reg);
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len); void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len);
// -----------------------------------------------------------------------------
// Lights
// -----------------------------------------------------------------------------
unsigned char lightChannels();
void lightState(unsigned char i, bool state);
bool lightState(unsigned char i);
void lightState(bool state);
bool lightState();
void lightBrightness(unsigned int brightness);
unsigned int lightBrightness();
unsigned int lightChannel(unsigned char id);
void lightChannel(unsigned char id, unsigned char value);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// MQTT // MQTT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if MQTT_SUPPORT
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
void mqttRegister(mqtt_callback_f callback);
String mqttMagnitude(char * topic);
#else
#define mqtt_callback_f void *
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
#include <ESPAsyncTCP.h>
#include <AsyncMqttClient.h>
#elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
#include <MQTTClient.h>
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
#include <PubSubClient.h>
#endif #endif
using mqtt_callback_f = std::function<void(unsigned int, const char *, char *)>;
void mqttRegister(mqtt_callback_f callback);
String mqttTopic(const char * magnitude, bool is_set);
String mqttTopic(const char * magnitude, unsigned int index, bool is_set);
String mqttMagnitude(char * topic);
void mqttSendRaw(const char * topic, const char * message, bool retain);
void mqttSendRaw(const char * topic, const char * message);
void mqttSend(const char * topic, const char * message, bool force, bool retain);
void mqttSend(const char * topic, const char * message, bool force);
void mqttSend(const char * topic, const char * message);
void mqttSend(const char * topic, unsigned int index, const char * message, bool force);
void mqttSend(const char * topic, unsigned int index, const char * message);
const String& mqttPayloadOnline();
const String& mqttPayloadOffline();
const char* mqttPayloadStatus(bool status);
void mqttSendStatus();
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// OTA // OTA
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include "ESPAsyncTCP.h"
#include <ArduinoOTA.h>
#if OTA_CLIENT == OTA_CLIENT_ASYNCTCP
#include <ESPAsyncTCP.h>
#endif
#if OTA_CLIENT == OTA_CLIENT_HTTPUPDATE
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#endif
#if SECURE_CLIENT != SECURE_CLIENT_NONE
#include <WiFiClientSecure.h>
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// RFM69 // RFM69
@ -148,10 +270,34 @@ typedef struct {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include <bitset> #include <bitset>
enum class RelayStatus : unsigned char {
OFF = 0,
ON = 1,
TOGGLE = 2,
UNKNOWN = 0xFF
};
RelayStatus relayParsePayload(const char * payload);
bool relayStatus(unsigned char id, bool status, bool report, bool group_report);
bool relayStatus(unsigned char id, bool status);
bool relayStatus(unsigned char id);
void relayToggle(unsigned char id, bool report, bool group_report);
void relayToggle(unsigned char id);
unsigned char relayCount();
const String& relayPayloadOn();
const String& relayPayloadOff();
const String& relayPayloadToggle();
const char* relayPayload(RelayStatus status);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Settings // Settings
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include <Embedis.h> #include <Embedis.h>
template<typename T> bool setSetting(const String& key, T value); template<typename T> bool setSetting(const String& key, T value);
template<typename T> bool setSetting(const String& key, unsigned int index, T value); template<typename T> bool setSetting(const String& key, unsigned int index, T value);
template<typename T> String getSetting(const String& key, T defaultValue); template<typename T> String getSetting(const String& key, T defaultValue);
@ -159,6 +305,17 @@ template<typename T> String getSetting(const String& key, unsigned int index, T
void settingsGetJson(JsonObject& data); void settingsGetJson(JsonObject& data);
bool settingsRestoreJson(JsonObject& data); bool settingsRestoreJson(JsonObject& data);
struct settings_cfg_t {
String& setting;
const char* key;
const char* default_value;
};
using settings_filter_t = std::function<String(String& value)>;
using settings_cfg_list_t = std::initializer_list<settings_cfg_t>;
void settingsProcessConfig(const settings_cfg_list_t& config, settings_filter_t filter = nullptr);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Terminal // Terminal
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -177,64 +334,117 @@ bool inline eraseSDKConfig();
#define ARRAYINIT(type, name, ...) type name[] = {__VA_ARGS__}; #define ARRAYINIT(type, name, ...) type name[] = {__VA_ARGS__};
size_t strnlen(const char*, size_t);
char* strnstr(const char*, const char*, size_t);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WebServer // WebServer
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class AsyncClient;
class AsyncWebServer;
#if WEB_SUPPORT #if WEB_SUPPORT
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
AsyncWebServer * webServer(); AsyncWebServer * webServer();
#else #else
#define AsyncWebServerRequest void
#define ArRequestHandlerFunction void
#define AsyncWebSocketClient void
#define AsyncWebSocket void
#define AwsEventType void *
class AsyncWebServerRequest;
class ArRequestHandlerFunction;
class AsyncWebSocketClient;
class AsyncWebSocket;
class AwsEventType;
#endif #endif
typedef std::function<bool(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)> web_body_callback_f;
typedef std::function<bool(AsyncWebServerRequest *request)> web_request_callback_f;
void webBodyRegister(web_body_callback_f callback);
void webRequestRegister(web_request_callback_f callback);
using web_body_callback_f = std::function<bool(AsyncWebServerRequest*, uint8_t*, size_t, size_t, size_t)>;
using web_request_callback_f = std::function<bool(AsyncWebServerRequest*)>;
void webBodyRegister(web_body_callback_f);
void webRequestRegister(web_request_callback_f);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WebSockets // WebSockets
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include <queue>
// TODO: pending configuration headers refactoring... here for now
struct ws_counter_t;
struct ws_data_t;
struct ws_debug_t;
struct ws_callbacks_t;
using ws_on_send_callback_f = std::function<void(JsonObject& root)>;
using ws_on_action_callback_f = std::function<void(uint32_t client_id, const char * action, JsonObject& data)>;
using ws_on_keycheck_callback_f = std::function<bool(const char * key, JsonVariant& value)>;
using ws_on_send_callback_list_t = std::vector<ws_on_send_callback_f>;
using ws_on_action_callback_list_t = std::vector<ws_on_action_callback_f>;
using ws_on_keycheck_callback_list_t = std::vector<ws_on_keycheck_callback_f>;
#if WEB_SUPPORT #if WEB_SUPPORT
typedef std::function<void(JsonObject&)> ws_on_send_callback_f;
void wsOnSendRegister(ws_on_send_callback_f callback);
void wsSend(uint32_t, JsonObject& root);
struct ws_callbacks_t {
ws_on_send_callback_list_t on_visible;
ws_on_send_callback_list_t on_connected;
ws_on_send_callback_list_t on_data;
ws_on_action_callback_list_t on_action;
ws_on_keycheck_callback_list_t on_keycheck;
ws_callbacks_t& onVisible(ws_on_send_callback_f);
ws_callbacks_t& onConnected(ws_on_send_callback_f);
ws_callbacks_t& onData(ws_on_send_callback_f);
ws_callbacks_t& onAction(ws_on_action_callback_f);
ws_callbacks_t& onKeyCheck(ws_on_keycheck_callback_f);
};
ws_callbacks_t& wsRegister();
void wsSetup();
void wsSend(uint32_t client_id, const char* data);
void wsSend(uint32_t client_id, JsonObject& root);
void wsSend(JsonObject& root); void wsSend(JsonObject& root);
void wsSend(ws_on_send_callback_f sender);
void wsSend(ws_on_send_callback_f callback);
typedef std::function<void(uint32_t, const char *, JsonObject&)> ws_on_action_callback_f;
void wsOnActionRegister(ws_on_action_callback_f callback);
void wsSend_P(PGM_P data);
void wsSend_P(uint32_t client_id, PGM_P data);
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
void INLINE wsPost(const ws_on_send_callback_f& callback);
void INLINE wsPost(uint32_t client_id, const ws_on_send_callback_f& callback);
void INLINE wsPost(const ws_on_send_callback_list_t& callbacks);
void INLINE wsPost(uint32_t client_id, const ws_on_send_callback_list_t& callbacks);
bool wsConnected();
bool wsConnected(uint32_t);
bool wsDebugSend(const char*, const char*);
#else
#define ws_on_send_callback_f void *
#define ws_on_action_callback_f void *
#define ws_on_receive_callback_f void *
void INLINE wsPostAll(uint32_t client_id, const ws_on_send_callback_list_t& callbacks);
void INLINE wsPostAll(const ws_on_send_callback_list_t& callbacks);
void INLINE wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& callbacks);
void INLINE wsPostSequence(uint32_t client_id, ws_on_send_callback_list_t&& callbacks);
void INLINE wsPostSequence(const ws_on_send_callback_list_t& callbacks);
bool INLINE wsConnected();
bool INLINE wsConnected(uint32_t client_id);
bool wsDebugSend(const char* prefix, const char* message);
#endif #endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WIFI // WIFI
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include "JustWifi.h"
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
#include <JustWifi.h>
struct wifi_scan_info_t;
using wifi_scan_f = std::function<void(wifi_scan_info_t& info)>;
using wifi_callback_f = std::function<void(justwifi_messages_t code, char * parameter)>;
void wifiRegister(wifi_callback_f callback); void wifiRegister(wifi_callback_f callback);
bool wifiConnected(); bool wifiConnected();
#if LWIP_VERSION_MAJOR == 1
#include <netif/etharp.h>
#else // LWIP_VERSION_MAJOR >= 2
#include <lwip/etharp.h>
#endif
// -----------------------------------------------------------------------------
// THERMOSTAT // THERMOSTAT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using thermostat_callback_f = std::function<void(bool)>;
#if THERMOSTAT_SUPPORT #if THERMOSTAT_SUPPORT
typedef std::function<void(bool)> thermostat_callback_f;
void thermostatRegister(thermostat_callback_f callback); void thermostatRegister(thermostat_callback_f callback);
#else
#define thermostat_callback_f void *
#endif #endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -242,3 +452,25 @@ bool wifiConnected();
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include "rtcmem.h" #include "rtcmem.h"
// -----------------------------------------------------------------------------
// Warn about broken Arduino functions
// -----------------------------------------------------------------------------
// Division by zero bug
// https://github.com/esp8266/Arduino/pull/2397
// https://github.com/esp8266/Arduino/pull/2408
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
long __attribute__((deprecated("Please avoid using map() with Core 2.3.0"))) map(long x, long in_min, long in_max, long out_min, long out_max);
#endif
// -----------------------------------------------------------------------------
// std::make_unique backport for C++11
// -----------------------------------------------------------------------------
#if 201103L >= __cplusplus
namespace std {
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
}
#endif

+ 1
- 1
code/espurna/config/rtcmem.h View File

@ -33,7 +33,7 @@ struct RtcmemData {
uint32_t relay; uint32_t relay;
uint32_t mqtt; uint32_t mqtt;
uint64_t light; uint64_t light;
double energy;
double energy[4];
}; };
static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big"); static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big");


+ 278
- 17
code/espurna/config/sensors.h View File

@ -167,11 +167,18 @@
// (BMX280_ADDRESS == 0x00) then sensor #1 is auto-discovered // (BMX280_ADDRESS == 0x00) then sensor #1 is auto-discovered
// (BMX280_ADDRESS != 0x00) then sensor #1 is the unnamed address // (BMX280_ADDRESS != 0x00) then sensor #1 is the unnamed address
#ifndef BMX280_MODE
#define BMX280_MODE 1 // 0 for sleep mode, 1 or 2 for forced mode, 3 for normal mode #define BMX280_MODE 1 // 0 for sleep mode, 1 or 2 for forced mode, 3 for normal mode
#endif
#ifndef BMX280_STANDBY
#define BMX280_STANDBY 0 // 0 for 0.5ms, 1 for 62.5ms, 2 for 125ms #define BMX280_STANDBY 0 // 0 for 0.5ms, 1 for 62.5ms, 2 for 125ms
// 3 for 250ms, 4 for 500ms, 5 for 1000ms // 3 for 250ms, 4 for 500ms, 5 for 1000ms
// 6 for 10ms, 7 for 20ms // 6 for 10ms, 7 for 20ms
#endif
#ifndef BMX280_FILTER
#define BMX280_FILTER 0 // 0 for OFF, 1 for 2 values, 2 for 4 values, 3 for 8 values and 4 for 16 values #define BMX280_FILTER 0 // 0 for OFF, 1 for 2 values, 2 for 4 values, 3 for 8 values and 4 for 16 values
#endif
#ifndef BMX280_TEMPERATURE
#define BMX280_TEMPERATURE 1 // Oversampling for temperature (set to 0 to disable magnitude) #define BMX280_TEMPERATURE 1 // Oversampling for temperature (set to 0 to disable magnitude)
// 0b000 = 0 = Skip measurement // 0b000 = 0 = Skip measurement
// 0b001 = 1 = 1x 16bit/0.0050C resolution // 0b001 = 1 = 1x 16bit/0.0050C resolution
@ -179,6 +186,8 @@
// 0b011 = 3 = 4x 18bit/0.0012C // 0b011 = 3 = 4x 18bit/0.0012C
// 0b100 = 4 = 8x 19bit/0.0006C // 0b100 = 4 = 8x 19bit/0.0006C
// 0b101 = 5 = 16x 20bit/0.0003C // 0b101 = 5 = 16x 20bit/0.0003C
#endif
#ifndef BMX280_HUMIDITY
#define BMX280_HUMIDITY 1 // Oversampling for humidity (set to 0 to disable magnitude, only for BME280) #define BMX280_HUMIDITY 1 // Oversampling for humidity (set to 0 to disable magnitude, only for BME280)
// 0b000 = 0 = Skip measurement // 0b000 = 0 = Skip measurement
// 0b001 = 1 = 1x 0.07% resolution // 0b001 = 1 = 1x 0.07% resolution
@ -186,6 +195,8 @@
// 0b011 = 3 = 4x 0.04% // 0b011 = 3 = 4x 0.04%
// 0b100 = 4 = 8x 0.03% // 0b100 = 4 = 8x 0.03%
// 0b101 = 5 = 16x 0.02% // 0b101 = 5 = 16x 0.02%
#endif
#ifndef BMX280_PRESSURE
#define BMX280_PRESSURE 1 // Oversampling for pressure (set to 0 to disable magnitude) #define BMX280_PRESSURE 1 // Oversampling for pressure (set to 0 to disable magnitude)
// 0b000 = 0 = Skipped // 0b000 = 0 = Skipped
// 0b001 = 1 = 1x 16bit/2.62 Pa resolution // 0b001 = 1 = 1x 16bit/2.62 Pa resolution
@ -193,7 +204,8 @@
// 0b011 = 3 = 4x 18bit/0.66 Pa // 0b011 = 3 = 4x 18bit/0.66 Pa
// 0b100 = 4 = 8x 19bit/0.33 Pa // 0b100 = 4 = 8x 19bit/0.33 Pa
// 0b101 = 5 = 16x 20bit/0.16 Pa // 0b101 = 5 = 16x 20bit/0.16 Pa
#endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Dallas OneWire temperature sensors // Dallas OneWire temperature sensors
// Enable support by passing DALLAS_SUPPORT=1 build flag // Enable support by passing DALLAS_SUPPORT=1 build flag
@ -260,16 +272,100 @@
#define DIGITAL_SUPPORT 0 #define DIGITAL_SUPPORT 0
#endif #endif
#ifndef DIGITAL_PIN
#define DIGITAL_PIN 2
#ifndef DIGITAL1_PIN
#define DIGITAL1_PIN 2
#endif
#ifndef DIGITAL1_PIN_MODE
#define DIGITAL1_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL1_DEFAULT_STATE
#define DIGITAL1_DEFAULT_STATE 1
#endif
#ifndef DIGITAL2_PIN
#define DIGITAL2_PIN 2
#endif
#ifndef DIGITAL2_PIN_MODE
#define DIGITAL2_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL2_DEFAULT_STATE
#define DIGITAL2_DEFAULT_STATE 1
#endif
#ifndef DIGITAL3_PIN
#define DIGITAL3_PIN 2
#endif
#ifndef DIGITAL3_PIN_MODE
#define DIGITAL3_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL3_DEFAULT_STATE
#define DIGITAL3_DEFAULT_STATE 1
#endif
#ifndef DIGITAL4_PIN
#define DIGITAL4_PIN 2
#endif
#ifndef DIGITAL4_PIN_MODE
#define DIGITAL4_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL4_DEFAULT_STATE
#define DIGITAL4_DEFAULT_STATE 1
#endif
#ifndef DIGITAL5_PIN
#define DIGITAL5_PIN 2
#endif
#ifndef DIGITAL5_PIN_MODE
#define DIGITAL5_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL5_DEFAULT_STATE
#define DIGITAL5_DEFAULT_STATE 1
#endif #endif
#ifndef DIGITAL_PIN_MODE
#define DIGITAL_PIN_MODE INPUT_PULLUP
#ifndef DIGITAL6_PIN
#define DIGITAL6_PIN 2
#endif #endif
#ifndef DIGITAL_DEFAULT_STATE
#define DIGITAL_DEFAULT_STATE 1
#ifndef DIGITAL6_PIN_MODE
#define DIGITAL6_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL6_DEFAULT_STATE
#define DIGITAL6_DEFAULT_STATE 1
#endif
#ifndef DIGITAL7_PIN
#define DIGITAL7_PIN 2
#endif
#ifndef DIGITAL7_PIN_MODE
#define DIGITAL7_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL7_DEFAULT_STATE
#define DIGITAL7_DEFAULT_STATE 1
#endif
#ifndef DIGITAL8_PIN
#define DIGITAL8_PIN 2
#endif
#ifndef DIGITAL8_PIN_MODE
#define DIGITAL8_PIN_MODE INPUT_PULLUP
#endif
#ifndef DIGITAL8_DEFAULT_STATE
#define DIGITAL8_DEFAULT_STATE 1
#endif #endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -366,24 +462,173 @@
#define EVENTS_SUPPORT 0 // Do not build with counter support by default #define EVENTS_SUPPORT 0 // Do not build with counter support by default
#endif #endif
#ifndef EVENTS_TRIGGER
#define EVENTS_TRIGGER 1 // 1 to trigger callback on events,
#ifndef EVENTS1_TRIGGER
#define EVENTS1_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS1_PIN
#define EVENTS1_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS1_PIN_MODE
#define EVENTS1_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS1_INTERRUPT_MODE
#define EVENTS1_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS1_DEBOUNCE
#define EVENTS1_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS2_TRIGGER
#define EVENTS2_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically // 0 to only count them and report periodically
#endif #endif
#ifndef EVENTS_PIN
#define EVENTS_PIN 2 // GPIO to monitor
#ifndef EVENTS2_PIN
#define EVENTS2_PIN 2 // GPIO to monitor
#endif #endif
#ifndef EVENTS_PIN_MODE
#define EVENTS_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#ifndef EVENTS2_PIN_MODE
#define EVENTS2_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif #endif
#ifndef EVENTS_INTERRUPT_MODE
#define EVENTS_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#ifndef EVENTS2_INTERRUPT_MODE
#define EVENTS2_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif #endif
#define EVENTS_DEBOUNCE 50 // Do not register events within less than 50 millis
#ifndef EVENTS2_DEBOUNCE
#define EVENTS2_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS3_TRIGGER
#define EVENTS3_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS3_PIN
#define EVENTS3_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS3_PIN_MODE
#define EVENTS3_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS3_INTERRUPT_MODE
#define EVENTS3_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS3_DEBOUNCE
#define EVENTS3_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS4_TRIGGER
#define EVENTS4_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS4_PIN
#define EVENTS4_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS4_PIN_MODE
#define EVENTS4_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS4_INTERRUPT_MODE
#define EVENTS4_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS4_DEBOUNCE
#define EVENTS4_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS5_TRIGGER
#define EVENTS5_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS5_PIN
#define EVENTS5_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS5_PIN_MODE
#define EVENTS5_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS5_INTERRUPT_MODE
#define EVENTS5_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS5_DEBOUNCE
#define EVENTS5_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS6_TRIGGER
#define EVENTS6_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS6_PIN
#define EVENTS6_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS6_PIN_MODE
#define EVENTS6_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS6_INTERRUPT_MODE
#define EVENTS6_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS6_DEBOUNCE
#define EVENTS6_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS7_TRIGGER
#define EVENTS7_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS7_PIN
#define EVENTS7_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS7_PIN_MODE
#define EVENTS7_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS7_INTERRUPT_MODE
#define EVENTS7_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS7_DEBOUNCE
#define EVENTS7_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
#ifndef EVENTS8_TRIGGER
#define EVENTS8_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS8_PIN
#define EVENTS8_PIN 2 // GPIO to monitor
#endif
#ifndef EVENTS8_PIN_MODE
#define EVENTS8_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef EVENTS8_INTERRUPT_MODE
#define EVENTS8_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE
#endif
#ifndef EVENTS8_DEBOUNCE
#define EVENTS8_DEBOUNCE 50 // Do not register events within less than 50 millis
#endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Geiger sensor // Geiger sensor
@ -669,7 +914,6 @@
#ifndef PULSEMETER_SUPPORT #ifndef PULSEMETER_SUPPORT
#define PULSEMETER_SUPPORT 0 #define PULSEMETER_SUPPORT 0
#endif #endif
#ifndef PULSEMETER_PIN #ifndef PULSEMETER_PIN
#define PULSEMETER_PIN 5 #define PULSEMETER_PIN 5
#endif #endif
@ -1011,6 +1255,19 @@
#define I2C_CLEAR_BUS 0 // Clear I2C bus on boot #define I2C_CLEAR_BUS 0 // Clear I2C bus on boot
#define I2C_PERFORM_SCAN 1 // Perform a bus scan on boot #define I2C_PERFORM_SCAN 1 // Perform a bus scan on boot
// -----------------------------------------------------------------------------
// ADE7953 Shelly Sensor
// Enable support by passing ADE7953_SUPPORT=1 build flag
// -----------------------------------------------------------------------------
#ifndef ADE7953_SUPPORT
#define ADE7953_SUPPORT 0
#endif
#ifndef ADE7953_ADDRESS
#define ADE7953_ADDRESS 0x38
#endif
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// Class loading // Class loading
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
@ -1161,4 +1418,8 @@
#include "../sensors/VL53L1XSensor.h" #include "../sensors/VL53L1XSensor.h"
#endif #endif
#if ADE7953_SUPPORT
#include "../sensors/ADE7953Sensor.h"
#endif
#endif // SENSOR_SUPPORT #endif // SENSOR_SUPPORT

+ 41
- 0
code/espurna/config/types.h View File

@ -75,6 +75,8 @@
#define RELAY_BOOT_ON 1 #define RELAY_BOOT_ON 1
#define RELAY_BOOT_SAME 2 #define RELAY_BOOT_SAME 2
#define RELAY_BOOT_TOGGLE 3 #define RELAY_BOOT_TOGGLE 3
#define RELAY_BOOT_LOCKED_OFF 4
#define RELAY_BOOT_LOCKED_ON 5
#define RELAY_TYPE_NORMAL 0 #define RELAY_TYPE_NORMAL 0
#define RELAY_TYPE_INVERSE 1 #define RELAY_TYPE_INVERSE 1
@ -101,6 +103,10 @@
#define RELAY_GROUP_SYNC_INVERSE 1 #define RELAY_GROUP_SYNC_INVERSE 1
#define RELAY_GROUP_SYNC_RECEIVEONLY 2 #define RELAY_GROUP_SYNC_RECEIVEONLY 2
#define RELAY_LOCK_OFF 0
#define RELAY_LOCK_ON 1
#define RELAY_LOCK_DISABLED 2
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// UDP SYSLOG // UDP SYSLOG
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -146,6 +152,12 @@
#define MQTT_DISCONNECT_EVENT 1 #define MQTT_DISCONNECT_EVENT 1
#define MQTT_MESSAGE_EVENT 2 #define MQTT_MESSAGE_EVENT 2
// MQTT_LIBRARY
#define MQTT_LIBRARY_ASYNCMQTTCLIENT 0
#define MQTT_LIBRARY_ARDUINOMQTT 1
#define MQTT_LIBRARY_PUBSUBCLIENT 2
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// LED // LED
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -303,6 +315,7 @@
#define SENSOR_BMP180_ID 34 #define SENSOR_BMP180_ID 34
#define SENSOR_MAX6675_ID 35 #define SENSOR_MAX6675_ID 35
#define SENSOR_LDR_ID 36 #define SENSOR_LDR_ID 36
#define SENSOR_ADE7953_ID 37
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// Magnitudes // Magnitudes
@ -343,3 +356,31 @@
#define MAGNITUDE_PH 31 #define MAGNITUDE_PH 31
#define MAGNITUDE_MAX 32 #define MAGNITUDE_MAX 32
//------------------------------------------------------------------------------
// Telnet server
//------------------------------------------------------------------------------
#define TELNET_SERVER_ASYNC 0
#define TELNET_SERVER_WIFISERVER 1
//------------------------------------------------------------------------------
// OTA Client (not related to the Web OTA support)
//------------------------------------------------------------------------------
#define OTA_CLIENT_NONE 0
#define OTA_CLIENT_ASYNCTCP 1
#define OTA_CLIENT_HTTPUPDATE 2
//------------------------------------------------------------------------------
// Secure Client
//------------------------------------------------------------------------------
#define SECURE_CLIENT_NONE 0
#define SECURE_CLIENT_AXTLS 1
#define SECURE_CLIENT_BEARSSL 2
#define SECURE_CLIENT_CHECK_NONE 0 // !!! INSECURE CONNECTION !!!
#define SECURE_CLIENT_CHECK_FINGERPRINT 1 // legacy fingerprint validation
#define SECURE_CLIENT_CHECK_CA 2 // set trust anchor from PROGMEM CA certificate

+ 55
- 18
code/espurna/crash.ino View File

@ -29,7 +29,8 @@ extern "C" {
* 8. depc * 8. depc
* 9. adress of stack start * 9. adress of stack start
* 10. adress of stack end * 10. adress of stack end
* 11. stack trace bytes
* 11. stack trace size
* 12. stack trace bytes
* ... * ...
*/ */
#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes #define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes
@ -42,12 +43,19 @@ extern "C" {
#define SAVE_CRASH_DEPC 0x16 // 4 bytes #define SAVE_CRASH_DEPC 0x16 // 4 bytes
#define SAVE_CRASH_STACK_START 0x1A // 4 bytes #define SAVE_CRASH_STACK_START 0x1A // 4 bytes
#define SAVE_CRASH_STACK_END 0x1E // 4 bytes #define SAVE_CRASH_STACK_END 0x1E // 4 bytes
#define SAVE_CRASH_STACK_TRACE 0x22 // variable
#define SAVE_CRASH_STACK_SIZE 0x22 // 2 bytes
#define SAVE_CRASH_STACK_TRACE 0x24 // variable
#define SAVE_CRASH_STACK_TRACE_MAX 0x80 // limit at 128 bytes (increment/decrement by 16)
uint16_t _save_crash_stack_trace_max = SAVE_CRASH_STACK_TRACE_MAX;
bool _save_crash_enabled = true;
/** /**
* Save crash information in EEPROM * Save crash information in EEPROM
* This function is called automatically if ESP8266 suffers an exception * This function is called automatically if ESP8266 suffers an exception
* It should be kept quick / consise to be able to execute before hardware wdt may kick in * It should be kept quick / consise to be able to execute before hardware wdt may kick in
* This method assumes EEPROM has already been initialized, which is the first thing ESPurna does
*/ */
extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) { extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) {
@ -56,37 +64,44 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack
return; return;
} }
// This method assumes EEPROM has already been initialized
// which is the first thing ESPurna does
// Check if runtime setting disabled this callback
if (!_save_crash_enabled) {
return;
}
// write crash time to EEPROM
// write crash time to EEPROM, which we will later use as a marker that there was a crash
uint32_t crash_time = millis(); uint32_t crash_time = millis();
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
// write reset info to EEPROM
// rst_info::reason and ::exccause are uint32_t, but are holding small values
EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason);
EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause);
// write epc1, epc2, epc3, excvaddr and depc to EEPROM
// write epc1, epc2, epc3, excvaddr and depc to EEPROM as uint32_t
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc);
// write stack start and end address to EEPROM
// EEPROM size is limited, write as little as possible.
// we sometimes want to avoid big stack traces, e.g. if stack_end == 0x3fffffb0, we are in SYS context.
// but still should get enough relevant info and it is possible to set needed size at build/runtime
const uint16_t stack_size = constrain((stack_end - stack_start), 0, _save_crash_stack_trace_max);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_SIZE, stack_size);
// starting address of Embedis data plus reserve
const uint16_t settings_start = SPI_FLASH_SEC_SIZE - settingsSize() - 0x10;
// starting EEPROM address of Embedis data plus reserve
const uint16_t settings_start = (
((SPI_FLASH_SEC_SIZE - settingsSize() + 31) & -32) - 0x20);
// write stack trace to EEPROM and avoid overwriting settings // write stack trace to EEPROM and avoid overwriting settings
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
for (uint32_t i = stack_start; i < stack_end; i++) {
if (current_address >= settings_start) break;
byte* byteValue = (byte*) i;
EEPROMr.write(current_address++, *byteValue);
int16_t eeprom_addr = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
for (uint32_t* addr = (uint32_t*)stack_start; addr < (uint32_t*)(stack_start + stack_size); addr++) {
if (eeprom_addr >= settings_start) break;
EEPROMr.put(eeprom_addr, *addr);
eeprom_addr += sizeof(uint32_t);
} }
EEPROMr.commit(); EEPROMr.commit();
@ -129,19 +144,22 @@ void crashDump() {
DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc); DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc);
uint32_t stack_start, stack_end; uint32_t stack_start, stack_end;
uint16_t stack_size;
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_SIZE, stack_size);
DEBUG_MSG_P(PSTR("[DEBUG] sp=0x%08x end=0x%08x\n"), stack_start, stack_end);
DEBUG_MSG_P(PSTR("sp=0x%08x end=0x%08x saved=0x%04x\n\n"), stack_start, stack_end, stack_size);
if (0xFFFF == stack_size) return;
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
int16_t stack_len = stack_end - stack_start;
uint32_t stack_trace; uint32_t stack_trace;
DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] ")); DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] "));
for (int16_t i = 0; i < stack_len; i += 0x10) {
for (int16_t i = 0; i < stack_size; i += 0x10) {
DEBUG_MSG_P(PSTR("%08x: "), stack_start + i); DEBUG_MSG_P(PSTR("%08x: "), stack_start + i);
for (byte j = 0; j < 4; j++) { for (byte j = 0; j < 4; j++) {
EEPROMr.get(current_address, stack_trace); EEPROMr.get(current_address, stack_trace);
@ -154,4 +172,23 @@ void crashDump() {
} }
void crashSetup() {
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("CRASH"), [](Embedis* e) {
crashDump();
crashClear();
terminalOK();
});
#endif
// Minumum of 16 and align for column formatter in crashDump()
_save_crash_stack_trace_max = getSetting("sysTraceMax", SAVE_CRASH_STACK_TRACE_MAX).toInt();
_save_crash_stack_trace_max = (_save_crash_stack_trace_max + 15) & -16;
setSetting("sysScTraceMax", _save_crash_stack_trace_max);
_save_crash_enabled = getSetting("sysCrashSave", 1).toInt() == 1;
}
#endif // DEBUG_SUPPORT #endif // DEBUG_SUPPORT

+ 18
- 50
code/espurna/debug.ino View File

@ -8,6 +8,8 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if DEBUG_SUPPORT #if DEBUG_SUPPORT
#include "libs/DebugSend.h"
#if DEBUG_UDP_SUPPORT #if DEBUG_UDP_SUPPORT
#include <WiFiUdp.h> #include <WiFiUdp.h>
WiFiUDP _udp_debug; WiFiUDP _udp_debug;
@ -36,7 +38,7 @@ char _udp_syslog_header[40] = {0};
} }
#endif #endif
void _debugSend(const char * message) {
void debugSendImpl(const char * message) {
const size_t msg_len = strlen(message); const size_t msg_len = strlen(message);
@ -85,63 +87,29 @@ void _debugSend(const char * message) {
} }
// -----------------------------------------------------------------------------
void debugSend(const char * format, ...) {
va_list args;
va_start(args, format);
char test[1];
int len = ets_vsnprintf(test, 1, format, args) + 1;
char * buffer = new char[len];
ets_vsnprintf(buffer, len, format, args);
va_end(args);
_debugSend(buffer);
delete[] buffer;
}
void debugSend_P(PGM_P format_P, ...) {
char format[strlen_P(format_P)+1];
memcpy_P(format, format_P, sizeof(format));
va_list args;
va_start(args, format_P);
char test[1];
int len = ets_vsnprintf(test, 1, format, args) + 1;
char * buffer = new char[len];
ets_vsnprintf(buffer, len, format, args);
va_end(args);
_debugSend(buffer);
delete[] buffer;
}
#if DEBUG_WEB_SUPPORT #if DEBUG_WEB_SUPPORT
void debugWebSetup() {
wsOnSendRegister([](JsonObject& root) {
root["dbgVisible"] = 1;
});
wsOnActionRegister([](uint32_t client_id, const char * action, JsonObject& data) {
void _debugWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
if (strcmp(action, "dbgcmd") == 0) { if (strcmp(action, "dbgcmd") == 0) {
const char* command = data.get<const char*>("command");
char buffer[strlen(command) + 2];
snprintf(buffer, sizeof(buffer), "%s\n", command);
terminalInject((void*) buffer, strlen(buffer));
if (!data.containsKey("command") || !data["command"].is<const char*>()) return;
const char* command = data["command"];
if (command && strlen(command)) {
auto command = data.get<const char*>("command");
terminalInject((void*) command, strlen(command));
terminalInject('\n');
}
} }
#endif #endif
});
}
void debugWebSetup() {
wsRegister()
.onVisible([](JsonObject& root) { root["dbgVisible"] = 1; })
.onAction(_debugWebSocketOnAction);
#if DEBUG_UDP_SUPPORT #if DEBUG_UDP_SUPPORT
#if DEBUG_UDP_PORT == 514 #if DEBUG_UDP_PORT == 514


+ 44
- 30
code/espurna/domoticz.ino View File

@ -48,6 +48,8 @@ void _domoticzStatus(unsigned char id, bool status) {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#include "light.h"
void _domoticzLight(unsigned int idx, const JsonObject& root) { void _domoticzLight(unsigned int idx, const JsonObject& root) {
if (!lightHasColor()) return; if (!lightHasColor()) return;
@ -55,10 +57,34 @@ void _domoticzLight(unsigned int idx, const JsonObject& root) {
JsonObject& color = root["Color"]; JsonObject& color = root["Color"];
if (!color.success()) return; if (!color.success()) return;
// for ColorMode... see:
// https://github.com/domoticz/domoticz/blob/development/hardware/ColorSwitch.h
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Set_a_light_to_a_certain_color_or_color_temperature
DEBUG_MSG_P(PSTR("[DOMOTICZ] Received rgb:%u,%u,%u ww:%u,cw:%u t:%u brightness:%u for IDX %u\n"),
color["r"].as<unsigned char>(),
color["g"].as<unsigned char>(),
color["b"].as<unsigned char>(),
color["ww"].as<unsigned char>(),
color["cw"].as<unsigned char>(),
color["t"].as<unsigned char>(),
color["Level"].as<unsigned char>(),
idx
);
// m field contains information about color mode (enum ColorMode from domoticz ColorSwitch.h): // m field contains information about color mode (enum ColorMode from domoticz ColorSwitch.h):
unsigned int cmode = color["m"]; unsigned int cmode = color["m"];
if (cmode == 3 || cmode == 4) { // ColorModeRGB or ColorModeCustom - see domoticz ColorSwitch.h
if (cmode == 2) { // ColorModeWhite - WW,CW,temperature (t unused for now)
if (lightChannels() < 2) return;
lightChannel(0, color["ww"]);
lightChannel(1, color["cw"]);
} else if (cmode == 3 || cmode == 4) { // ColorModeRGB or ColorModeCustom
if (lightChannels() < 3) return;
lightChannel(0, color["r"]); lightChannel(0, color["r"]);
lightChannel(1, color["g"]); lightChannel(1, color["g"]);
@ -69,34 +95,21 @@ void _domoticzLight(unsigned int idx, const JsonObject& root) {
if (lightChannels() > 3) { if (lightChannels() > 3) {
lightChannel(3, color["ww"]); lightChannel(3, color["ww"]);
} }
if (lightChannels() > 4) { if (lightChannels() > 4) {
lightChannel(4, color["cw"]); lightChannel(4, color["cw"]);
} }
// domoticz uses 100 as maximum value while we're using LIGHT_MAX_BRIGHTNESS
unsigned int brightness = (root["Level"].as<uint8_t>() / 100.0) * LIGHT_MAX_BRIGHTNESS;
lightBrightness(brightness);
DEBUG_MSG_P(PSTR("[DOMOTICZ] Received rgb:%u,%u,%u ww:%u,cw:%u brightness:%u for IDX %u\n"),
color["r"].as<uint8_t>(),
color["g"].as<uint8_t>(),
color["b"].as<uint8_t>(),
color["ww"].as<uint8_t>(),
color["cw"].as<uint8_t>(),
brightness,
idx
);
lightUpdate(true, mqttForward());
} }
// domoticz uses 100 as maximum value while we're using Light::BRIGHTNESS_MAX (unsigned char)
lightBrightness((root["Level"].as<unsigned char>() / 100.0) * Light::BRIGHTNESS_MAX);
lightUpdate(true, mqttForward());
} }
#endif #endif
void _domoticzMqtt(unsigned int type, const char * topic, const char * payload) {
void _domoticzMqtt(unsigned int type, const char * topic, char * payload) {
if (!_dcz_enabled) return; if (!_dcz_enabled) return;
@ -118,8 +131,8 @@ void _domoticzMqtt(unsigned int type, const char * topic, const char * payload)
if (dczTopicOut.equals(topic)) { if (dczTopicOut.equals(topic)) {
// Parse response // Parse response
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char *) payload);
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.parseObject(payload);
if (!root.success()) { if (!root.success()) {
DEBUG_MSG_P(PSTR("[DOMOTICZ] Error parsing data\n")); DEBUG_MSG_P(PSTR("[DOMOTICZ] Error parsing data\n"));
return; return;
@ -166,13 +179,16 @@ void _domoticzBrokerCallback(const unsigned char type, const char * topic, unsig
#if WEB_SUPPORT #if WEB_SUPPORT
bool _domoticzWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _domoticzWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "dcz", 3) == 0); return (strncmp(key, "dcz", 3) == 0);
} }
void _domoticzWebSocketOnSend(JsonObject& root) {
void _domoticzWebSocketOnVisible(JsonObject& root) {
root["dczVisible"] = static_cast<unsigned char>(haveRelaysOrSensors());
}
void _domoticzWebSocketOnConnected(JsonObject& root) {
unsigned char visible = 0;
root["dczEnabled"] = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1; root["dczEnabled"] = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1;
root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC); root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC);
root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC); root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
@ -181,15 +197,11 @@ void _domoticzWebSocketOnSend(JsonObject& root) {
for (unsigned char i=0; i<relayCount(); i++) { for (unsigned char i=0; i<relayCount(); i++) {
relays.add(domoticzIdx(i)); relays.add(domoticzIdx(i));
} }
visible = (relayCount() > 0);
#if SENSOR_SUPPORT #if SENSOR_SUPPORT
_sensorWebSocketMagnitudes(root, "dcz"); _sensorWebSocketMagnitudes(root, "dcz");
visible = visible || (magnitudeCount() > 0);
#endif #endif
root["dczVisible"] = visible;
} }
#endif // WEB_SUPPORT #endif // WEB_SUPPORT
@ -248,8 +260,10 @@ void domoticzSetup() {
_domoticzConfigure(); _domoticzConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_domoticzWebSocketOnSend);
wsOnReceiveRegister(_domoticzWebSocketOnReceive);
wsRegister()
.onVisible(_domoticzWebSocketOnVisible)
.onConnected(_domoticzWebSocketOnConnected)
.onKeyCheck(_domoticzWebSocketOnKeyCheck);
#endif #endif
#if BROKER_SUPPORT #if BROKER_SUPPORT


+ 7
- 0
code/espurna/eeprom.ino View File

@ -23,6 +23,9 @@ void eepromRotate(bool value) {
DEBUG_MSG_P(PSTR("[EEPROM] Disabling EEPROM rotation\n")); DEBUG_MSG_P(PSTR("[EEPROM] Disabling EEPROM rotation\n"));
} }
EEPROMr.rotate(value); EEPROMr.rotate(value);
// Because .rotate(false) marks EEPROM as dirty, this is equivalent to the .backup(0)
eepromCommit();
} }
} }
@ -54,6 +57,10 @@ void eepromCommit() {
_eeprom_commit = true; _eeprom_commit = true;
} }
void eepromBackup(uint32_t index){
EEPROMr.backup(index);
}
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
void _eepromInitCommands() { void _eepromInitCommands() {


+ 8
- 9
code/espurna/encoder.ino View File

@ -8,7 +8,7 @@ Copyright (C) 2018-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) #if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
#include <Encoder.h>
#include "libs/Encoder.h"
#include <vector> #include <vector>
typedef struct { typedef struct {
@ -22,6 +22,7 @@ typedef struct {
} encoder_t; } encoder_t;
std::vector<encoder_t> _encoders; std::vector<encoder_t> _encoders;
unsigned long _encoder_min_delta = 1;
void _encoderConfigure() { void _encoderConfigure() {
@ -85,24 +86,22 @@ void _encoderConfigure() {
} }
} }
_encoder_min_delta = getSetting("encMinDelta", ENCODER_MINIMUM_DELTA).toInt();
if (!_encoder_min_delta) _encoder_min_delta = 1;
} }
void _encoderLoop() { void _encoderLoop() {
// for each encoder
// for each encoder, read delta (read()) and map button action
for (unsigned char i=0; i<_encoders.size(); i++) { for (unsigned char i=0; i<_encoders.size(); i++) {
// get encoder
encoder_t encoder = _encoders[i]; encoder_t encoder = _encoders[i];
// read encoder
long delta = encoder.encoder->read(); long delta = encoder.encoder->read();
encoder.encoder->write(0); encoder.encoder->write(0);
if (0 == delta) continue;
DEBUG_MSG_P(PSTR("[ENCODER] Delta: %d\n"), delta);
if ((0 == delta) || (_encoder_min_delta > abs(delta))) continue;
// action
if (encoder.button_pin == GPIO_NONE) { if (encoder.button_pin == GPIO_NONE) {
// if there is no button, the encoder drives CHANNEL1 // if there is no button, the encoder drives CHANNEL1
@ -110,7 +109,7 @@ void _encoderLoop() {
} else { } else {
// check if button is pressed
// otherwise, use button based on encoder mode
bool pressed = (digitalRead(encoder.button_pin) != encoder.button_logic); bool pressed = (digitalRead(encoder.button_pin) != encoder.button_logic);
if (ENCODER_MODE_CHANNEL == encoder.mode) { if (ENCODER_MODE_CHANNEL == encoder.mode) {


+ 32
- 5
code/espurna/espurna.ino View File

@ -22,9 +22,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "config/all.h" #include "config/all.h"
#include <vector> #include <vector>
#include "libs/HeapStats.h"
std::vector<void (*)()> _loop_callbacks; std::vector<void (*)()> _loop_callbacks;
std::vector<void (*)()> _reload_callbacks; std::vector<void (*)()> _reload_callbacks;
bool _reload_config = false;
unsigned long _loop_delay = 0; unsigned long _loop_delay = 0;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -40,6 +43,10 @@ void espurnaRegisterReload(void (*callback)()) {
} }
void espurnaReload() { void espurnaReload() {
_reload_config = true;
}
void _espurnaReload() {
for (unsigned char i = 0; i < _reload_callbacks.size(); i++) { for (unsigned char i = 0; i < _reload_callbacks.size(); i++) {
(_reload_callbacks[i])(); (_reload_callbacks[i])();
} }
@ -60,7 +67,7 @@ void setup() {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Cache initial free heap value // Cache initial free heap value
getInitialFreeHeap();
setInitialFreeHeap();
// Serial debug // Serial debug
#if DEBUG_SUPPORT #if DEBUG_SUPPORT
@ -76,6 +83,15 @@ void setup() {
// Init persistance // Init persistance
settingsSetup(); settingsSetup();
// Init crash recorder
#if DEBUG_SUPPORT
crashSetup();
#endif
// Return bogus free heap value for broken devices
// XXX: device is likely to trigger other bugs! tread carefuly
wtfHeap(getSetting("wtfHeap", 0).toInt());
// Init Serial, SPIFFS and system check // Init Serial, SPIFFS and system check
systemSetup(); systemSetup();
@ -94,10 +110,15 @@ void setup() {
info(); info();
wifiSetup(); wifiSetup();
otaSetup();
#if OTA_ARDUINOOTA_SUPPORT
arduinoOtaSetup();
#endif
#if TELNET_SUPPORT #if TELNET_SUPPORT
telnetSetup(); telnetSetup();
#endif #endif
#if OTA_CLIENT != OTA_CLIENT_NONE
otaClientSetup();
#endif
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Check if system is stable // Check if system is stable
@ -171,6 +192,9 @@ void setup() {
#if NOFUSS_SUPPORT #if NOFUSS_SUPPORT
nofussSetup(); nofussSetup();
#endif #endif
#if SENSOR_SUPPORT
sensorSetup();
#endif
#if INFLUXDB_SUPPORT #if INFLUXDB_SUPPORT
idbSetup(); idbSetup();
#endif #endif
@ -189,9 +213,6 @@ void setup() {
#if HOMEASSISTANT_SUPPORT #if HOMEASSISTANT_SUPPORT
haSetup(); haSetup();
#endif #endif
#if SENSOR_SUPPORT
sensorSetup();
#endif
#if SCHEDULER_SUPPORT #if SCHEDULER_SUPPORT
schSetup(); schSetup();
#endif #endif
@ -231,6 +252,12 @@ void setup() {
void loop() { void loop() {
// Reload config before running any callbacks
if (_reload_config) {
_espurnaReload();
_reload_config = false;
}
// Call registered loop callbacks // Call registered loop callbacks
for (unsigned char i = 0; i < _loop_callbacks.size(); i++) { for (unsigned char i = 0; i < _loop_callbacks.size(); i++) {
(_loop_callbacks[i])(); (_loop_callbacks[i])();


+ 198
- 154
code/espurna/homeassistant.ino View File

@ -9,7 +9,6 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if HOMEASSISTANT_SUPPORT #if HOMEASSISTANT_SUPPORT
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <queue>
bool _haEnabled = false; bool _haEnabled = false;
bool _haSendFlag = false; bool _haSendFlag = false;
@ -18,13 +17,74 @@ bool _haSendFlag = false;
// UTILS // UTILS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
String _haFixName(String name) {
// per yaml 1.1 spec, following scalars are converted to bool. we want the string, so quoting the output
// y|Y|yes|Yes|YES|n|N|no|No|NO |true|True|TRUE|false|False|FALSE |on|On|ON|off|Off|OFF
String _haFixPayload(const String& value) {
if (value.equalsIgnoreCase("y")
|| value.equalsIgnoreCase("n")
|| value.equalsIgnoreCase("yes")
|| value.equalsIgnoreCase("no")
|| value.equalsIgnoreCase("true")
|| value.equalsIgnoreCase("false")
|| value.equalsIgnoreCase("on")
|| value.equalsIgnoreCase("off")
) {
String temp;
temp.reserve(value.length() + 2);
temp = "\"";
temp += value;
temp += "\"";
return temp;
}
return value;
}
String& _haFixName(String& name) {
for (unsigned char i=0; i<name.length(); i++) { for (unsigned char i=0; i<name.length(); i++) {
if (!isalnum(name.charAt(i))) name.setCharAt(i, '_'); if (!isalnum(name.charAt(i))) name.setCharAt(i, '_');
} }
return name; return name;
} }
#if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER))
const String switchType("light");
#else
const String switchType("switch");
#endif
struct ha_config_t {
static const size_t DEFAULT_BUFFER_SIZE = 2048;
ha_config_t(size_t size) :
jsonBuffer(size),
deviceConfig(jsonBuffer.createObject()),
root(jsonBuffer.createObject()),
identifier(getIdentifier()),
name(getSetting("desc", getSetting("hostname"))),
version(String(APP_NAME " " APP_VERSION " (") + getCoreVersion() + ")")
{
deviceConfig.createNestedArray("identifiers").add(identifier.c_str());
deviceConfig["name"] = name.c_str();
deviceConfig["sw_version"] = version.c_str();
deviceConfig["manufacturer"] = MANUFACTURER;
deviceConfig["model"] = DEVICE;
}
ha_config_t() : ha_config_t(DEFAULT_BUFFER_SIZE) {}
size_t size() { return jsonBuffer.size(); }
DynamicJsonBuffer jsonBuffer;
JsonObject& deviceConfig;
JsonObject& root;
const String identifier;
const String name;
const String version;
};
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// SENSORS // SENSORS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -35,12 +95,11 @@ void _haSendMagnitude(unsigned char i, JsonObject& config) {
unsigned char type = magnitudeType(i); unsigned char type = magnitudeType(i);
config["name"] = _haFixName(getSetting("hostname") + String(" ") + magnitudeTopic(type)); config["name"] = _haFixName(getSetting("hostname") + String(" ") + magnitudeTopic(type));
config.set("platform", "mqtt");
config["state_topic"] = mqttTopic(magnitudeTopicIndex(i).c_str(), false); config["state_topic"] = mqttTopic(magnitudeTopicIndex(i).c_str(), false);
config["unit_of_measurement"] = magnitudeUnits(type); config["unit_of_measurement"] = magnitudeUnits(type);
} }
void _haSendMagnitudes(const JsonObject& deviceConfig) {
void _haSendMagnitudes(ha_config_t& config) {
for (unsigned char i=0; i<magnitudeCount(); i++) { for (unsigned char i=0; i<magnitudeCount(); i++) {
@ -51,21 +110,20 @@ void _haSendMagnitudes(const JsonObject& deviceConfig) {
String output; String output;
if (_haEnabled) { if (_haEnabled) {
DynamicJsonBuffer jsonBuffer;
JsonObject& config = jsonBuffer.createObject();
_haSendMagnitude(i, config);
config["uniq_id"] = getIdentifier() + "_" + magnitudeTopic(magnitudeType(i)) + "_" + String(i);
config["device"] = deviceConfig;
_haSendMagnitude(i, config.root);
config.root["uniq_id"] = getIdentifier() + "_" + magnitudeTopic(magnitudeType(i)) + "_" + String(i);
config.root["device"] = config.deviceConfig;
config.printTo(output);
jsonBuffer.clear();
output.reserve(config.root.measureLength());
config.root.printTo(output);
} }
mqttSendRaw(topic.c_str(), output.c_str()); mqttSendRaw(topic.c_str(), output.c_str());
mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true);
} }
mqttSendStatus();
} }
#endif // SENSOR_SUPPORT #endif // SENSOR_SUPPORT
@ -82,16 +140,15 @@ void _haSendSwitch(unsigned char i, JsonObject& config) {
} }
config.set("name", _haFixName(name)); config.set("name", _haFixName(name));
config.set("platform", "mqtt");
if (relayCount()) { if (relayCount()) {
config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false); config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false);
config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true); config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true);
config["payload_on"] = String(HOMEASSISTANT_PAYLOAD_ON);
config["payload_off"] = String(HOMEASSISTANT_PAYLOAD_OFF);
config["payload_on"] = relayPayload(RelayStatus::ON);
config["payload_off"] = relayPayload(RelayStatus::OFF);
config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false); config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false);
config["payload_available"] = String(HOMEASSISTANT_PAYLOAD_AVAILABLE);
config["payload_not_available"] = String(HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE);
config["payload_available"] = mqttPayloadStatus(true);
config["payload_not_available"] = mqttPayloadStatus(false);
} }
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
@ -104,7 +161,10 @@ void _haSendSwitch(unsigned char i, JsonObject& config) {
if (lightHasColor()) { if (lightHasColor()) {
config["rgb_state_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, false); config["rgb_state_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, false);
config["rgb_command_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, true); config["rgb_command_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, true);
}
if (lightUseCCT()) {
config["color_temp_command_topic"] = mqttTopic(MQTT_TOPIC_MIRED, true); config["color_temp_command_topic"] = mqttTopic(MQTT_TOPIC_MIRED, true);
config["color_temp_state_topic"] = mqttTopic(MQTT_TOPIC_MIRED, false);
} }
if (lightChannels() > 3) { if (lightChannels() > 3) {
@ -118,145 +178,113 @@ void _haSendSwitch(unsigned char i, JsonObject& config) {
} }
void _haSendSwitches(const JsonObject& deviceConfig) {
#if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER))
String type = String("light");
#else
String type = String("switch");
#endif
void _haSendSwitches(ha_config_t& config) {
for (unsigned char i=0; i<relayCount(); i++) { for (unsigned char i=0; i<relayCount(); i++) {
String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) + String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) +
"/" + type +
"/" + switchType +
"/" + getSetting("hostname") + "_" + String(i) + "/" + getSetting("hostname") + "_" + String(i) +
"/config"; "/config";
String output; String output;
if (_haEnabled) { if (_haEnabled) {
DynamicJsonBuffer jsonBuffer;
JsonObject& config = jsonBuffer.createObject();
_haSendSwitch(i, config);
config["uniq_id"] = getIdentifier() + "_" + type + "_" + String(i);
config["device"] = deviceConfig;
config.printTo(output);
jsonBuffer.clear();
_haSendSwitch(i, config.root);
config.root["uniq_id"] = getIdentifier() + "_" + switchType + "_" + String(i);
config.root["device"] = config.deviceConfig;
output.reserve(config.root.measureLength());
config.root.printTo(output);
} }
mqttSendRaw(topic.c_str(), output.c_str()); mqttSendRaw(topic.c_str(), output.c_str());
mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true);
} }
mqttSendStatus();
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void _haDumpConfig(std::function<void(String&)> printer, bool wrapJson = false) {
constexpr const size_t HA_YAML_BUFFER_SIZE = 1024;
#if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER))
String type = String("light");
#else
String type = String("switch");
#endif
void _haSwitchYaml(unsigned char index, JsonObject& root) {
for (unsigned char i=0; i<relayCount(); i++) {
String output;
output.reserve(HA_YAML_BUFFER_SIZE);
DynamicJsonBuffer jsonBuffer;
JsonObject& config = jsonBuffer.createObject();
_haSendSwitch(i, config);
JsonObject& config = root.createNestedObject("config");
_haSendSwitch(index, config);
String output;
output.reserve(config.measureLength() + 32);
if (index == 0) output += "\n\n" + switchType + ":";
output += "\n";
bool first = true;
if (wrapJson) {
output += "{\"haConfig\": \"";
for (auto kv : config) {
if (first) {
output += " - ";
first = false;
} else {
output += " ";
} }
output += "\n\n" + type + ":\n";
bool first = true;
for (auto kv : config) {
if (first) {
output += " - ";
first = false;
} else {
output += " ";
}
output += kv.key;
output += ": ";
output += kv.key;
output += ": ";
if (strncmp(kv.key, "payload_", strlen("payload_")) == 0) {
output += _haFixPayload(kv.value.as<String>());
} else {
output += kv.value.as<String>(); output += kv.value.as<String>();
output += "\n";
}
output += " ";
if (wrapJson) {
output += "\"}";
} }
jsonBuffer.clear();
printer(output);
output += "\n";
} }
output += " ";
#if SENSOR_SUPPORT
for (unsigned char i=0; i<magnitudeCount(); i++) {
DynamicJsonBuffer jsonBuffer;
JsonObject& config = jsonBuffer.createObject();
_haSendMagnitude(i, config);
String output;
output.reserve(config.measureLength() + 32);
root.remove("config");
root["haConfig"] = output;
}
if (wrapJson) {
output += "{\"haConfig\": \"";
}
#if SENSOR_SUPPORT
output += "\n\nsensor:\n";
bool first = true;
for (auto kv : config) {
if (first) {
output += " - ";
first = false;
} else {
output += " ";
}
String value = kv.value.as<String>();
value.replace("%", "'%'");
output += kv.key;
output += ": ";
output += value;
output += "\n";
}
output += " ";
void _haSensorYaml(unsigned char index, JsonObject& root) {
if (wrapJson) {
output += "\"}";
}
String output;
output.reserve(HA_YAML_BUFFER_SIZE);
jsonBuffer.clear();
JsonObject& config = root.createNestedObject("config");
_haSendMagnitude(index, config);
printer(output);
if (index == 0) output += "\n\nsensor:";
output += "\n";
bool first = true;
for (auto kv : config) {
if (first) {
output += " - ";
first = false;
} else {
output += " ";
} }
String value = kv.value.as<String>();
value.replace("%", "'%'");
output += kv.key;
output += ": ";
output += value;
output += "\n";
}
output += " ";
root.remove("config");
root["haConfig"] = output;
#endif
} }
#endif // SENSOR_SUPPORT
void _haGetDeviceConfig(JsonObject& config) { void _haGetDeviceConfig(JsonObject& config) {
String identifier = getIdentifier();
config.createNestedArray("identifiers").add(identifier);
config.createNestedArray("identifiers").add(getIdentifier());
config["name"] = getSetting("desc", getSetting("hostname")); config["name"] = getSetting("desc", getSetting("hostname"));
config["manufacturer"] = String(MANUFACTURER);
config["model"] = String(DEVICE);
config["sw_version"] = String(APP_NAME) + " " + String(APP_VERSION) + " (" + getCoreVersion() + ")";
config["manufacturer"] = MANUFACTURER;
config["model"] = DEVICE;
config["sw_version"] = String(APP_NAME) + " " + APP_VERSION + " (" + getCoreVersion() + ")";
} }
void _haSend() { void _haSend() {
@ -270,17 +298,14 @@ void _haSend() {
DEBUG_MSG_P(PSTR("[HA] Sending autodiscovery MQTT message\n")); DEBUG_MSG_P(PSTR("[HA] Sending autodiscovery MQTT message\n"));
// Get common device config // Get common device config
DynamicJsonBuffer jsonBuffer;
JsonObject& deviceConfig = jsonBuffer.createObject();
_haGetDeviceConfig(deviceConfig);
ha_config_t config;
// Send messages // Send messages
_haSendSwitches(deviceConfig);
_haSendSwitches(config);
#if SENSOR_SUPPORT #if SENSOR_SUPPORT
_haSendMagnitudes(deviceConfig);
_haSendMagnitudes(config);
#endif #endif
jsonBuffer.clear();
_haSendFlag = false; _haSendFlag = false;
} }
@ -294,34 +319,68 @@ void _haConfigure() {
#if WEB_SUPPORT #if WEB_SUPPORT
std::queue<uint32_t> _ha_send_config;
bool _haWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _haWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "ha", 2) == 0); return (strncmp(key, "ha", 2) == 0);
} }
void _haWebSocketOnSend(JsonObject& root) {
void _haWebSocketOnVisible(JsonObject& root) {
root["haVisible"] = 1; root["haVisible"] = 1;
}
void _haWebSocketOnConnected(JsonObject& root) {
root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX); root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
root["haEnabled"] = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1; root["haEnabled"] = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1;
} }
void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (strcmp(action, "haconfig") == 0) { if (strcmp(action, "haconfig") == 0) {
_ha_send_config.push(client_id);
ws_on_send_callback_list_t callbacks;
#if SENSOR_SUPPORT
callbacks.reserve(magnitudeCount() + relayCount());
#else
callbacks.reserve(relayCount());
#endif // SENSOR_SUPPORT
{
for (unsigned char idx=0; idx<relayCount(); ++idx) {
callbacks.push_back([idx](JsonObject& root) {
_haSwitchYaml(idx, root);
});
}
}
#if SENSOR_SUPPORT
{
for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
callbacks.push_back([idx](JsonObject& root) {
_haSensorYaml(idx, root);
});
}
}
#endif // SENSOR_SUPPORT
if (callbacks.size()) wsPostSequence(client_id, std::move(callbacks));
} }
} }
#endif
#endif // WEB_SUPPORT
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
void _haInitCommands() { void _haInitCommands() {
terminalRegisterCommand(F("HA.CONFIG"), [](Embedis* e) { terminalRegisterCommand(F("HA.CONFIG"), [](Embedis* e) {
_haDumpConfig([](String& data) {
DEBUG_MSG(data.c_str());
});
for (unsigned char idx=0; idx<relayCount(); ++idx) {
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject();
_haSwitchYaml(idx, root);
DEBUG_MSG(root["haConfig"].as<String>().c_str());
}
#if SENSOR_SUPPORT
for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject();
_haSensorYaml(idx, root);
DEBUG_MSG(root["haConfig"].as<String>().c_str());
}
#endif // SENSOR_SUPPORT
DEBUG_MSG("\n"); DEBUG_MSG("\n");
terminalOK(); terminalOK();
}); });
@ -330,7 +389,7 @@ void _haInitCommands() {
setSetting("haEnabled", "1"); setSetting("haEnabled", "1");
_haConfigure(); _haConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend(_haWebSocketOnSend);
wsPost(_haWebSocketOnConnected);
#endif #endif
terminalOK(); terminalOK();
}); });
@ -339,7 +398,7 @@ void _haInitCommands() {
setSetting("haEnabled", "0"); setSetting("haEnabled", "0");
_haConfigure(); _haConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend(_haWebSocketOnSend);
wsPost(_haWebSocketOnConnected);
#endif #endif
terminalOK(); terminalOK();
}); });
@ -350,32 +409,16 @@ void _haInitCommands() {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if WEB_SUPPORT
void _haLoop() {
if (_ha_send_config.empty()) return;
uint32_t client_id = _ha_send_config.front();
_ha_send_config.pop();
if (!wsConnected(client_id)) return;
// TODO check wsConnected after each "printer" call?
_haDumpConfig([client_id](String& output) {
wsSend(client_id, output.c_str());
yield();
}, true);
}
#endif
void haSetup() { void haSetup() {
_haConfigure(); _haConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_haWebSocketOnSend);
wsOnActionRegister(_haWebSocketOnAction);
wsOnReceiveRegister(_haWebSocketOnReceive);
espurnaRegisterLoop(_haLoop);
wsRegister()
.onVisible(_haWebSocketOnVisible)
.onConnected(_haWebSocketOnConnected)
.onAction(_haWebSocketOnAction)
.onKeyCheck(_haWebSocketOnKeyCheck);
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
@ -385,6 +428,7 @@ void haSetup() {
// On MQTT connect check if we have something to send // On MQTT connect check if we have something to send
mqttRegister([](unsigned int type, const char * topic, const char * payload) { mqttRegister([](unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) _haSend(); if (type == MQTT_CONNECT_EVENT) _haSend();
if (type == MQTT_DISCONNECT_EVENT) _haSendFlag = false;
}); });
// Main callbacks // Main callbacks


+ 9
- 4
code/espurna/influxdb.ino View File

@ -17,12 +17,15 @@ SyncClientWrap * _idb_client;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
bool _idbWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "idb", 3) == 0); return (strncmp(key, "idb", 3) == 0);
} }
void _idbWebSocketOnSend(JsonObject& root) {
void _idbWebSocketOnVisible(JsonObject& root) {
root["idbVisible"] = 1; root["idbVisible"] = 1;
}
void _idbWebSocketOnConnected(JsonObject& root) {
root["idbEnabled"] = getSetting("idbEnabled", INFLUXDB_ENABLED).toInt() == 1; root["idbEnabled"] = getSetting("idbEnabled", INFLUXDB_ENABLED).toInt() == 1;
root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST); root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST);
root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT).toInt(); root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT).toInt();
@ -118,8 +121,10 @@ void idbSetup() {
_idbConfigure(); _idbConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_idbWebSocketOnSend);
wsOnReceiveRegister(_idbWebSocketOnReceive);
wsRegister()
.onVisible(_idbWebSocketOnVisible)
.onConnected(_idbWebSocketOnConnected)
.onKeyCheck(_idbWebSocketOnKeyCheck);
#endif #endif
#if BROKER_SUPPORT #if BROKER_SUPPORT


+ 2
- 1
code/espurna/ir.ino View File

@ -303,7 +303,8 @@ void _irProcess(unsigned char type, unsigned long code) {
} }
if (button_mode == IR_BUTTON_MODE_RGB) { if (button_mode == IR_BUTTON_MODE_RGB) {
lightColor(button_value);
lightColor((button_value >> 8) & 0xffffff);
lightBrightness(button_value & 0xff);
} }
/* /*


+ 13
- 8
code/espurna/led.ino View File

@ -71,13 +71,16 @@ void _ledBlink(unsigned char id, unsigned long delayOff, unsigned long delayOn)
#if WEB_SUPPORT #if WEB_SUPPORT
bool _ledWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _ledWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "led", 3) == 0); return (strncmp(key, "led", 3) == 0);
} }
void _ledWebSocketOnSend(JsonObject& root) {
if (_ledCount() == 0) return;
void _ledWebSocketOnVisible(JsonObject& root) {
root["ledVisible"] = 1; root["ledVisible"] = 1;
}
void _ledWebSocketOnConnected(JsonObject& root) {
if (_ledCount() == 0) return;
JsonArray& leds = root.createNestedArray("ledConfig"); JsonArray& leds = root.createNestedArray("ledConfig");
for (byte i=0; i<_ledCount(); i++) { for (byte i=0; i<_ledCount(); i++) {
JsonObject& led = leds.createNestedObject(); JsonObject& led = leds.createNestedObject();
@ -127,13 +130,13 @@ void _ledMQTTCallback(unsigned int type, const char * topic, const char * payloa
if (_ledMode(ledID) != LED_MODE_MQTT) return; if (_ledMode(ledID) != LED_MODE_MQTT) return;
// get value // get value
unsigned char value = relayParsePayload(payload);
const auto value = relayParsePayload(payload);
// Action to perform // Action to perform
if (value == 2) {
if (value == RelayStatus::TOGGLE) {
_ledToggle(ledID); _ledToggle(ledID);
} else { } else {
_ledStatus(ledID, value == 1);
_ledStatus(ledID, (value == RelayStatus::ON));
} }
} }
@ -200,8 +203,10 @@ void ledSetup() {
#endif #endif
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_ledWebSocketOnSend);
wsOnReceiveRegister(_ledWebSocketOnReceive);
wsRegister()
.onVisible(_ledWebSocketOnVisible)
.onConnected(_ledWebSocketOnConnected)
.onKeyCheck(_ledWebSocketOnKeyCheck);
#endif #endif
#if BROKER_SUPPORT #if BROKER_SUPPORT


+ 47
- 0
code/espurna/libs/DebugSend.h View File

@ -0,0 +1,47 @@
// -----------------------------------------------------------------------------
// printf-like debug methods
// -----------------------------------------------------------------------------
#pragma once
void debugSendImpl(const char*);
void _debugSend(const char * format, va_list args) {
char temp[64];
int len = ets_vsnprintf(temp, sizeof(temp), format, args);
if (len < 64) { debugSendImpl(temp); return; }
auto buffer = new char[len + 1];
ets_vsnprintf(buffer, len + 1, format, args);
debugSendImpl(buffer);
delete[] buffer;
}
void debugSend(const char* format, ...) {
va_list args;
va_start(args, format);
_debugSend(format, args);
va_end(args);
}
void debugSend_P(PGM_P format_P, ...) {
char format[strlen_P(format_P) + 1];
memcpy_P(format, format_P, sizeof(format));
va_list args;
va_start(args, format_P);
_debugSend(format, args);
va_end(args);
}

+ 228
- 0
code/espurna/libs/Encoder.h View File

@ -0,0 +1,228 @@
/* ---------------------------- Original copyright -----------------------------
*
* Encoder Library, for measuring quadrature encoded signals
* http://www.pjrc.com/teensy/td_libs_Encoder.html
* Copyright (c) 2011,2013 PJRC.COM, LLC - Paul Stoffregen <paul@pjrc.com>
*
* Version 1.2 - fix -2 bug in C-only code
* Version 1.1 - expand to support boards with up to 60 interrupts
* Version 1.0 - initial release
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* -----------------------------------------------------------------------------
*
* Encoder.h, updated for ESP8266 use in ESPurna. Other hardware is not supported.
*
* - Added ESP-specific attributes to ISR handlers to place them in IRAM.
* - Reduced per-encoder structure sizes - only 5 Encoders can be used on ESP8266,
* and we can directly reference pin number instead of storing both register and bitmask
*
*/
#pragma once
// _______ _______
// Pin1 ______| |_______| |______ Pin1
// negative <--- _______ _______ __ --> positive
// Pin2 __| |_______| |_______| Pin2
// new new old old
// pin2 pin1 pin2 pin1 Result
// ---- ---- ---- ---- ------
// 0 0 0 0 no movement
// 0 0 0 1 +1
// 0 0 1 0 -1
// 0 0 1 1 +2 (assume pin1 edges only)
// 0 1 0 0 -1
// 0 1 0 1 no movement
// 0 1 1 0 -2 (assume pin1 edges only)
// 0 1 1 1 +1
// 1 0 0 0 +1
// 1 0 0 1 -2 (assume pin1 edges only)
// 1 0 1 0 no movement
// 1 0 1 1 -1
// 1 1 0 0 +2 (assume pin1 edges only)
// 1 1 0 1 -1
// 1 1 1 0 +1
// 1 1 1 1 no movement
namespace EncoderLibrary {
typedef struct {
uint8_t pin1;
uint8_t pin2;
uint8_t state;
int32_t position;
} encoder_values_t;
constexpr const unsigned char ENCODERS_MAXIMUM {5u};
encoder_values_t * EncoderValues[ENCODERS_MAXIMUM] = {nullptr};
uint8_t _encoderFindStorage() {
for (uint8_t i = 0; i < ENCODERS_MAXIMUM; i++) {
if (EncoderValues[i] == nullptr) {
return i;
}
}
return ENCODERS_MAXIMUM;
}
void _encoderCleanStorage(uint8_t pin1, uint8_t pin2) {
for (uint8_t i = 0; i < ENCODERS_MAXIMUM; i++) {
if (EncoderValues[i] == nullptr) continue;
if (((EncoderValues[i])->pin1 == pin1) && ((EncoderValues[i])->pin2 == pin2)) {
EncoderValues[i] = nullptr;
break;
}
}
}
// update() is not meant to be called from outside Encoder,
// but it is public to allow static interrupt routines.
void ICACHE_RAM_ATTR update(encoder_values_t *target) {
uint8_t p1val = GPIP(target->pin1);
uint8_t p2val = GPIP(target->pin2);
uint8_t state = target->state & 3;
if (p1val) state |= 4;
if (p2val) state |= 8;
target->state = (state >> 2);
switch (state) {
case 1: case 7: case 8: case 14:
target->position++;
return;
case 2: case 4: case 11: case 13:
target->position--;
return;
case 3: case 12:
target->position += 2;
return;
case 6: case 9:
target->position -= 2;
return;
}
}
// 2 pins per encoder, 1 isr per encoder
void ICACHE_RAM_ATTR isr0() { update(EncoderValues[0]); }
void ICACHE_RAM_ATTR isr1() { update(EncoderValues[1]); }
void ICACHE_RAM_ATTR isr2() { update(EncoderValues[2]); }
void ICACHE_RAM_ATTR isr3() { update(EncoderValues[3]); }
void ICACHE_RAM_ATTR isr4() { update(EncoderValues[4]); }
constexpr void (*_isr_funcs[5])() = {
isr0, isr1, isr2, isr3, isr4
};
class Encoder {
private:
encoder_values_t values;
public:
Encoder(uint8_t pin1, uint8_t pin2) {
values.pin1 = pin1;
values.pin2 = pin2;
pinMode(values.pin1, INPUT_PULLUP);
pinMode(values.pin2, INPUT_PULLUP);
values.position = 0;
// allow time for a passive R-C filter to charge
// through the pullup resistors, before reading
// the initial state
delayMicroseconds(2000);
uint8_t current = 0;
if (GPIP(values.pin1)) {
current |= 1;
}
if (GPIP(values.pin2)) {
current |= 2;
}
values.state = current;
attach();
}
~Encoder() {
detach();
}
uint8_t pin1() {
return values.pin1;
}
uint8_t pin2() {
return values.pin2;
}
int32_t read() {
noInterrupts();
update(&values);
int32_t ret = values.position;
interrupts();
return ret;
}
void write(int32_t position) {
noInterrupts();
values.position = position;
interrupts();
}
bool attach() {
uint8_t index = _encoderFindStorage();
if (index >= ENCODERS_MAXIMUM) return false;
EncoderValues[index] = &values;
attachInterrupt(values.pin1, _isr_funcs[index], CHANGE);
attachInterrupt(values.pin2, _isr_funcs[index], CHANGE);
return true;
}
void detach() {
noInterrupts();
_encoderCleanStorage(values.pin1, values.pin2);
detachInterrupt(values.pin1);
detachInterrupt(values.pin2);
interrupts();
}
};
}
using EncoderLibrary::Encoder;

+ 112
- 0
code/espurna/libs/HeapStats.h View File

@ -0,0 +1,112 @@
/*
Show extended heap stats when EspClass::getHeapStats() is available
*/
#pragma once
#include <type_traits>
struct heap_stats_t {
uint32_t available;
uint16_t usable;
uint8_t frag_pct;
};
namespace EspClass_has_getHeapStats {
struct _detector {
template<typename T, typename = decltype(
std::declval<T>().getHeapStats(0,0,0))>
static std::true_type detect(int);
template<typename>
static std::false_type detect(...);
};
template <typename T>
struct detector : public _detector {
using result = decltype(
std::declval<detector>().template detect<T>(0));
};
template <typename T>
struct typed_check : public detector<T>::result {
};
typed_check<EspClass> check{};
};
template <typename T>
void _getHeapStats(std::true_type&, T& instance, heap_stats_t& stats) {
instance.getHeapStats(&stats.available, &stats.usable, &stats.frag_pct);
}
template <typename T>
void _getHeapStats(std::false_type&, T& instance, heap_stats_t& stats) {
stats.available = instance.getFreeHeap();
stats.usable = 0;
stats.frag_pct = 0;
}
void getHeapStats(heap_stats_t& stats) {
_getHeapStats(EspClass_has_getHeapStats::check, ESP, stats);
}
// WTF
// Calling ESP.getFreeHeap() is making the system crash on a specific
// AiLight bulb, but anywhere else it should work as expected
static bool _heap_value_wtf = false;
heap_stats_t getHeapStats() {
heap_stats_t stats;
if (_heap_value_wtf) {
stats.available = 9999;
stats.usable = 9999;
stats.frag_pct = 0;
return stats;
}
getHeapStats(stats);
return stats;
}
void wtfHeap(bool value) {
_heap_value_wtf = value;
}
unsigned int getFreeHeap() {
return ESP.getFreeHeap();
}
static unsigned int _initial_heap_value = 0;
void setInitialFreeHeap() {
_initial_heap_value = getFreeHeap();
}
unsigned int getInitialFreeHeap() {
if (0 == _initial_heap_value) {
setInitialFreeHeap();
}
return _initial_heap_value;
}
void infoMemory(const char* name, const heap_stats_t& stats) {
infoMemory(name, getInitialFreeHeap(), stats.available);
}
void infoHeapStats(const char* name, const heap_stats_t& stats) {
DEBUG_MSG_P(
PSTR("[MAIN] %-6s: %5u contiguous bytes available (%u%% fragmentation)\n"),
name,
stats.usable,
stats.frag_pct
);
}
void infoHeapStats(bool show_frag_stats = true) {
const auto stats = getHeapStats();
infoMemory("Heap", stats);
if (show_frag_stats && EspClass_has_getHeapStats::check) {
infoHeapStats("Heap", stats);
}
}

+ 247
- 0
code/espurna/libs/SecureClientHelpers.h View File

@ -0,0 +1,247 @@
// -----------------------------------------------------------------------------
// WiFiClientSecure validation helpers
// -----------------------------------------------------------------------------
#pragma once
#if SECURE_CLIENT != SECURE_CLIENT_NONE
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
#include <WiFiClientSecureBearSSL.h>
#elif SECURE_CLIENT == SECURE_CLIENT_AXTLS
#include <WiFiClientSecureAxTLS.h>
#endif
namespace SecureClientHelpers {
using host_callback_f = std::function<String()>;
using check_callback_f = std::function<int()>;
using fp_callback_f = std::function<String()>;
using cert_callback_f = std::function<const char*()>;
using mfln_callback_f = std::function<uint16_t()>;
const char * _secureClientCheckAsString(int check) {
switch (check) {
case SECURE_CLIENT_CHECK_NONE: return "no validation";
case SECURE_CLIENT_CHECK_FINGERPRINT: return "fingerprint validation";
case SECURE_CLIENT_CHECK_CA: return "CA validation";
default: return "unknown";
}
}
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
using SecureClientClass = axTLS::WiFiClientSecure;
struct SecureClientConfig {
SecureClientConfig(const char* tag, host_callback_f host_cb, check_callback_f check_cb, fp_callback_f fp_cb, bool debug = false) :
tag(tag),
on_host(host_cb),
on_check(check_cb),
on_fingerprint(fp_cb),
debug(debug)
{}
String tag;
host_callback_f on_host;
check_callback_f on_check;
fp_callback_f on_fingerprint;
bool debug;
};
struct SecureClientChecks {
SecureClientChecks(SecureClientConfig& config) :
config(config)
{}
int getCheck() {
return (config.on_check) ? config.on_check() : (SECURE_CLIENT_CHECK);
}
bool beforeConnected(SecureClientClass& client) {
return true;
}
// Special condition for legacy client!
// Otherwise, we are required to connect twice. And it is deemed broken & deprecated anyways...
bool afterConnected(SecureClientClass& client) {
bool result = false;
int check = getCheck();
if(config.debug) {
DEBUG_MSG_P(PSTR("[%s] Using SSL check type: %s\n"), config.tag.c_str(), _secureClientCheckAsString(check));
}
if (check == SECURE_CLIENT_CHECK_NONE) {
if (config.debug) DEBUG_MSG_P(PSTR("[%s] !!! Secure connection will not be validated !!!\n"), config.tag.c_str());
result = true;
} else if (check == SECURE_CLIENT_CHECK_FINGERPRINT) {
if (config.on_fingerprint) {
char _buffer[60] = {0};
if (config.on_fingerprint && config.on_host && sslFingerPrintChar(config.on_fingerprint().c_str(), _buffer)) {
result = client.verify(_buffer, config.on_host().c_str());
}
if (!result) DEBUG_MSG_P(PSTR("[%s] Wrong fingerprint, cannot connect\n"), config.tag.c_str());
}
} else if (check == SECURE_CLIENT_CHECK_CA) {
if (config.debug) DEBUG_MSG_P(PSTR("[%s] CA verification is not supported with axTLS client\n"), config.tag.c_str());
}
return result;
}
SecureClientConfig& config;
bool debug;
};
#endif // SECURE_CLIENT_AXTLS
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
using SecureClientClass = BearSSL::WiFiClientSecure;
struct SecureClientConfig {
SecureClientConfig(const char* tag, check_callback_f check_cb, cert_callback_f cert_cb, fp_callback_f fp_cb, mfln_callback_f mfln_cb, bool debug = false) :
tag(tag),
on_check(check_cb),
on_certificate(cert_cb),
on_fingerprint(fp_cb),
on_mfln(mfln_cb),
debug(debug)
{}
String tag;
check_callback_f on_check;
cert_callback_f on_certificate;
fp_callback_f on_fingerprint;
mfln_callback_f on_mfln;
bool debug;
};
struct SecureClientChecks {
SecureClientChecks(SecureClientConfig& config) :
config(config)
{}
int getCheck() {
return (config.on_check) ? config.on_check() : (SECURE_CLIENT_CHECK);
}
bool prepareMFLN(SecureClientClass& client) {
const uint16_t requested_mfln = (config.on_mfln) ? config.on_mfln() : (SECURE_CLIENT_MFLN);
bool result = false;
switch (requested_mfln) {
// default, do nothing
case 0:
result = true;
break;
// match valid sizes only
case 512:
case 1024:
case 2048:
case 4096:
{
client.setBufferSizes(requested_mfln, requested_mfln);
result = true;
if (config.debug) {
DEBUG_MSG_P(PSTR("[%s] MFLN buffer size set to %u\n"), config.tag.c_str(), requested_mfln);
}
break;
}
default:
{
if (config.debug) {
DEBUG_MSG_P(PSTR("[%s] Warning: MFLN buffer size must be one of 512, 1024, 2048 or 4096\n"), config.tag.c_str());
}
}
}
return result;
}
bool beforeConnected(SecureClientClass& client) {
int check = getCheck();
bool settime = (check == SECURE_CLIENT_CHECK_CA);
if(config.debug) {
DEBUG_MSG_P(PSTR("[%s] Using SSL check type: %s\n"), config.tag.c_str(), _secureClientCheckAsString(check));
}
if (!ntpSynced() && settime) {
if (config.debug) DEBUG_MSG_P(PSTR("[%s] Time not synced! Cannot use CA validation\n"), config.tag.c_str());
return false;
}
prepareMFLN(client);
if (check == SECURE_CLIENT_CHECK_NONE) {
if (config.debug) DEBUG_MSG_P(PSTR("[%s] !!! Secure connection will not be validated !!!\n"), config.tag.c_str());
client.setInsecure();
} else if (check == SECURE_CLIENT_CHECK_FINGERPRINT) {
uint8_t _buffer[20] = {0};
if (config.on_fingerprint && sslFingerPrintArray(config.on_fingerprint().c_str(), _buffer)) {
client.setFingerprint(_buffer);
}
} else if (check == SECURE_CLIENT_CHECK_CA) {
client.setX509Time(ntpLocal2UTC(now()));
if (!certs.getCount()) {
if (config.on_certificate) certs.append(config.on_certificate());
}
client.setTrustAnchors(&certs);
}
return true;
}
bool afterConnected(SecureClientClass&) {
return true;
}
bool debug;
SecureClientConfig& config;
BearSSL::X509List certs;
};
#endif // SECURE_CLIENT_BEARSSL
class SecureClient {
public:
SecureClient(SecureClientConfig& config) :
_config(config),
_checks(_config),
_client(std::make_unique<SecureClientClass>())
{}
bool afterConnected() {
return _checks.afterConnected(get());
}
bool beforeConnected() {
return _checks.beforeConnected(get());
}
SecureClientClass& get() {
return *_client.get();
}
private:
SecureClientConfig _config;
SecureClientChecks _checks;
std::unique_ptr<SecureClientClass> _client;
};
};
using SecureClientConfig = SecureClientHelpers::SecureClientConfig;
using SecureClientChecks = SecureClientHelpers::SecureClientChecks;
using SecureClient = SecureClientHelpers::SecureClient;
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE

+ 68
- 0
code/espurna/libs/URL.h View File

@ -0,0 +1,68 @@
// -----------------------------------------------------------------------------
// Parse char string as URL
//
// Adapted from HTTPClient::beginInternal()
// https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp
//
// -----------------------------------------------------------------------------
#pragma once
struct URL {
String value;
String protocol;
String host;
String path;
uint16_t port;
URL(const char* url) { init(url); }
URL(const String& url) { init(url); }
void init(String url);
};
void URL::init(String url) {
this->value = url;
// cut the protocol part
int index = url.indexOf("://");
if (index > 0) {
this->protocol = url.substring(0, index);
url.remove(0, (index + 3));
}
if (this->protocol == "http") {
this->port = 80;
} else if (this->protocol == "https") {
this->port = 443;
}
// cut the host part
String _host;
index = url.indexOf('/');
if (index >= 0) {
_host = url.substring(0, index);
} else {
_host = url;
}
// store the remaining part as path
if (index >= 0) {
url.remove(0, index);
this->path = url;
} else {
this->path = "/";
}
// separate host from port, when present
index = _host.indexOf(':');
if (index >= 0) {
this->port = _host.substring(index + 1).toInt();
this->host = _host.substring(0, index);
} else {
this->host = _host;
}
}

+ 26
- 0
code/espurna/light.h View File

@ -0,0 +1,26 @@
// -----------------------------------------------------------------------------
// Light
// -----------------------------------------------------------------------------
#pragma once
namespace Light {
constexpr const unsigned char VALUE_MIN = LIGHT_MIN_VALUE;
constexpr const unsigned char VALUE_MAX = LIGHT_MAX_VALUE;
constexpr const unsigned int BRIGHTNESS_MIN = LIGHT_MIN_BRIGHTNESS;
constexpr const unsigned int BRIGHTNESS_MAX = LIGHT_MAX_BRIGHTNESS;
// Default to the Philips Hue value that HA also use.
// https://developers.meethue.com/documentation/core-concepts
constexpr const unsigned int MIREDS_COLDWHITE = LIGHT_COLDWHITE_MIRED;
constexpr const unsigned int MIREDS_WARMWHITE = LIGHT_WARMWHITE_MIRED;
constexpr const unsigned int KELVIN_WARMWHITE = LIGHT_WARMWHITE_KELVIN;
constexpr const unsigned int KELVIN_COLDWHITE = LIGHT_COLDWHITE_KELVIN;
constexpr const unsigned int PWM_MIN = LIGHT_MIN_PWM;
constexpr const unsigned int PWM_MAX = LIGHT_MAX_PWM;
constexpr const unsigned int PWM_LIMIT = LIGHT_LIMIT_PWM;
}

+ 278
- 177
code/espurna/light.ino View File

@ -8,7 +8,10 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#include "light.h"
#include <Ticker.h> #include <Ticker.h>
#include <Schedule.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <vector> #include <vector>
@ -29,15 +32,15 @@ Ticker _light_comms_ticker;
Ticker _light_save_ticker; Ticker _light_save_ticker;
Ticker _light_transition_ticker; Ticker _light_transition_ticker;
typedef struct {
unsigned char pin;
bool reverse;
bool state;
unsigned char inputValue; // value that has been inputted
unsigned char value; // normalized value including brightness
struct channel_t {
unsigned char pin; // real GPIO pin
bool reverse; // wether we should invert the value before using it
bool state; // is the channel ON
unsigned char inputValue; // raw value, without the brightness
unsigned char value; // normalized value, including brightness
unsigned char target; // target value unsigned char target; // target value
double current; // transition value double current; // transition value
} channel_t;
};
std::vector<channel_t> _light_channel; std::vector<channel_t> _light_channel;
bool _light_dirty = false; bool _light_dirty = false;
@ -48,9 +51,20 @@ bool _light_has_color = false;
bool _light_use_white = false; bool _light_use_white = false;
bool _light_use_cct = false; bool _light_use_cct = false;
bool _light_use_gamma = false; bool _light_use_gamma = false;
bool _light_provider_update = false;
bool _light_use_transitions = false;
unsigned int _light_transition_time = LIGHT_TRANSITION_TIME;
unsigned long _light_steps_left = 1; unsigned long _light_steps_left = 1;
unsigned char _light_brightness = LIGHT_MAX_BRIGHTNESS;
unsigned int _light_mireds = round((LIGHT_COLDWHITE_MIRED+LIGHT_WARMWHITE_MIRED)/2);
bool _light_dirty = false;
bool _light_state = false;
unsigned char _light_brightness = Light::BRIGHTNESS_MAX;
unsigned int _light_mireds = lround((Light::MIREDS_COLDWHITE + Light::MIREDS_WARMWHITE) / 2);
using light_brightness_func_t = void();
light_brightness_func_t* _light_brightness_func = nullptr;
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
#include <my92xx.h> #include <my92xx.h>
@ -58,9 +72,18 @@ my92xx * _my92xx;
ARRAYINIT(unsigned char, _light_channel_map, MY92XX_MAPPING); ARRAYINIT(unsigned char, _light_channel_map, MY92XX_MAPPING);
#endif #endif
// UI hint about channel distribution
const char _light_channel_desc[5][5] PROGMEM = {
{'W', 0, 0, 0, 0},
{'W', 'C', 0, 0, 0},
{'R', 'G', 'B', 0, 0},
{'R', 'G', 'B', 'W', 0},
{'R', 'G', 'B', 'W', 'C'}
};
static_assert((LIGHT_CHANNELS * LIGHT_CHANNELS) <= (sizeof(_light_channel_desc)), "Out-of-bounds array access");
// Gamma Correction lookup table (8 bit) // Gamma Correction lookup table (8 bit)
// TODO: move to PROGMEM
const unsigned char _light_gamma_table[] = {
const unsigned char _light_gamma_table[] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6,
@ -78,19 +101,20 @@ const unsigned char _light_gamma_table[] = {
191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221,
223, 225, 227, 229, 231, 233, 235, 238, 240, 242, 244, 246, 248, 251, 253, 255 223, 225, 227, 229, 231, 233, 235, 238, 240, 242, 244, 246, 248, 251, 253, 255
}; };
static_assert(Light::VALUE_MAX <= sizeof(_light_gamma_table), "Out-of-bounds array access");
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// UTILS // UTILS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void _setValue(unsigned char id, unsigned int value) {
void _setValue(const unsigned char id, const unsigned int value) {
if (_light_channel[id].value != value) { if (_light_channel[id].value != value) {
_light_channel[id].value = value; _light_channel[id].value = value;
_light_dirty = true; _light_dirty = true;
} }
} }
void _setInputValue(unsigned char id, unsigned int value) {
void _setInputValue(const unsigned char id, const unsigned int value) {
if (_light_channel[id].inputValue != value) { if (_light_channel[id].inputValue != value) {
_light_channel[id].inputValue = value; _light_channel[id].inputValue = value;
//_light_dirty = true; //_light_dirty = true;
@ -98,76 +122,98 @@ void _setInputValue(unsigned char id, unsigned int value) {
} }
void _setRGBInputValue(unsigned char red, unsigned char green, unsigned char blue) { void _setRGBInputValue(unsigned char red, unsigned char green, unsigned char blue) {
_setInputValue(0, constrain(red, 0, LIGHT_MAX_VALUE));
_setInputValue(1, constrain(green, 0, LIGHT_MAX_VALUE));
_setInputValue(2, constrain(blue, 0, LIGHT_MAX_VALUE));
_setInputValue(0, constrain(red, Light::VALUE_MIN, Light::VALUE_MAX));
_setInputValue(1, constrain(green, Light::VALUE_MIN, Light::VALUE_MAX));
_setInputValue(2, constrain(blue, Light::VALUE_MIN, Light::VALUE_MAX));
} }
void _generateBrightness() {
double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
void _setCCTInputValue(unsigned char warm, unsigned char cold) {
_setInputValue(0, constrain(warm, Light::VALUE_MIN, Light::VALUE_MAX));
_setInputValue(1, constrain(cold, Light::VALUE_MIN, Light::VALUE_MAX));
}
// Convert RGB to RGBW(W)
if (_light_has_color && _light_use_white) {
void _lightApplyBrightness(unsigned char channels = lightChannels()) {
// Substract the common part from RGB channels and add it to white channel. So [250,150,50] -> [200,100,0,50]
unsigned char white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue));
for (unsigned int i=0; i < 3; i++) {
_setValue(i, _light_channel[i].inputValue - white);
}
double brightness = static_cast<double>(_light_brightness) / static_cast<double>(Light::BRIGHTNESS_MAX);
// Split the White Value across 2 White LED Strips.
if (_light_use_cct) {
channels = std::min(channels, lightChannels());
// This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end.
double miredFactor = ((double) _light_mireds - (double) LIGHT_COLDWHITE_MIRED)/((double) LIGHT_WARMWHITE_MIRED - (double) LIGHT_COLDWHITE_MIRED);
for (unsigned char i=0; i < lightChannels(); i++) {
if (i >= channels) brightness = 1;
_setValue(i, _light_channel[i].inputValue * brightness);
}
// set cold white
_light_channel[3].inputValue = 0;
_setValue(3, round(((double) 1.0 - miredFactor) * white));
}
// set warm white
_light_channel[4].inputValue = 0;
_setValue(4, round(miredFactor * white));
} else {
_light_channel[3].inputValue = 0;
_setValue(3, white);
}
void _lightApplyBrightnessColor() {
// Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63]
unsigned char max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue));
unsigned char max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value));
unsigned char channelSize = _light_use_cct ? 5 : 4;
double brightness = static_cast<double>(_light_brightness) / static_cast<double>(Light::BRIGHTNESS_MAX);
if (_light_use_cct) {
max_out = std::max(max_out, _light_channel[4].value);
}
// Substract the common part from RGB channels and add it to white channel. So [250,150,50] -> [200,100,0,50]
unsigned char white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue));
for (unsigned int i=0; i < 3; i++) {
_setValue(i, _light_channel[i].inputValue - white);
}
double factor = (max_out > 0) ? (double) (max_in / max_out) : 0;
for (unsigned char i=0; i < channelSize; i++) {
_setValue(i, round((double) _light_channel[i].value * factor * brightness));
}
// Split the White Value across 2 White LED Strips.
if (_light_use_cct) {
// Scale white channel to match brightness
for (unsigned char i=3; i < channelSize; i++) {
_setValue(i, constrain(_light_channel[i].value * LIGHT_WHITE_FACTOR, 0, LIGHT_MAX_BRIGHTNESS));
}
// This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end.
double miredFactor = ((double) _light_mireds - (double) Light::MIREDS_COLDWHITE)/((double) Light::MIREDS_WARMWHITE - (double) Light::MIREDS_COLDWHITE);
// For the rest of channels, don't apply brightness, it is already in the inputValue
// i should be 4 when RGBW and 5 when RGBWW
for (unsigned char i=channelSize; i < _light_channel.size(); i++) {
_setValue(i, _light_channel[i].inputValue);
}
// set cold white
_light_channel[3].inputValue = 0;
_setValue(3, lround(((double) 1.0 - miredFactor) * white));
// set warm white
_light_channel[4].inputValue = 0;
_setValue(4, lround(miredFactor * white));
} else { } else {
_light_channel[3].inputValue = 0;
_setValue(3, white);
}
// Apply brightness equally to all channels
for (unsigned char i=0; i < _light_channel.size(); i++) {
_setValue(i, _light_channel[i].inputValue * brightness);
}
// Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63]
unsigned char max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue));
unsigned char max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value));
unsigned char channelSize = _light_use_cct ? 5 : 4;
if (_light_use_cct) {
max_out = std::max(max_out, _light_channel[4].value);
}
double factor = (max_out > 0) ? (double) (max_in / max_out) : 0;
for (unsigned char i=0; i < channelSize; i++) {
_setValue(i, lround((double) _light_channel[i].value * factor * brightness));
} }
// Scale white channel to match brightness
for (unsigned char i=3; i < channelSize; i++) {
_setValue(i, constrain(static_cast<unsigned int>(_light_channel[i].value * LIGHT_WHITE_FACTOR), Light::BRIGHTNESS_MIN, Light::BRIGHTNESS_MAX));
}
// For the rest of channels, don't apply brightness, it is already in the inputValue
// i should be 4 when RGBW and 5 when RGBWW
for (unsigned char i=channelSize; i < _light_channel.size(); i++) {
_setValue(i, _light_channel[i].inputValue);
}
}
String lightDesc(unsigned char id) {
if (id >= _light_channel.size()) return F("UNKNOWN");
const char tag = pgm_read_byte(&_light_channel_desc[_light_channel.size() - 1][id]);
switch (tag) {
case 'W': return F("WARM WHITE");
case 'C': return F("COLD WHITE");
case 'R': return F("RED");
case 'G': return F("GREEN");
case 'B': return F("BLUE");
default: break;
}
return F("UNKNOWN");
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -177,7 +223,7 @@ void _generateBrightness() {
void _fromLong(unsigned long value, bool brightness) { void _fromLong(unsigned long value, bool brightness) {
if (brightness) { if (brightness) {
_setRGBInputValue((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF); _setRGBInputValue((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF);
_light_brightness = (value & 0xFF) * LIGHT_MAX_BRIGHTNESS / 255;
_light_brightness = (value & 0xFF) * Light::BRIGHTNESS_MAX / 255;
} else { } else {
_setRGBInputValue((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value) & 0xFF); _setRGBInputValue((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value) & 0xFF);
} }
@ -216,12 +262,12 @@ void _fromRGB(const char * rgb) {
// RGB but less than 3 values received, assume it is 0 // RGB but less than 3 values received, assume it is 0
if (_light_has_color && (count < 3)) { if (_light_has_color && (count < 3)) {
// check channel 1 and 2:
for (int i = 1; i <= 2; i++) {
if (count < (i+1)) {
_setInputValue(i, 0);
// check channel 1 and 2:
for (int i = 1; i <= 2; i++) {
if (count < (i+1)) {
_setInputValue(i, 0);
}
} }
}
} }
break; break;
} }
@ -259,32 +305,32 @@ void _fromHSV(const char * hsv) {
double f = (h - floor(h)); double f = (h - floor(h));
double s = (double) value[1] / 100.0; double s = (double) value[1] / 100.0;
_light_brightness = round((double) value[2] * 2.55); // (255/100)
unsigned char p = round(255 * (1.0 - s));
unsigned char q = round(255 * (1.0 - s * f));
unsigned char t = round(255 * (1.0 - s * (1.0 - f)));
_light_brightness = lround((double) value[2] * (static_cast<double>(Light::BRIGHTNESS_MAX) / 100.0)); // (default 255/100)
unsigned char p = lround(Light::VALUE_MAX * (1.0 - s));
unsigned char q = lround(Light::VALUE_MAX * (1.0 - s * f));
unsigned char t = lround(Light::VALUE_MAX * (1.0 - s * (1.0 - f)));
switch (int(h)) { switch (int(h)) {
case 0: case 0:
_setRGBInputValue(255, t, p);
_setRGBInputValue(Light::VALUE_MAX, t, p);
break; break;
case 1: case 1:
_setRGBInputValue(q, 255, p);
_setRGBInputValue(q, Light::VALUE_MAX, p);
break; break;
case 2: case 2:
_setRGBInputValue(p, 255, t);
_setRGBInputValue(p, Light::VALUE_MAX, t);
break; break;
case 3: case 3:
_setRGBInputValue(p, q, 255);
_setRGBInputValue(p, q, Light::VALUE_MAX);
break; break;
case 4: case 4:
_setRGBInputValue(t, p, 255);
_setRGBInputValue(t, p, Light::VALUE_MAX);
break; break;
case 5: case 5:
_setRGBInputValue(255, p, q);
_setRGBInputValue(Light::VALUE_MAX, p, q);
break; break;
default: default:
_setRGBInputValue(0, 0, 0);
_setRGBInputValue(Light::VALUE_MIN, Light::VALUE_MIN, Light::VALUE_MIN);
break; break;
} }
} }
@ -293,25 +339,39 @@ void _fromHSV(const char * hsv) {
// https://github.com/stelgenhof/AiLight // https://github.com/stelgenhof/AiLight
void _fromKelvin(unsigned long kelvin) { void _fromKelvin(unsigned long kelvin) {
if (!_light_has_color) return;
if (!_light_has_color) {
if(!_light_use_cct) return;
_light_mireds = constrain(static_cast<unsigned int>(lround(1000000UL / kelvin)), Light::MIREDS_COLDWHITE, Light::MIREDS_WARMWHITE);
// This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end.
double factor = ((double) _light_mireds - (double) Light::MIREDS_COLDWHITE)/((double) Light::MIREDS_WARMWHITE - (double) Light::MIREDS_COLDWHITE);
unsigned char warm = lround(factor * Light::VALUE_MAX);
unsigned char cold = lround(((double) 1.0 - factor) * Light::VALUE_MAX);
_setCCTInputValue(warm, cold);
return;
}
_light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
_light_mireds = constrain(static_cast<unsigned int>(lround(1000000UL / kelvin)), Light::MIREDS_COLDWHITE, Light::MIREDS_WARMWHITE);
if (_light_use_cct) { if (_light_use_cct) {
_setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE);
_setRGBInputValue(Light::VALUE_MAX, Light::VALUE_MAX, Light::VALUE_MAX);
return; return;
} }
// Calculate colors // Calculate colors
kelvin /= 100; kelvin /= 100;
unsigned int red = (kelvin <= 66) unsigned int red = (kelvin <= 66)
? LIGHT_MAX_VALUE
? Light::VALUE_MAX
: 329.698727446 * fs_pow((double) (kelvin - 60), -0.1332047592); : 329.698727446 * fs_pow((double) (kelvin - 60), -0.1332047592);
unsigned int green = (kelvin <= 66) unsigned int green = (kelvin <= 66)
? 99.4708025861 * fs_log(kelvin) - 161.1195681661 ? 99.4708025861 * fs_log(kelvin) - 161.1195681661
: 288.1221695283 * fs_pow((double) kelvin, -0.0755148492); : 288.1221695283 * fs_pow((double) kelvin, -0.0755148492);
unsigned int blue = (kelvin >= 66) unsigned int blue = (kelvin >= 66)
? LIGHT_MAX_VALUE
? Light::VALUE_MAX
: ((kelvin <= 19) : ((kelvin <= 19)
? 0 ? 0
: 138.5177312231 * fs_log(kelvin - 10) - 305.0447927307); : 138.5177312231 * fs_log(kelvin - 10) - 305.0447927307);
@ -322,7 +382,7 @@ void _fromKelvin(unsigned long kelvin) {
// Color temperature is measured in mireds (kelvin = 1e6/mired) // Color temperature is measured in mireds (kelvin = 1e6/mired)
void _fromMireds(unsigned long mireds) { void _fromMireds(unsigned long mireds) {
unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000);
unsigned long kelvin = constrain(static_cast<unsigned int>(1000000UL / mireds), Light::KELVIN_WARMWHITE, Light::KELVIN_COLDWHITE);
_fromKelvin(kelvin); _fromKelvin(kelvin);
} }
@ -330,7 +390,7 @@ void _fromMireds(unsigned long mireds) {
// Output Values // Output Values
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void _toRGB(char * rgb, size_t len, bool target) {
void _toRGB(char * rgb, size_t len, bool target = false) {
unsigned long value = 0; unsigned long value = 0;
value += target ? _light_channel[0].target : _light_channel[0].inputValue; value += target ? _light_channel[0].target : _light_channel[0].inputValue;
@ -342,20 +402,17 @@ void _toRGB(char * rgb, size_t len, bool target) {
snprintf_P(rgb, len, PSTR("#%06X"), value); snprintf_P(rgb, len, PSTR("#%06X"), value);
} }
void _toRGB(char * rgb, size_t len) {
_toRGB(rgb, len, false);
}
void _toHSV(char * hsv, size_t len, bool target) {
double h, s, v;
double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
void _toHSV(char * hsv, size_t len) {
double h {0.}, s {0.}, v {0.};
double r {0.}, g {0.}, b {0.};
double min {0.}, max {0.};
double r = (double) ((target ? _light_channel[0].target : _light_channel[0].inputValue) * brightness) / 255.0;
double g = (double) ((target ? _light_channel[1].target : _light_channel[1].inputValue) * brightness) / 255.0;
double b = (double) ((target ? _light_channel[2].target : _light_channel[2].inputValue) * brightness) / 255.0;
r = static_cast<double>(_light_channel[0].target) / Light::VALUE_MAX;
g = static_cast<double>(_light_channel[1].target) / Light::VALUE_MAX;
b = static_cast<double>(_light_channel[2].target) / Light::VALUE_MAX;
double min = std::min(r, std::min(g, b));
double max = std::max(r, std::max(g, b));
min = std::min(r, std::min(g, b));
max = std::max(r, std::max(g, b));
v = 100.0 * max; v = 100.0 * max;
if (v == 0) { if (v == 0) {
@ -379,22 +436,22 @@ void _toHSV(char * hsv, size_t len, bool target) {
} }
} }
// String
snprintf_P(hsv, len, PSTR("%d,%d,%d"), round(h), round(s), round(v));
}
void _toHSV(char * hsv, size_t len) {
_toHSV(hsv, len, false);
// Convert to string. Using lround, since we can't (yet) printf floats
snprintf(hsv, len, "%d,%d,%d",
static_cast<int>(lround(h)),
static_cast<int>(lround(s)),
static_cast<int>(lround(v))
);
} }
void _toLong(char * color, size_t len, bool target) { void _toLong(char * color, size_t len, bool target) {
if (!_light_has_color) return; if (!_light_has_color) return;
snprintf_P(color, len, PSTR("%d,%d,%d"),
(int) (target ? _light_channel[0].target : _light_channel[0].inputValue),
(int) (target ? _light_channel[1].target : _light_channel[1].inputValue),
(int) (target ? _light_channel[2].target : _light_channel[2].inputValue)
snprintf_P(color, len, PSTR("%u,%u,%u"),
(target ? _light_channel[0].target : _light_channel[0].inputValue),
(target ? _light_channel[1].target : _light_channel[1].inputValue),
(target ? _light_channel[2].target : _light_channel[2].inputValue)
); );
} }
@ -405,7 +462,7 @@ void _toLong(char * color, size_t len) {
void _toCSV(char * buffer, size_t len, bool applyBrightness, bool target) { void _toCSV(char * buffer, size_t len, bool applyBrightness, bool target) {
char num[10]; char num[10];
float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1;
float b = applyBrightness ? (float) _light_brightness / Light::BRIGHTNESS_MAX : 1;
for (unsigned char i=0; i<_light_channel.size(); i++) { for (unsigned char i=0; i<_light_channel.size(); i++) {
itoa((target ? _light_channel[i].target : _light_channel[i].inputValue) * b, num, 10); itoa((target ? _light_channel[i].target : _light_channel[i].inputValue) * b, num, 10);
if (i>0) strncat(buffer, ",", len--); if (i>0) strncat(buffer, ",", len--);
@ -418,14 +475,24 @@ void _toCSV(char * buffer, size_t len, bool applyBrightness) {
_toCSV(buffer, len, applyBrightness, false); _toCSV(buffer, len, applyBrightness, false);
} }
// See cores/esp8266/WMath.cpp::map
// Redefining as local method here to avoid breaking in unexpected ways in inputs like (0, 0, 0, 0, 1)
template <typename T, typename Tin, typename Tout> T _lightMap(T x, Tin in_min, Tin in_max, Tout out_min, Tout out_max) {
auto divisor = (in_max - in_min);
if (divisor == 0){
return -1; //AVR returns -1, SAM returns 0
}
return (x - in_min) * (out_max - out_min) / divisor + out_min;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PROVIDER // PROVIDER
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
unsigned int _toPWM(unsigned long value, bool gamma, bool reverse) {
value = constrain(value, 0, LIGHT_MAX_VALUE);
if (gamma) value = _light_gamma_table[value];
if (LIGHT_MAX_VALUE != LIGHT_LIMIT_PWM) value = map(value, 0, LIGHT_MAX_VALUE, 0, LIGHT_LIMIT_PWM);
unsigned int _toPWM(unsigned int value, bool gamma, bool reverse) {
value = constrain(value, Light::VALUE_MIN, Light::VALUE_MAX);
if (gamma) value = pgm_read_byte(_light_gamma_table + value);
if (Light::VALUE_MAX != Light::PWM_LIMIT) value = _lightMap(value, Light::VALUE_MIN, Light::VALUE_MAX, Light::PWM_MIN, Light::PWM_LIMIT);
if (reverse) value = LIGHT_LIMIT_PWM - value; if (reverse) value = LIGHT_LIMIT_PWM - value;
return value; return value;
} }
@ -458,6 +525,10 @@ void _transition() {
void _lightProviderUpdate() { void _lightProviderUpdate() {
if (_light_provider_update) return;
_light_provider_update = true;
_transition(); _transition();
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
@ -479,6 +550,12 @@ void _lightProviderUpdate() {
#endif #endif
_light_provider_update = false;
}
void _lightProviderDoUpdate() {
schedule_function(_lightProviderUpdate);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -536,11 +613,10 @@ void _lightSaveSettings() {
void _lightRestoreSettings() { void _lightRestoreSettings() {
for (unsigned int i=0; i < _light_channel.size(); i++) { for (unsigned int i=0; i < _light_channel.size(); i++) {
_light_channel[i].inputValue = getSetting("ch", i, i==0 ? 255 : 0).toInt();
_light_channel[i].inputValue = getSetting("ch", i, (i == 0) ? Light::VALUE_MAX : 0).toInt();
} }
_light_brightness = getSetting("brightness", LIGHT_MAX_BRIGHTNESS).toInt();
_light_brightness = getSetting("brightness", Light::BRIGHTNESS_MAX).toInt();
_light_mireds = getSetting("mireds", _light_mireds).toInt(); _light_mireds = getSetting("mireds", _light_mireds).toInt();
lightUpdate(false, false);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -557,13 +633,16 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
mqttSubscribe(MQTT_TOPIC_BRIGHTNESS); mqttSubscribe(MQTT_TOPIC_BRIGHTNESS);
if (_light_has_color) { if (_light_has_color) {
mqttSubscribe(MQTT_TOPIC_MIRED);
mqttSubscribe(MQTT_TOPIC_KELVIN);
mqttSubscribe(MQTT_TOPIC_COLOR_RGB); mqttSubscribe(MQTT_TOPIC_COLOR_RGB);
mqttSubscribe(MQTT_TOPIC_COLOR_HSV); mqttSubscribe(MQTT_TOPIC_COLOR_HSV);
mqttSubscribe(MQTT_TOPIC_TRANSITION); mqttSubscribe(MQTT_TOPIC_TRANSITION);
} }
if (_light_has_color || _light_use_cct) {
mqttSubscribe(MQTT_TOPIC_MIRED);
mqttSubscribe(MQTT_TOPIC_KELVIN);
}
// Group color // Group color
if (mqtt_group_color.length() > 0) mqttSubscribeRaw(mqtt_group_color.c_str()); if (mqtt_group_color.length() > 0) mqttSubscribeRaw(mqtt_group_color.c_str());
@ -614,7 +693,7 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
// Brightness // Brightness
if (t.equals(MQTT_TOPIC_BRIGHTNESS)) { if (t.equals(MQTT_TOPIC_BRIGHTNESS)) {
_light_brightness = constrain(atoi(payload), 0, LIGHT_MAX_BRIGHTNESS);
lightBrightness(atoi(payload));
lightUpdate(true, mqttForward()); lightUpdate(true, mqttForward());
return; return;
} }
@ -655,13 +734,17 @@ void lightMQTT() {
} }
mqttSend(MQTT_TOPIC_COLOR_RGB, buffer); mqttSend(MQTT_TOPIC_COLOR_RGB, buffer);
_toHSV(buffer, sizeof(buffer), true);
_toHSV(buffer, sizeof(buffer));
mqttSend(MQTT_TOPIC_COLOR_HSV, buffer); mqttSend(MQTT_TOPIC_COLOR_HSV, buffer);
// Mireds
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds);
mqttSend(MQTT_TOPIC_MIRED, buffer);
}
if (_light_has_color || _light_use_cct) {
// Mireds
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds);
mqttSend(MQTT_TOPIC_MIRED, buffer);
} }
// Channels // Channels
@ -715,6 +798,10 @@ bool lightHasColor() {
return _light_has_color; return _light_has_color;
} }
bool lightUseCCT() {
return _light_use_cct;
}
void _lightComms(unsigned char mask) { void _lightComms(unsigned char mask) {
// Report color & brightness to MQTT broker // Report color & brightness to MQTT broker
@ -725,7 +812,7 @@ void _lightComms(unsigned char mask) {
// Report color to WS clients (using current brightness setting) // Report color to WS clients (using current brightness setting)
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend(_lightWebSocketStatus);
wsPost(_lightWebSocketStatus);
#endif #endif
// Report channels to local broker // Report channels to local broker
@ -738,7 +825,7 @@ void _lightComms(unsigned char mask) {
void lightUpdate(bool save, bool forward, bool group_forward) { void lightUpdate(bool save, bool forward, bool group_forward) {
// Calculate values based on inputs and brightness // Calculate values based on inputs and brightness
_generateBrightness();
_light_brightness_func();
// Only update if a channel has changed // Only update if a channel has changed
if (!_light_dirty) return; if (!_light_dirty) return;
@ -752,7 +839,7 @@ void lightUpdate(bool save, bool forward, bool group_forward) {
// Configure color transition // Configure color transition
_light_steps_left = _light_use_transitions ? _light_transition_time / LIGHT_TRANSITION_STEP : 1; _light_steps_left = _light_use_transitions ? _light_transition_time / LIGHT_TRANSITION_STEP : 1;
_light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderUpdate);
_light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderDoUpdate);
// Delay every communication 100ms to avoid jamming // Delay every communication 100ms to avoid jamming
unsigned char mask = 0; unsigned char mask = 0;
@ -839,9 +926,9 @@ unsigned int lightChannel(unsigned char id) {
return 0; return 0;
} }
void lightChannel(unsigned char id, int value) {
void lightChannel(unsigned char id, unsigned char value) {
if (id <= _light_channel.size()) { if (id <= _light_channel.size()) {
_light_channel[id].inputValue = constrain(value, 0, LIGHT_MAX_VALUE);
_setInputValue(id, constrain(value, Light::VALUE_MIN, Light::VALUE_MAX));
} }
} }
@ -853,15 +940,15 @@ unsigned int lightBrightness() {
return _light_brightness; return _light_brightness;
} }
void lightBrightness(int b) {
_light_brightness = constrain(b, 0, LIGHT_MAX_BRIGHTNESS);
void lightBrightness(unsigned int brightness) {
_light_brightness = constrain(brightness, Light::BRIGHTNESS_MIN, Light::BRIGHTNESS_MAX);
} }
void lightBrightnessStep(int steps) { void lightBrightnessStep(int steps) {
lightBrightness(_light_brightness + steps * LIGHT_STEP); lightBrightness(_light_brightness + steps * LIGHT_STEP);
} }
unsigned long lightTransitionTime() {
unsigned int lightTransitionTime() {
if (_light_use_transitions) { if (_light_use_transitions) {
return _light_transition_time; return _light_transition_time;
} else { } else {
@ -887,7 +974,7 @@ void lightTransitionTime(unsigned long m) {
#if WEB_SUPPORT #if WEB_SUPPORT
bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _lightWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "light", 5) == 0) return true; if (strncmp(key, "light", 5) == 0) return true;
if (strncmp(key, "use", 3) == 0) return true; if (strncmp(key, "use", 3) == 0) return true;
return false; return false;
@ -895,16 +982,16 @@ bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) {
void _lightWebSocketStatus(JsonObject& root) { void _lightWebSocketStatus(JsonObject& root) {
if (_light_has_color) { if (_light_has_color) {
if (_light_use_cct) {
root["useCCT"] = _light_use_cct;
root["mireds"] = _light_mireds;
}
if (getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1) { if (getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1) {
root["rgb"] = lightColor(true); root["rgb"] = lightColor(true);
} else { } else {
root["hsv"] = lightColor(false); root["hsv"] = lightColor(false);
} }
} }
if (_light_use_cct) {
root["useCCT"] = _light_use_cct;
root["mireds"] = _light_mireds;
}
JsonArray& channels = root.createNestedArray("channels"); JsonArray& channels = root.createNestedArray("channels");
for (unsigned char id=0; id < _light_channel.size(); id++) { for (unsigned char id=0; id < _light_channel.size(); id++) {
channels.add(lightChannel(id)); channels.add(lightChannel(id));
@ -912,8 +999,11 @@ void _lightWebSocketStatus(JsonObject& root) {
root["brightness"] = lightBrightness(); root["brightness"] = lightBrightness();
} }
void _lightWebSocketOnSend(JsonObject& root) {
void _lightWebSocketOnVisible(JsonObject& root) {
root["colorVisible"] = 1; root["colorVisible"] = 1;
}
void _lightWebSocketOnConnected(JsonObject& root) {
root["mqttGroupColor"] = getSetting("mqttGroupColor"); root["mqttGroupColor"] = getSetting("mqttGroupColor");
root["useColor"] = _light_has_color; root["useColor"] = _light_has_color;
root["useWhite"] = _light_use_white; root["useWhite"] = _light_use_white;
@ -939,14 +1029,16 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
lightUpdate(true, true); lightUpdate(true, true);
} }
} }
if (_light_use_cct) {
if (strcmp(action, "mireds") == 0) {
_fromMireds(data["mireds"]);
lightUpdate(true, true);
}
}
} }
if (_light_use_cct) {
if (strcmp(action, "mireds") == 0) {
_fromMireds(data["mireds"]);
lightUpdate(true, true);
}
}
if (strcmp(action, "channel") == 0) { if (strcmp(action, "channel") == 0) {
if (data.containsKey("id") && data.containsKey("value")) { if (data.containsKey("id") && data.containsKey("value")) {
lightChannel(data["id"], data["value"]); lightChannel(data["id"], data["value"]);
@ -987,7 +1079,7 @@ void _lightAPISetup() {
apiRegister(MQTT_TOPIC_COLOR_HSV, apiRegister(MQTT_TOPIC_COLOR_HSV,
[](char * buffer, size_t len) { [](char * buffer, size_t len) {
_toHSV(buffer, len, true);
_toHSV(buffer, len);
}, },
[](const char * payload) { [](const char * payload) {
lightColor(payload, false); lightColor(payload, false);
@ -1082,7 +1174,7 @@ void _lightInitCommands() {
lightChannel(id, value); lightChannel(id, value);
lightUpdate(true, true); lightUpdate(true, true);
} }
DEBUG_MSG_P(PSTR("Channel #%d: %d\n"), id, lightChannel(id));
DEBUG_MSG_P(PSTR("Channel #%d (%s): %d\n"), id, lightDesc(id).c_str(), lightChannel(id));
terminalOK(); terminalOK();
}); });
@ -1129,26 +1221,19 @@ void _lightInitCommands() {
#endif // TERMINAL_SUPPORT #endif // TERMINAL_SUPPORT
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
const unsigned long _light_iomux[16] PROGMEM = {
PERIPHS_IO_MUX_GPIO0_U, PERIPHS_IO_MUX_U0TXD_U, PERIPHS_IO_MUX_GPIO2_U, PERIPHS_IO_MUX_U0RXD_U,
PERIPHS_IO_MUX_GPIO4_U, PERIPHS_IO_MUX_GPIO5_U, PERIPHS_IO_MUX_SD_CLK_U, PERIPHS_IO_MUX_SD_DATA0_U,
PERIPHS_IO_MUX_SD_DATA1_U, PERIPHS_IO_MUX_SD_DATA2_U, PERIPHS_IO_MUX_SD_DATA3_U, PERIPHS_IO_MUX_SD_CMD_U,
PERIPHS_IO_MUX_MTDI_U, PERIPHS_IO_MUX_MTCK_U, PERIPHS_IO_MUX_MTMS_U, PERIPHS_IO_MUX_MTDO_U
};
unsigned long getIOMux(unsigned long gpio) {
unsigned long muxes[16] = {
PERIPHS_IO_MUX_GPIO0_U, PERIPHS_IO_MUX_U0TXD_U, PERIPHS_IO_MUX_GPIO2_U, PERIPHS_IO_MUX_U0RXD_U,
PERIPHS_IO_MUX_GPIO4_U, PERIPHS_IO_MUX_GPIO5_U, PERIPHS_IO_MUX_SD_CLK_U, PERIPHS_IO_MUX_SD_DATA0_U,
PERIPHS_IO_MUX_SD_DATA1_U, PERIPHS_IO_MUX_SD_DATA2_U, PERIPHS_IO_MUX_SD_DATA3_U, PERIPHS_IO_MUX_SD_CMD_U,
PERIPHS_IO_MUX_MTDI_U, PERIPHS_IO_MUX_MTCK_U, PERIPHS_IO_MUX_MTMS_U, PERIPHS_IO_MUX_MTDO_U
};
return muxes[gpio];
}
unsigned long getIOFunc(unsigned long gpio) {
unsigned long funcs[16] = {
FUNC_GPIO0, FUNC_GPIO1, FUNC_GPIO2, FUNC_GPIO3,
FUNC_GPIO4, FUNC_GPIO5, FUNC_GPIO6, FUNC_GPIO7,
FUNC_GPIO8, FUNC_GPIO9, FUNC_GPIO10, FUNC_GPIO11,
FUNC_GPIO12, FUNC_GPIO13, FUNC_GPIO14, FUNC_GPIO15
};
return funcs[gpio];
}
const unsigned long _light_iofunc[16] PROGMEM = {
FUNC_GPIO0, FUNC_GPIO1, FUNC_GPIO2, FUNC_GPIO3,
FUNC_GPIO4, FUNC_GPIO5, FUNC_GPIO6, FUNC_GPIO7,
FUNC_GPIO8, FUNC_GPIO9, FUNC_GPIO10, FUNC_GPIO11,
FUNC_GPIO12, FUNC_GPIO13, FUNC_GPIO14, FUNC_GPIO15
};
#endif #endif
@ -1161,13 +1246,23 @@ void _lightConfigure() {
} }
_light_use_white = getSetting("useWhite", LIGHT_USE_WHITE).toInt() == 1; _light_use_white = getSetting("useWhite", LIGHT_USE_WHITE).toInt() == 1;
if (_light_use_white && (_light_channel.size() < 4)) {
if (_light_use_white && (_light_channel.size() < 4) && (_light_channel.size() != 2)) {
_light_use_white = false; _light_use_white = false;
setSetting("useWhite", _light_use_white); setSetting("useWhite", _light_use_white);
} }
if (_light_has_color) {
if (_light_use_white) {
_light_brightness_func = _lightApplyBrightnessColor;
} else {
_light_brightness_func = []() { _lightApplyBrightness(3); };
}
} else {
_light_brightness_func = []() { _lightApplyBrightness(); };
}
_light_use_cct = getSetting("useCCT", LIGHT_USE_CCT).toInt() == 1; _light_use_cct = getSetting("useCCT", LIGHT_USE_CCT).toInt() == 1;
if (_light_use_cct && ((_light_channel.size() < 5) || !_light_use_white)) {
if (_light_use_cct && (((_light_channel.size() < 5) && (_light_channel.size() != 2)) || !_light_use_white)) {
_light_use_cct = false; _light_use_cct = false;
setSetting("useCCT", _light_use_cct); setSetting("useCCT", _light_use_cct);
} }
@ -1185,6 +1280,8 @@ void lightSetup() {
digitalWrite(LIGHT_ENABLE_PIN, HIGH); digitalWrite(LIGHT_ENABLE_PIN, HIGH);
#endif #endif
_light_channel.reserve(LIGHT_CHANNELS);
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
_my92xx = new my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND); _my92xx = new my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND);
@ -1219,11 +1316,12 @@ void lightSetup() {
uint32 pwm_duty_init[PWM_CHANNEL_NUM_MAX]; uint32 pwm_duty_init[PWM_CHANNEL_NUM_MAX];
uint32 io_info[PWM_CHANNEL_NUM_MAX][3]; uint32 io_info[PWM_CHANNEL_NUM_MAX][3];
for (unsigned int i=0; i < _light_channel.size(); i++) { for (unsigned int i=0; i < _light_channel.size(); i++) {
const auto pin = _light_channel.at(i).pin;
pwm_duty_init[i] = 0; pwm_duty_init[i] = 0;
io_info[i][0] = getIOMux(_light_channel[i].pin);
io_info[i][1] = getIOFunc(_light_channel[i].pin);
io_info[i][2] = _light_channel[i].pin;
pinMode(_light_channel[i].pin, OUTPUT);
io_info[i][0] = pgm_read_dword(&_light_iomux[pin]);
io_info[i][1] = pgm_read_dword(&_light_iofunc[pin]);
io_info[i][2] = pin;
pinMode(pin, OUTPUT);
} }
pwm_init(LIGHT_MAX_PWM, pwm_duty_init, PWM_CHANNEL_NUM_MAX, io_info); pwm_init(LIGHT_MAX_PWM, pwm_duty_init, PWM_CHANNEL_NUM_MAX, io_info);
pwm_start(); pwm_start();
@ -1240,11 +1338,14 @@ void lightSetup() {
} else { } else {
_lightRestoreSettings(); _lightRestoreSettings();
} }
lightUpdate(false, false);
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_lightWebSocketOnSend);
wsOnActionRegister(_lightWebSocketOnAction);
wsOnReceiveRegister(_lightWebSocketOnReceive);
wsRegister()
.onVisible(_lightWebSocketOnVisible)
.onConnected(_lightWebSocketOnConnected)
.onAction(_lightWebSocketOnAction)
.onKeyCheck(_lightWebSocketOnKeyCheck);
#endif #endif
#if API_SUPPORT #if API_SUPPORT


+ 4
- 3
code/espurna/lightfox.ino View File

@ -47,7 +47,7 @@ void lightfoxClear() {
#if WEB_SUPPORT #if WEB_SUPPORT
void _lightfoxWebSocketOnSend(JsonObject& root) {
void _lightfoxWebSocketOnConnected(JsonObject& root) {
root["lightfoxVisible"] = 1; root["lightfoxVisible"] = 1;
uint8_t buttonsCount = _buttons.size(); uint8_t buttonsCount = _buttons.size();
root["lightfoxRelayCount"] = relayCount(); root["lightfoxRelayCount"] = relayCount();
@ -94,8 +94,9 @@ void _lightfoxInitCommands() {
void lightfoxSetup() { void lightfoxSetup() {
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_lightfoxWebSocketOnSend);
wsOnActionRegister(_lightfoxWebSocketOnAction);
wsRegister()
.onConnected(_lightfoxWebSocketOnConnected)
.onAction(_lightfoxWebSocketOnAction);
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT


+ 1
- 0
code/espurna/mdns.ino View File

@ -50,6 +50,7 @@ void mdnsServerSetup() {
// Public ESPurna related txt for OTA discovery // Public ESPurna related txt for OTA discovery
MDNS.addServiceTxt("arduino", "tcp", "app_name", APP_NAME); MDNS.addServiceTxt("arduino", "tcp", "app_name", APP_NAME);
MDNS.addServiceTxt("arduino", "tcp", "app_version", APP_VERSION); MDNS.addServiceTxt("arduino", "tcp", "app_version", APP_VERSION);
MDNS.addServiceTxt("arduino", "tcp", "build_date", buildTime());
MDNS.addServiceTxt("arduino", "tcp", "mac", WiFi.macAddress()); MDNS.addServiceTxt("arduino", "tcp", "mac", WiFi.macAddress());
MDNS.addServiceTxt("arduino", "tcp", "target_board", getBoardName()); MDNS.addServiceTxt("arduino", "tcp", "target_board", getBoardName());
{ {


+ 12
- 0
code/espurna/migrate.ino View File

@ -1315,6 +1315,18 @@ void migrate() {
setSetting("chLogic", 3, 0); setSetting("chLogic", 3, 0);
setSetting("relays", 1); setSetting("relays", 1);
#elif defined(ISELECTOR_SM_PW702)
setSetting("board", 98);
setSetting("ledGPIO", 0, 4);
setSetting("ledLogic", 0, 0);
setSetting("ledGPIO", 1, 5);
setSetting("ledLogic", 0, 0);
setSetting("btnGPIO", 0, 13);
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 12);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#else #else
// Allow users to define new settings without migration config // Allow users to define new settings without migration config


+ 395
- 203
code/espurna/mqtt.ino View File

@ -3,6 +3,7 @@
MQTT MODULE MQTT MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com> Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Updated secure client support by Niek van der Maas < mail at niekvandermaas dot nl>
*/ */
@ -13,30 +14,50 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <ESP8266mDNS.h> #include <ESP8266mDNS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <vector> #include <vector>
#include <utility>
#include <Ticker.h> #include <Ticker.h>
#include <TimeLib.h>
#if MQTT_USE_ASYNC // Using AsyncMqttClient
#include "libs/SecureClientHelpers.h"
#include <AsyncMqttClient.h>
AsyncMqttClient _mqtt;
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
#else // Using PubSubClient
AsyncMqttClient _mqtt;
#include <PubSubClient.h>
PubSubClient _mqtt;
bool _mqtt_connected = false;
#else // MQTT_LIBRARY_ARDUINOMQTT / MQTT_LIBRARY_PUBSUBCLIENT
WiFiClient _mqtt_client;
#if SECURE_CLIENT != SECURE_CLIENT_NONE
std::unique_ptr<SecureClient> _mqtt_client_secure = nullptr;
#if MQTT_SECURE_CLIENT_INCLUDE_CA
#include "static/mqtt_client_trusted_root_ca.h" // Assumes this header file defines a _mqtt_client_trusted_root_ca[] PROGMEM = "...PEM data..."
#else
#include "static/letsencrypt_isrgroot_pem.h" // Default to LetsEncrypt X3 certificate
#define _mqtt_client_trusted_root_ca _ssl_letsencrypt_isrg_x3_ca
#endif // MQTT_SECURE_CLIENT_INCLUDE_CA
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
WiFiClient _mqtt_client;
#if ASYNC_TCP_SSL_ENABLED
WiFiClientSecure _mqtt_client_secure;
#endif // ASYNC_TCP_SSL_ENABLED
#if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
#ifdef MQTT_MAX_PACKET_SIZE
MQTTClient _mqtt(MQTT_MAX_PACKET_SIZE);
#else
MQTTClient _mqtt;
#endif
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
PubSubClient _mqtt;
#endif
#endif // MQTT_LIBRARY == MQTT_ASYNCMQTTCLIENT
#endif // MQTT_USE_ASYNC
bool _mqtt_enabled = MQTT_ENABLED; bool _mqtt_enabled = MQTT_ENABLED;
bool _mqtt_use_json = false; bool _mqtt_use_json = false;
unsigned long _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; unsigned long _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
unsigned long _mqtt_last_connection = 0; unsigned long _mqtt_last_connection = 0;
bool _mqtt_connected = false;
bool _mqtt_connecting = false; bool _mqtt_connecting = false;
unsigned char _mqtt_qos = MQTT_QOS; unsigned char _mqtt_qos = MQTT_QOS;
bool _mqtt_retain = MQTT_RETAIN; bool _mqtt_retain = MQTT_RETAIN;
@ -46,18 +67,24 @@ String _mqtt_topic_json;
String _mqtt_setter; String _mqtt_setter;
String _mqtt_getter; String _mqtt_getter;
bool _mqtt_forward; bool _mqtt_forward;
char *_mqtt_user = 0;
char *_mqtt_pass = 0;
char *_mqtt_will;
char *_mqtt_clientid;
String _mqtt_user;
String _mqtt_pass;
String _mqtt_will;
String _mqtt_server;
uint16_t _mqtt_port;
String _mqtt_clientid;
String _mqtt_payload_online;
String _mqtt_payload_offline;
std::vector<mqtt_callback_f> _mqtt_callbacks; std::vector<mqtt_callback_f> _mqtt_callbacks;
typedef struct {
unsigned char parent = 255;
struct mqtt_message_t {
static const unsigned char END = 255;
unsigned char parent = END;
char * topic; char * topic;
char * message = NULL; char * message = NULL;
} mqtt_message_t;
};
std::vector<mqtt_message_t> _mqtt_queue; std::vector<mqtt_message_t> _mqtt_queue;
Ticker _mqtt_flush_ticker; Ticker _mqtt_flush_ticker;
@ -65,6 +92,115 @@ Ticker _mqtt_flush_ticker;
// Private // Private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
SecureClientConfig _mqtt_sc_config {
"MQTT",
[]() -> String {
return _mqtt_server;
},
[]() -> int {
return getSetting("mqttScCheck", MQTT_SECURE_CLIENT_CHECK).toInt();
},
[]() -> String {
return getSetting("mqttfp", MQTT_SSL_FINGERPRINT);
},
true
};
#endif
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
SecureClientConfig _mqtt_sc_config {
"MQTT",
[]() -> int {
return getSetting("mqttScCheck", MQTT_SECURE_CLIENT_CHECK).toInt();
},
[]() -> PGM_P {
return _mqtt_client_trusted_root_ca;
},
[]() -> String {
return getSetting("mqttfp", MQTT_SSL_FINGERPRINT);
},
[]() -> uint16_t {
return getSetting("mqttScMFLN", MQTT_SECURE_CLIENT_MFLN).toInt();
},
true
};
#endif
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
void _mqttSetupAsyncClient(bool secure = false) {
_mqtt.setServer(_mqtt_server.c_str(), _mqtt_port);
_mqtt.setClientId(_mqtt_clientid.c_str());
_mqtt.setKeepAlive(_mqtt_keepalive);
_mqtt.setCleanSession(false);
_mqtt.setWill(_mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str());
if (_mqtt_user.length() && _mqtt_pass.length()) {
DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user.c_str());
_mqtt.setCredentials(_mqtt_user.c_str(), _mqtt_pass.c_str());
}
#if SECURE_CLIENT != SECURE_CLIENT_NONE
if (secure) {
DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n"));
_mqtt.setSecure(secure);
}
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
_mqtt.connect();
}
#endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
#if (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT)
bool _mqttSetupSyncClient(bool secure = false) {
#if SECURE_CLIENT != SECURE_CLIENT_NONE
if (secure) {
if (!_mqtt_client_secure) _mqtt_client_secure = std::make_unique<SecureClient>(_mqtt_sc_config);
return _mqtt_client_secure->beforeConnected();
}
#endif
return true;
}
bool _mqttConnectSyncClient(bool secure = false) {
bool result = false;
#if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
_mqtt.begin(_mqtt_server.c_str(), _mqtt_port, (secure ? _mqtt_client_secure->get() : _mqtt_client));
_mqtt.setWill(_mqtt_will.c_str(), _mqtt_payload_offline.c_str(), _mqtt_qos, _mqtt_retain);
result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_user.c_str(), _mqtt_pass.c_str());
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
_mqtt.setClient(secure ? _mqtt_client_secure->get() : _mqtt_client);
_mqtt.setServer(_mqtt_server.c_str(), _mqtt_port);
if (_mqtt_user.length() && _mqtt_pass.length()) {
DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user.c_str());
result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_user.c_str(), _mqtt_pass.c_str(), _mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str());
} else {
result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str());
}
#endif
#if SECURE_CLIENT != SECURE_CLIENT_NONE
if (result && secure) {
result = _mqtt_client_secure->afterConnected();
}
#endif
return result;
}
#endif // (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT)
void _mqttConnect() { void _mqttConnect() {
// Do not connect if disabled // Do not connect if disabled
@ -82,179 +218,155 @@ void _mqttConnect() {
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX;
} }
String h = getSetting("mqttServer", MQTT_SERVER);
#if MDNS_CLIENT_SUPPORT #if MDNS_CLIENT_SUPPORT
h = mdnsResolve(h);
_mqtt_server = mdnsResolve(_mqtt_server);
#endif #endif
char * host = strdup(h.c_str());
unsigned int port = getSetting("mqttPort", MQTT_PORT).toInt();
if (_mqtt_user) free(_mqtt_user);
if (_mqtt_pass) free(_mqtt_pass);
if (_mqtt_will) free(_mqtt_will);
if (_mqtt_clientid) free(_mqtt_clientid);
String user = getSetting("mqttUser", MQTT_USER);
_mqttPlaceholders(&user);
_mqtt_user = strdup(user.c_str());
_mqtt_pass = strdup(getSetting("mqttPassword", MQTT_PASS).c_str());
_mqtt_will = strdup(mqttTopic(MQTT_TOPIC_STATUS, false).c_str());
String clientid = getSetting("mqttClientID", getIdentifier());
_mqttPlaceholders(&clientid);
_mqtt_clientid = strdup(clientid.c_str());
DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%d\n"), host, port);
#if MQTT_USE_ASYNC
_mqtt_connecting = true;
_mqtt.setServer(host, port);
_mqtt.setClientId(_mqtt_clientid);
_mqtt.setKeepAlive(_mqtt_keepalive);
_mqtt.setCleanSession(false);
_mqtt.setWill(_mqtt_will, _mqtt_qos, _mqtt_retain, "0");
if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) {
DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user);
_mqtt.setCredentials(_mqtt_user, _mqtt_pass);
}
#if ASYNC_TCP_SSL_ENABLED
DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%u\n"), _mqtt_server.c_str(), _mqtt_port);
bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1;
_mqtt.setSecure(secure);
if (secure) {
DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n"));
unsigned char fp[20] = {0};
if (sslFingerPrintArray(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) {
_mqtt.addServerFingerprint(fp);
} else {
DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n"));
}
}
DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid.c_str());
DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0);
DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive);
DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will.c_str());
#endif // ASYNC_TCP_SSL_ENABLED
_mqtt_connecting = true;
DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid);
DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0);
DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive);
DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will);
#if SECURE_CLIENT != SECURE_CLIENT_NONE
const bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1;
#else
const bool secure = false;
#endif
_mqtt.connect();
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
_mqttSetupAsyncClient(secure);
#elif (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT)
if (_mqttSetupSyncClient(secure) && _mqttConnectSyncClient(secure)) {
_mqttOnConnect();
} else {
DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n"));
_mqttOnDisconnect();
}
#else
#error "please check that MQTT_LIBRARY is valid"
#endif
#else // not MQTT_USE_ASYNC
}
bool response = true;
void _mqttPlaceholders(String& text) {
#if ASYNC_TCP_SSL_ENABLED
text.replace("{hostname}", getSetting("hostname"));
text.replace("{magnitude}", "#");
bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1;
if (secure) {
DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n"));
if (_mqtt_client_secure.connect(host, port)) {
char fp[60] = {0};
if (sslFingerPrintChar(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) {
if (_mqtt_client_secure.verify(fp, host)) {
_mqtt.setClient(_mqtt_client_secure);
} else {
DEBUG_MSG_P(PSTR("[MQTT] Invalid fingerprint\n"));
response = false;
}
_mqtt_client_secure.stop();
yield();
} else {
DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n"));
response = false;
}
} else {
DEBUG_MSG_P(PSTR("[MQTT] Client connection failed\n"));
response = false;
}
String mac = WiFi.macAddress();
mac.replace(":", "");
text.replace("{mac}", mac);
} else {
_mqtt.setClient(_mqtt_client);
}
}
#else // not ASYNC_TCP_SSL_ENABLED
template<typename T>
void _mqttApplySetting(T& current, T& updated) {
if (current != updated) {
current = std::move(updated);
mqttDisconnect();
}
}
_mqtt.setClient(_mqtt_client);
template<typename T>
void _mqttApplySetting(T& current, const T& updated) {
if (current != updated) {
current = updated;
mqttDisconnect();
}
}
#endif // ASYNC_TCP_SSL_ENABLED
template<typename T>
void _mqttApplyTopic(T& current, const char* magnitude) {
String updated = mqttTopic(magnitude, false);
if (current != updated) {
mqttFlush();
current = std::move(updated);
}
}
if (response) {
void _mqttConfigure() {
_mqtt.setServer(host, port);
// Enable only when server is set
{
String server = getSetting("mqttServer", MQTT_SERVER);
uint16_t port = getSetting("mqttPort", MQTT_PORT).toInt();
bool enabled = false;
if (server.length()) {
enabled = getSetting("mqttEnabled", MQTT_ENABLED).toInt() == 1;
}
if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) {
DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user);
response = _mqtt.connect(_mqtt_clientid, _mqtt_user, _mqtt_pass, _mqtt_will, _mqtt_qos, _mqtt_retain, "0");
} else {
response = _mqtt.connect(_mqtt_clientid, _mqtt_will, _mqtt_qos, _mqtt_retain, "0");
}
_mqttApplySetting(_mqtt_server, server);
_mqttApplySetting(_mqtt_enabled, enabled);
_mqttApplySetting(_mqtt_port, port);
DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid);
DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0);
DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive);
DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will);
if (!enabled) return;
}
}
// Get base topic and apply placeholders
{
String topic = getSetting("mqttTopic", MQTT_TOPIC);
if (topic.endsWith("/")) topic.remove(topic.length()-1);
if (response) {
_mqttOnConnect();
} else {
DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n"));
_mqtt_last_connection = millis();
}
// Replace things inside curly braces (like {hostname}, {mac} etc.)
_mqttPlaceholders(topic);
#endif // MQTT_USE_ASYNC
if (topic.indexOf("#") == -1) topic.concat("/#");
_mqttApplySetting(_mqtt_topic, topic);
free(host);
_mqttApplyTopic(_mqtt_will, MQTT_TOPIC_STATUS);
}
}
// Getter and setter
{
String setter = getSetting("mqttSetter", MQTT_SETTER);
String getter = getSetting("mqttGetter", MQTT_GETTER);
bool forward = !setter.equals(getter) && RELAY_REPORT_STATUS;
void _mqttPlaceholders(String *text) {
text->replace("{hostname}", getSetting("hostname"));
text->replace("{magnitude}", "#");
String mac = WiFi.macAddress();
mac.replace(":", "");
text->replace("{mac}", mac);
_mqttApplySetting(_mqtt_setter, setter);
_mqttApplySetting(_mqtt_getter, getter);
_mqttApplySetting(_mqtt_forward, forward);
}
}
// MQTT options
{
String user = getSetting("mqttUser", MQTT_USER);
_mqttPlaceholders(user);
void _mqttConfigure() {
String pass = getSetting("mqttPassword", MQTT_PASS);
// Get base topic
_mqtt_topic = getSetting("mqttTopic", MQTT_TOPIC);
if (_mqtt_topic.endsWith("/")) _mqtt_topic.remove(_mqtt_topic.length()-1);
unsigned char qos = getSetting("mqttQoS", MQTT_QOS).toInt();
bool retain = getSetting("mqttRetain", MQTT_RETAIN).toInt() == 1;
unsigned long keepalive = getSetting("mqttKeep", MQTT_KEEPALIVE).toInt();
// Placeholders
_mqttPlaceholders(&_mqtt_topic);
if (_mqtt_topic.indexOf("#") == -1) _mqtt_topic = _mqtt_topic + "/#";
String id = getSetting("mqttClientID", getIdentifier());
_mqttPlaceholders(id);
// Getters and setters
_mqtt_setter = getSetting("mqttSetter", MQTT_SETTER);
_mqtt_getter = getSetting("mqttGetter", MQTT_GETTER);
_mqtt_forward = !_mqtt_getter.equals(_mqtt_setter) && RELAY_REPORT_STATUS;
_mqttApplySetting(_mqtt_user, user);
_mqttApplySetting(_mqtt_pass, pass);
_mqttApplySetting(_mqtt_qos, qos);
_mqttApplySetting(_mqtt_retain, retain);
_mqttApplySetting(_mqtt_keepalive, keepalive);
_mqttApplySetting(_mqtt_clientid, id);
}
// MQTT options
_mqtt_qos = getSetting("mqttQoS", MQTT_QOS).toInt();
_mqtt_retain = getSetting("mqttRetain", MQTT_RETAIN).toInt() == 1;
_mqtt_keepalive = getSetting("mqttKeep", MQTT_KEEPALIVE).toInt();
if (getSetting("mqttClientID").length() == 0) delSetting("mqttClientID");
// Enable
if (getSetting("mqttServer", MQTT_SERVER).length() == 0) {
mqttEnabled(false);
} else {
_mqtt_enabled = getSetting("mqttEnabled", MQTT_ENABLED).toInt() == 1;
// MQTT JSON
{
_mqttApplySetting(_mqtt_use_json, getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1);
_mqttApplyTopic(_mqtt_topic_json, MQTT_TOPIC_JSON);
} }
_mqtt_use_json = (getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1);
mqttQueueTopic(MQTT_TOPIC_JSON);
// Custom payload strings
settingsProcessConfig({
{_mqtt_payload_online, "mqttPayloadOnline", MQTT_STATUS_ONLINE},
{_mqtt_payload_offline, "mqttPayloadOffline", MQTT_STATUS_OFFLINE}
});
// Reset reconnect delay to reconnect sooner
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
} }
@ -267,19 +379,65 @@ void _mqttBackwards() {
} }
} }
void _mqttInfo() {
DEBUG_MSG_P(PSTR(
"[MQTT] "
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
"AsyncMqttClient"
#elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
"Arduino-MQTT"
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
"PubSubClient"
#endif
", SSL "
#if SECURE_CLIENT != SEURE_CLIENT_NONE
"ENABLED"
#else
"DISABLED"
#endif
", Autoconnect "
#if MQTT_AUTOCONNECT
"ENABLED"
#else
"DISABLED"
#endif
"\n"
));
DEBUG_MSG_P(PSTR("[MQTT] Client %s, %s\n"),
_mqtt_enabled ? "ENABLED" : "DISABLED",
_mqtt.connected() ? "CONNECTED" : "DISCONNECTED"
);
DEBUG_MSG_P(PSTR("[MQTT] Retry %s (Now %u, Last %u, Delay %u, Step %u)\n"),
_mqtt_connecting ? "CONNECTING" : "WAITING",
millis(),
_mqtt_last_connection,
_mqtt_reconnect_delay,
MQTT_RECONNECT_DELAY_STEP
);
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WEB // WEB
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if WEB_SUPPORT #if WEB_SUPPORT
bool _mqttWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _mqttWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "mqtt", 3) == 0); return (strncmp(key, "mqtt", 3) == 0);
} }
void _mqttWebSocketOnSend(JsonObject& root) {
void _mqttWebSocketOnVisible(JsonObject& root) {
root["mqttVisible"] = 1; root["mqttVisible"] = 1;
#if ASYNC_TCP_SSL_ENABLED
root["mqttsslVisible"] = 1;
#endif
}
void _mqttWebSocketOnData(JsonObject& root) {
root["mqttStatus"] = mqttConnected(); root["mqttStatus"] = mqttConnected();
}
void _mqttWebSocketOnConnected(JsonObject& root) {
root["mqttEnabled"] = mqttEnabled(); root["mqttEnabled"] = mqttEnabled();
root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER); root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER);
root["mqttPort"] = getSetting("mqttPort", MQTT_PORT); root["mqttPort"] = getSetting("mqttPort", MQTT_PORT);
@ -289,8 +447,7 @@ void _mqttWebSocketOnSend(JsonObject& root) {
root["mqttKeep"] = _mqtt_keepalive; root["mqttKeep"] = _mqtt_keepalive;
root["mqttRetain"] = _mqtt_retain; root["mqttRetain"] = _mqtt_retain;
root["mqttQoS"] = _mqtt_qos; root["mqttQoS"] = _mqtt_qos;
#if ASYNC_TCP_SSL_ENABLED
root["mqttsslVisible"] = 1;
#if SECURE_CLIENT != SECURE_CLIENT_NONE
root["mqttUseSSL"] = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; root["mqttUseSSL"] = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1;
root["mqttFP"] = getSetting("mqttFP", MQTT_SSL_FINGERPRINT); root["mqttFP"] = getSetting("mqttFP", MQTT_SSL_FINGERPRINT);
#endif #endif
@ -314,6 +471,11 @@ void _mqttInitCommands() {
terminalOK(); terminalOK();
}); });
terminalRegisterCommand(F("MQTT.INFO"), [](Embedis* e) {
_mqttInfo();
terminalOK();
});
} }
#endif // TERMINAL_SUPPORT #endif // TERMINAL_SUPPORT
@ -356,6 +518,8 @@ void _mqttOnConnect() {
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
_mqtt_last_connection = millis(); _mqtt_last_connection = millis();
_mqtt_connecting = false;
_mqtt_connected = true;
// Clean subscriptions // Clean subscriptions
mqttUnsubscribeRaw("#"); mqttUnsubscribeRaw("#");
@ -372,6 +536,7 @@ void _mqttOnDisconnect() {
// Reset reconnection delay // Reset reconnection delay
_mqtt_last_connection = millis(); _mqtt_last_connection = millis();
_mqtt_connecting = false; _mqtt_connecting = false;
_mqtt_connected = false;
DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n"));
@ -469,10 +634,13 @@ String mqttTopic(const char * magnitude, unsigned int index, bool is_set) {
void mqttSendRaw(const char * topic, const char * message, bool retain) { void mqttSendRaw(const char * topic, const char * message, bool retain) {
if (_mqtt.connected()) { if (_mqtt.connected()) {
#if MQTT_USE_ASYNC
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
unsigned int packetId = _mqtt.publish(topic, _mqtt_qos, retain, message); unsigned int packetId = _mqtt.publish(topic, _mqtt_qos, retain, message);
DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s (PID %d)\n"), topic, message, packetId); DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s (PID %d)\n"), topic, message, packetId);
#else
#elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
_mqtt.publish(topic, message, retain, _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message);
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
_mqtt.publish(topic, message, retain); _mqtt.publish(topic, message, retain);
DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message); DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message);
#endif #endif
@ -491,9 +659,6 @@ void mqttSend(const char * topic, const char * message, bool force, bool retain)
// Equeue message // Equeue message
if (useJson) { if (useJson) {
// Set default queue topic
mqttQueueTopic(MQTT_TOPIC_JSON);
// Enqueue new message // Enqueue new message
mqttEnqueue(topic, message); mqttEnqueue(topic, message);
@ -568,9 +733,9 @@ void mqttFlush() {
if (_mqtt_queue.size() == 0) return; if (_mqtt_queue.size() == 0) return;
// Build tree recursively // Build tree recursively
DynamicJsonBuffer jsonBuffer;
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
_mqttBuildTree(root, 255);
_mqttBuildTree(root, mqtt_message_t::END);
// Add extra propeties // Add extra propeties
#if NTP_SUPPORT && MQTT_ENQUEUE_DATETIME #if NTP_SUPPORT && MQTT_ENQUEUE_DATETIME
@ -608,14 +773,6 @@ void mqttFlush() {
} }
void mqttQueueTopic(const char * topic) {
String t = mqttTopic(topic, false);
if (!t.equals(_mqtt_topic_json)) {
mqttFlush();
_mqtt_topic_json = t;
}
}
int8_t mqttEnqueue(const char * topic, const char * message, unsigned char parent) { int8_t mqttEnqueue(const char * topic, const char * message, unsigned char parent) {
// Queue is not meant to send message "offline" // Queue is not meant to send message "offline"
@ -641,17 +798,17 @@ int8_t mqttEnqueue(const char * topic, const char * message, unsigned char paren
} }
int8_t mqttEnqueue(const char * topic, const char * message) { int8_t mqttEnqueue(const char * topic, const char * message) {
return mqttEnqueue(topic, message, 255);
return mqttEnqueue(topic, message, mqtt_message_t::END);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void mqttSubscribeRaw(const char * topic) { void mqttSubscribeRaw(const char * topic) {
if (_mqtt.connected() && (strlen(topic) > 0)) { if (_mqtt.connected() && (strlen(topic) > 0)) {
#if MQTT_USE_ASYNC
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
unsigned int packetId = _mqtt.subscribe(topic, _mqtt_qos); unsigned int packetId = _mqtt.subscribe(topic, _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s (PID %d)\n"), topic, packetId); DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s (PID %d)\n"), topic, packetId);
#else
#else // Arduino-MQTT or PubSubClient
_mqtt.subscribe(topic, _mqtt_qos); _mqtt.subscribe(topic, _mqtt_qos);
DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s\n"), topic); DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s\n"), topic);
#endif #endif
@ -664,10 +821,10 @@ void mqttSubscribe(const char * topic) {
void mqttUnsubscribeRaw(const char * topic) { void mqttUnsubscribeRaw(const char * topic) {
if (_mqtt.connected() && (strlen(topic) > 0)) { if (_mqtt.connected() && (strlen(topic) > 0)) {
#if MQTT_USE_ASYNC
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
unsigned int packetId = _mqtt.unsubscribe(topic); unsigned int packetId = _mqtt.unsubscribe(topic);
DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)\n"), topic, packetId); DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)\n"), topic, packetId);
#else
#else // Arduino-MQTT or PubSubClient
_mqtt.unsubscribe(topic); _mqtt.unsubscribe(topic);
DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s\n"), topic); DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s\n"), topic);
#endif #endif
@ -709,7 +866,11 @@ void mqttRegister(mqtt_callback_f callback) {
void mqttSetBroker(IPAddress ip, unsigned int port) { void mqttSetBroker(IPAddress ip, unsigned int port) {
setSetting("mqttServer", ip.toString()); setSetting("mqttServer", ip.toString());
_mqtt_server = ip.toString();
setSetting("mqttPort", port); setSetting("mqttPort", port);
_mqtt_port = port;
mqttEnabled(MQTT_AUTOCONNECT); mqttEnabled(MQTT_AUTOCONNECT);
} }
@ -717,9 +878,20 @@ void mqttSetBrokerIfNone(IPAddress ip, unsigned int port) {
if (getSetting("mqttServer", MQTT_SERVER).length() == 0) mqttSetBroker(ip, port); if (getSetting("mqttServer", MQTT_SERVER).length() == 0) mqttSetBroker(ip, port);
} }
void mqttReset() {
_mqttConfigure();
mqttDisconnect();
const String& mqttPayloadOnline() {
return _mqtt_payload_online;
}
const String& mqttPayloadOffline() {
return _mqtt_payload_offline;
}
const char* mqttPayloadStatus(bool status) {
return status ? _mqtt_payload_online.c_str() : _mqtt_payload_offline.c_str();
}
void mqttSendStatus() {
mqttSend(MQTT_TOPIC_STATUS, _mqtt_payload_online.c_str(), true);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -729,14 +901,22 @@ void mqttReset() {
void mqttSetup() { void mqttSetup() {
_mqttBackwards(); _mqttBackwards();
DEBUG_MSG_P(PSTR("[MQTT] Async %s, SSL %s, Autoconnect %s\n"),
MQTT_USE_ASYNC ? "ENABLED" : "DISABLED",
ASYNC_TCP_SSL_ENABLED ? "ENABLED" : "DISABLED",
MQTT_AUTOCONNECT ? "ENABLED" : "DISABLED"
);
#if MQTT_USE_ASYNC
_mqttInfo();
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
// XXX: should not place this in config, addServerFingerprint does not check for duplicates
#if SECURE_CLIENT != SECURE_CLIENT_NONE
{
if (_mqtt_sc_config.on_fingerprint) {
const String fingerprint = _mqtt_sc_config.on_fingerprint();
uint8_t buffer[20] = {0};
if (sslFingerPrintArray(fingerprint.c_str(), buffer)) {
_mqtt.addServerFingerprint(buffer);
}
}
}
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
_mqtt.onConnect([](bool sessionPresent) { _mqtt.onConnect([](bool sessionPresent) {
_mqttOnConnect(); _mqttOnConnect();
@ -757,7 +937,7 @@ void mqttSetup() {
if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
DEBUG_MSG_P(PSTR("[MQTT] Not authorized\n")); DEBUG_MSG_P(PSTR("[MQTT] Not authorized\n"));
} }
#if ASYNC_TCP_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
if (reason == AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT) { if (reason == AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT) {
DEBUG_MSG_P(PSTR("[MQTT] Bad fingerprint\n")); DEBUG_MSG_P(PSTR("[MQTT] Bad fingerprint\n"));
} }
@ -774,20 +954,33 @@ void mqttSetup() {
DEBUG_MSG_P(PSTR("[MQTT] Publish ACK for PID %d\n"), packetId); DEBUG_MSG_P(PSTR("[MQTT] Publish ACK for PID %d\n"), packetId);
}); });
#else // not MQTT_USE_ASYNC
#elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT
_mqtt.onMessageAdvanced([](MQTTClient *client, char topic[], char payload[], int length) {
_mqttOnMessage(topic, payload, length);
});
#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT
_mqtt.setCallback([](char* topic, byte* payload, unsigned int length) { _mqtt.setCallback([](char* topic, byte* payload, unsigned int length) {
_mqttOnMessage(topic, (char *) payload, length); _mqttOnMessage(topic, (char *) payload, length);
}); });
#endif // MQTT_USE_ASYNC
#endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
_mqttConfigure(); _mqttConfigure();
mqttRegister(_mqttCallback); mqttRegister(_mqttCallback);
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_mqttWebSocketOnSend);
wsOnReceiveRegister(_mqttWebSocketOnReceive);
wsRegister()
.onVisible(_mqttWebSocketOnVisible)
.onData(_mqttWebSocketOnData)
.onConnected(_mqttWebSocketOnConnected)
.onKeyCheck(_mqttWebSocketOnKeyCheck);
mqttRegister([](unsigned int type, const char*, const char*) {
if ((type == MQTT_CONNECT_EVENT) || (type == MQTT_DISCONNECT_EVENT)) wsPost(_mqttWebSocketOnData);
});
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
@ -804,11 +997,11 @@ void mqttLoop() {
if (WiFi.status() != WL_CONNECTED) return; if (WiFi.status() != WL_CONNECTED) return;
#if MQTT_USE_ASYNC
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
_mqttConnect(); _mqttConnect();
#else // not MQTT_USE_ASYNC
#else // MQTT_LIBRARY != MQTT_LIBRARY_ASYNCMQTTCLIENT
if (_mqtt.connected()) { if (_mqtt.connected()) {
@ -818,14 +1011,13 @@ void mqttLoop() {
if (_mqtt_connected) { if (_mqtt_connected) {
_mqttOnDisconnect(); _mqttOnDisconnect();
_mqtt_connected = false;
} }
_mqttConnect(); _mqttConnect();
} }
#endif
#endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
} }


+ 18
- 9
code/espurna/nofuss.ino View File

@ -20,12 +20,15 @@ bool _nofussEnabled = false;
#if WEB_SUPPORT #if WEB_SUPPORT
bool _nofussWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _nofussWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "nofuss", 6) == 0); return (strncmp(key, "nofuss", 6) == 0);
} }
void _nofussWebSocketOnSend(JsonObject& root) {
void _nofussWebSocketOnVisible(JsonObject& root) {
root["nofussVisible"] = 1; root["nofussVisible"] = 1;
}
void _nofussWebSocketOnConnected(JsonObject& root) {
root["nofussEnabled"] = getSetting("nofussEnabled", NOFUSS_ENABLED).toInt() == 1; root["nofussEnabled"] = getSetting("nofussEnabled", NOFUSS_ENABLED).toInt() == 1;
root["nofussServer"] = getSetting("nofussServer", NOFUSS_SERVER); root["nofussServer"] = getSetting("nofussServer", NOFUSS_SERVER);
} }
@ -54,16 +57,15 @@ void _nofussConfigure() {
} else { } else {
char buffer[20];
snprintf_P(buffer, sizeof(buffer), PSTR("%s-%s"), APP_NAME, DEVICE);
NoFUSSClient.setServer(nofussServer); NoFUSSClient.setServer(nofussServer);
NoFUSSClient.setDevice(buffer);
NoFUSSClient.setDevice(APP_NAME "_" DEVICE);
NoFUSSClient.setVersion(APP_VERSION); NoFUSSClient.setVersion(APP_VERSION);
NoFUSSClient.setBuild(String(__UNIX_TIMESTAMP__));
DEBUG_MSG_P(PSTR("[NOFUSS] Server : %s\n"), nofussServer.c_str()); DEBUG_MSG_P(PSTR("[NOFUSS] Server : %s\n"), nofussServer.c_str());
DEBUG_MSG_P(PSTR("[NOFUSS] Dervice: %s\n"), buffer);
DEBUG_MSG_P(PSTR("[NOFUSS] Dervice: %s\n"), APP_NAME "_" DEVICE);
DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), APP_VERSION); DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), APP_VERSION);
DEBUG_MSG_P(PSTR("[NOFUSS] Build: %s\n"), String(__UNIX_TIMESTAMP__).c_str());
DEBUG_MSG_P(PSTR("[NOFUSS] Enabled\n")); DEBUG_MSG_P(PSTR("[NOFUSS] Enabled\n"));
} }
@ -123,6 +125,9 @@ void nofussSetup() {
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false); eepromRotate(false);
// Force backup right now, because NoFUSS library will immediatly reset on success
eepromBackup(0);
} }
if (code == NOFUSS_FILESYSTEM_UPDATE_ERROR) { if (code == NOFUSS_FILESYSTEM_UPDATE_ERROR) {
@ -146,6 +151,8 @@ void nofussSetup() {
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend_P(PSTR("{\"action\": \"reload\"}")); wsSend_P(PSTR("{\"action\": \"reload\"}"));
#endif #endif
// TODO: NoFUSS will reset the board after this callback returns.
// Maybe this should be optional
nice_delay(100); nice_delay(100);
} }
@ -157,8 +164,10 @@ void nofussSetup() {
}); });
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_nofussWebSocketOnSend);
wsOnReceiveRegister(_nofussWebSocketOnReceive);
wsRegister()
.onVisible(_nofussWebSocketOnVisible)
.onConnected(_nofussWebSocketOnConnected)
.onKeyCheck(_nofussWebSocketOnKeyCheck);
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT


+ 16
- 11
code/espurna/ntp.ino View File

@ -26,13 +26,19 @@ bool _ntp_want_sync = false;
#if WEB_SUPPORT #if WEB_SUPPORT
bool _ntpWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "ntp", 3) == 0); return (strncmp(key, "ntp", 3) == 0);
} }
void _ntpWebSocketOnSend(JsonObject& root) {
void _ntpWebSocketOnVisible(JsonObject& root) {
root["ntpVisible"] = 1; root["ntpVisible"] = 1;
}
void _ntpWebSocketOnData(JsonObject& root) {
root["ntpStatus"] = (timeStatus() == timeSet); root["ntpStatus"] = (timeStatus() == timeSet);
}
void _ntpWebSocketOnConnected(JsonObject& root) {
root["ntpServer"] = getSetting("ntpServer", NTP_SERVER); root["ntpServer"] = getSetting("ntpServer", NTP_SERVER);
root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET).toInt(); root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET).toInt();
root["ntpDST"] = getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1; root["ntpDST"] = getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1;
@ -116,10 +122,6 @@ void _ntpReport() {
_ntp_report = false; _ntp_report = false;
#if WEB_SUPPORT
wsSend(_ntpWebSocketOnSend);
#endif
if (ntpSynced()) { if (ntpSynced()) {
time_t t = now(); time_t t = now();
DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str()); DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str());
@ -233,14 +235,14 @@ void ntpSetup() {
NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) { NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) {
if (error) { if (error) {
#if WEB_SUPPORT
wsSend_P(PSTR("{\"ntpStatus\": false}"));
#endif
if (error == noResponse) { if (error == noResponse) {
DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n")); DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n"));
} else if (error == invalidAddress) { } else if (error == invalidAddress) {
DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n")); DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n"));
} }
#if WEB_SUPPORT
wsPost(_ntpWebSocketOnData);
#endif
} else { } else {
_ntp_report = true; _ntp_report = true;
setTime(NTPw.getLastNTPSync()); setTime(NTPw.getLastNTPSync());
@ -256,8 +258,11 @@ void ntpSetup() {
}); });
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_ntpWebSocketOnSend);
wsOnReceiveRegister(_ntpWebSocketOnReceive);
wsRegister()
.onVisible(_ntpWebSocketOnVisible)
.onConnected(_ntpWebSocketOnConnected)
.onData(_ntpWebSocketOnData)
.onKeyCheck(_ntpWebSocketOnKeyCheck);
#endif #endif
// Main callbacks // Main callbacks


+ 0
- 295
code/espurna/ota.ino View File

@ -1,295 +0,0 @@
/*
OTA MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "ArduinoOTA.h"
// -----------------------------------------------------------------------------
// Arduino OTA
// -----------------------------------------------------------------------------
void _otaConfigure() {
ArduinoOTA.setPort(OTA_PORT);
ArduinoOTA.setHostname(getSetting("hostname").c_str());
#if USE_PASSWORD
ArduinoOTA.setPassword(getAdminPass().c_str());
#endif
}
void _otaLoop() {
ArduinoOTA.handle();
}
// -----------------------------------------------------------------------------
// Terminal OTA
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT
#include <ESPAsyncTCP.h>
AsyncClient * _ota_client;
char * _ota_host;
char * _ota_url;
unsigned int _ota_port = 80;
unsigned long _ota_size = 0;
const char OTA_REQUEST_TEMPLATE[] PROGMEM =
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: ESPurna\r\n"
"Connection: close\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 0\r\n\r\n\r\n";
void _otaFrom(const char * host, unsigned int port, const char * url) {
if (_ota_host) free(_ota_host);
if (_ota_url) free(_ota_url);
_ota_host = strdup(host);
_ota_url = strdup(url);
_ota_port = port;
_ota_size = 0;
if (_ota_client == NULL) {
_ota_client = new AsyncClient();
}
_ota_client->onDisconnect([](void *s, AsyncClient *c) {
DEBUG_MSG_P(PSTR("\n"));
if (Update.end(true)){
DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), _ota_size);
deferredReset(100, CUSTOM_RESET_OTA);
} else {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
eepromRotate(true);
}
DEBUG_MSG_P(PSTR("[OTA] Disconnected\n"));
_ota_client->free();
delete _ota_client;
_ota_client = NULL;
free(_ota_host);
_ota_host = NULL;
free(_ota_url);
_ota_url = NULL;
}, 0);
_ota_client->onTimeout([](void *s, AsyncClient *c, uint32_t time) {
_ota_client->close(true);
}, 0);
_ota_client->onData([](void * arg, AsyncClient * c, void * data, size_t len) {
char * p = (char *) data;
if (_ota_size == 0) {
Update.runAsync(true);
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
}
p = strstr((char *)data, "\r\n\r\n") + 4;
len = len - (p - (char *) data);
}
if (!Update.hasError()) {
if (Update.write((uint8_t *) p, len) != len) {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
}
}
_ota_size += len;
DEBUG_MSG_P(PSTR("[OTA] Progress: %u bytes\r"), _ota_size);
delay(0);
}, NULL);
_ota_client->onConnect([](void * arg, AsyncClient * client) {
#if ASYNC_TCP_SSL_ENABLED
if (443 == _ota_port) {
uint8_t fp[20] = {0};
sslFingerPrintArray(getSetting("otafp", OTA_GITHUB_FP).c_str(), fp);
SSL * ssl = _ota_client->getSSL();
if (ssl_match_fingerprint(ssl, fp) != SSL_OK) {
DEBUG_MSG_P(PSTR("[OTA] Warning: certificate doesn't match\n"));
}
}
#endif
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url);
char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + strlen(_ota_url) + strlen(_ota_host)];
snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url, _ota_host);
client->write(buffer);
}, NULL);
#if ASYNC_TCP_SSL_ENABLED
bool connected = _ota_client->connect(host, port, 443 == port);
#else
bool connected = _ota_client->connect(host, port);
#endif
if (!connected) {
DEBUG_MSG_P(PSTR("[OTA] Connection failed\n"));
_ota_client->close(true);
}
}
void _otaFrom(String url) {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n"));
return;
}
// Port from protocol
unsigned int port = 80;
if (url.startsWith("https://")) port = 443;
url = url.substring(url.indexOf("/") + 2);
// Get host
String host = url.substring(0, url.indexOf("/"));
// Explicit port
int p = host.indexOf(":");
if (p > 0) {
port = host.substring(p + 1).toInt();
host = host.substring(0, p);
}
// Get URL
String uri = url.substring(url.indexOf("/"));
_otaFrom(host.c_str(), port, uri.c_str());
}
#endif // TERMINAL_SUPPORT || OTA_MQTT_SUPPORT
#if TERMINAL_SUPPORT
void _otaInitCommands() {
terminalRegisterCommand(F("OTA"), [](Embedis* e) {
if (e->argc < 2) {
terminalError(F("Wrong arguments"));
} else {
terminalOK();
String url = String(e->argv[1]);
_otaFrom(url);
}
});
}
#endif // TERMINAL_SUPPORT
#if OTA_MQTT_SUPPORT
void _otaMQTTCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_OTA);
}
if (type == MQTT_MESSAGE_EVENT) {
// Match topic
String t = mqttMagnitude((char *) topic);
if (t.equals(MQTT_TOPIC_OTA)) {
DEBUG_MSG_P(PSTR("[OTA] Initiating from URL: %s\n"), payload);
_otaFrom(payload);
}
}
}
#endif // OTA_MQTT_SUPPORT
// -----------------------------------------------------------------------------
void otaSetup() {
_otaConfigure();
#if TERMINAL_SUPPORT
_otaInitCommands();
#endif
#if OTA_MQTT_SUPPORT
mqttRegister(_otaMQTTCallback);
#endif
// Main callbacks
espurnaRegisterLoop(_otaLoop);
espurnaRegisterReload(_otaConfigure);
// -------------------------------------------------------------------------
ArduinoOTA.onStart([]() {
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Start\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"message\": 2}"));
#endif
});
ArduinoOTA.onEnd([]() {
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[OTA] Done, restarting...\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"action\": \"reload\"}"));
#endif
deferredReset(100, CUSTOM_RESET_OTA);
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog);
_progOld = _prog;
}
});
ArduinoOTA.onError([](ota_error_t error) {
#if DEBUG_SUPPORT
DEBUG_MSG_P(PSTR("\n[OTA] Error #%u: "), error);
if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n"));
else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n"));
else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n"));
else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n"));
else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n"));
#endif
eepromRotate(true);
});
ArduinoOTA.begin();
}

+ 101
- 0
code/espurna/ota_arduinoota.ino View File

@ -0,0 +1,101 @@
/*
ARDUINO OTA MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if OTA_ARDUINOOTA_SUPPORT
// TODO: allocate ArduinoOTAClass on-demand, stop using global instance
void _arduinoOtaConfigure() {
ArduinoOTA.setPort(OTA_PORT);
ArduinoOTA.setHostname(getSetting("hostname").c_str());
#if USE_PASSWORD
ArduinoOTA.setPassword(getAdminPass().c_str());
#endif
ArduinoOTA.begin();
}
void _arduinoOtaLoop() {
ArduinoOTA.handle();
}
void _arduinoOtaOnStart() {
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
// Because ArduinoOTA is synchronous, force backup right now instead of waiting for the next loop()
eepromBackup(0);
DEBUG_MSG_P(PSTR("[OTA] Start\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"message\": 2}"));
#endif
}
void _arduinoOtaOnEnd() {
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[OTA] Done, restarting...\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"action\": \"reload\"}"));
#endif
deferredReset(100, CUSTOM_RESET_OTA);
}
void _arduinoOtaOnProgress(unsigned int progress, unsigned int total) {
// Removed to avoid websocket ping back during upgrade (see #1574)
// TODO: implement as separate from debugging message
#if WEB_SUPPORT
if (wsConnected()) return;
#endif
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog);
_progOld = _prog;
}
}
void _arduinoOtaOnError(ota_error_t error) {
#if DEBUG_SUPPORT
DEBUG_MSG_P(PSTR("\n[OTA] Error #%u: "), error);
if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n"));
else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n"));
else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n"));
else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n"));
else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n"));
#endif
eepromRotate(true);
}
void arduinoOtaSetup() {
espurnaRegisterLoop(_arduinoOtaLoop);
espurnaRegisterReload(_arduinoOtaConfigure);
ArduinoOTA.onStart(_arduinoOtaOnStart);
ArduinoOTA.onEnd(_arduinoOtaOnEnd);
ArduinoOTA.onError(_arduinoOtaOnError);
ArduinoOTA.onProgress(_arduinoOtaOnProgress);
_arduinoOtaConfigure();
}
#endif // OTA_ARDUINOOTA_SUPPORT

+ 227
- 0
code/espurna/ota_asynctcp.ino View File

@ -0,0 +1,227 @@
/*
ASYNC CLIENT OTA MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if OTA_CLIENT == OTA_CLIENT_ASYNCTCP
// -----------------------------------------------------------------------------
// Terminal OTA command
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT
#include <ESPAsyncTCP.h>
#include "libs/URL.h"
std::unique_ptr<AsyncClient> _ota_client = nullptr;
unsigned long _ota_size = 0;
bool _ota_connected = false;
std::unique_ptr<URL> _ota_url = nullptr;
const char OTA_REQUEST_TEMPLATE[] PROGMEM =
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: ESPurna\r\n"
"Connection: close\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 0\r\n\r\n\r\n";
void _otaClientOnDisconnect(void *s, AsyncClient *c) {
DEBUG_MSG_P(PSTR("\n"));
if (Update.end(true)){
DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), _ota_size);
deferredReset(100, CUSTOM_RESET_OTA);
} else {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
eepromRotate(true);
}
DEBUG_MSG_P(PSTR("[OTA] Disconnected\n"));
_ota_connected = false;
_ota_url = nullptr;
_ota_client = nullptr;
}
void _otaClientOnTimeout(void *s, AsyncClient *c, uint32_t time) {
_ota_connected = false;
_ota_url = nullptr;
_ota_client->close(true);
}
void _otaClientOnData(void * arg, AsyncClient * c, void * data, size_t len) {
char * p = (char *) data;
if (_ota_size == 0) {
Update.runAsync(true);
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
c->close(true);
return;
}
p = strstr((char *)data, "\r\n\r\n") + 4;
len = len - (p - (char *) data);
}
if (!Update.hasError()) {
if (Update.write((uint8_t *) p, len) != len) {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
c->close(true);
return;
}
}
_ota_size += len;
DEBUG_MSG_P(PSTR("[OTA] Progress: %u bytes\r"), _ota_size);
delay(0);
}
void _otaClientOnConnect(void *arg, AsyncClient *client) {
#if ASYNC_TCP_SSL_ENABLED
int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt();
if ((check == SECURE_CLIENT_CHECK_FINGERPRINT) && (443 == _ota_url->port)) {
uint8_t fp[20] = {0};
sslFingerPrintArray(getSetting("otafp", OTA_FINGERPRINT).c_str(), fp);
SSL * ssl = _ota_client->getSSL();
if (ssl_match_fingerprint(ssl, fp) != SSL_OK) {
DEBUG_MSG_P(PSTR("[OTA] Warning: certificate fingerpint doesn't match\n"));
client->close(true);
return;
}
}
#endif
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url->path.c_str());
char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + _ota_url->path.length() + _ota_url->host.length()];
snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url->path.c_str(), _ota_url->host.c_str());
client->write(buffer);
}
void _otaClientFrom(const String& url) {
if (_ota_connected) {
DEBUG_MSG_P(PSTR("[OTA] Already connected\n"));
return;
}
_ota_size = 0;
if (_ota_url) _ota_url = nullptr;
_ota_url = std::make_unique<URL>(url);
/*
DEBUG_MSG_P(PSTR("[OTA] proto:%s host:%s port:%u path:%s\n"),
_ota_url->protocol.c_str(),
_ota_url->host.c_str(),
_ota_url->port,
_ota_url->path.c_str()
);
*/
// we only support HTTP
if ((!_ota_url->protocol.equals("http")) && (!_ota_url->protocol.equals("https"))) {
DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n"));
_ota_url = nullptr;
return;
}
if (!_ota_client) {
_ota_client = std::make_unique<AsyncClient>();
}
_ota_client->onDisconnect(_otaClientOnDisconnect, nullptr);
_ota_client->onTimeout(_otaClientOnTimeout, nullptr);
_ota_client->onData(_otaClientOnData, nullptr);
_ota_client->onConnect(_otaClientOnConnect, nullptr);
#if ASYNC_TCP_SSL_ENABLED
_ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port, 443 == _ota_url->port);
#else
_ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port);
#endif
if (!_ota_connected) {
DEBUG_MSG_P(PSTR("[OTA] Connection failed\n"));
_ota_url = nullptr;
_ota_client->close(true);
}
}
#endif // TERMINAL_SUPPORT || OTA_MQTT_SUPPORT
#if TERMINAL_SUPPORT
void _otaClientInitCommands() {
terminalRegisterCommand(F("OTA"), [](Embedis* e) {
if (e->argc < 2) {
terminalError(F("OTA <url>"));
} else {
_otaClientFrom(String(e->argv[1]));
terminalOK();
}
});
}
#endif // TERMINAL_SUPPORT
#if OTA_MQTT_SUPPORT
void _otaClientMqttCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_OTA);
}
if (type == MQTT_MESSAGE_EVENT) {
String t = mqttMagnitude((char *) topic);
if (t.equals(MQTT_TOPIC_OTA)) {
DEBUG_MSG_P(PSTR("[OTA] Initiating from URL: %s\n"), payload);
_otaClientFrom(payload);
}
}
}
#endif // OTA_MQTT_SUPPORT
// -----------------------------------------------------------------------------
void otaClientSetup() {
#if TERMINAL_SUPPORT
_otaClientInitCommands();
#endif
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)
mqttRegister(_otaClientMqttCallback);
#endif
}
#endif // OTA_CLIENT == OTA_CLIENT_ASYNCTCP

+ 264
- 0
code/espurna/ota_httpupdate.ino View File

@ -0,0 +1,264 @@
/*
HTTP(s) OTA MODULE
Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
// -----------------------------------------------------------------------------
// OTA by using Core's HTTP(s) updater
// -----------------------------------------------------------------------------
#if OTA_CLIENT == OTA_CLIENT_HTTPUPDATE
#include <memory>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include "libs/URL.h"
#if SECURE_CLIENT != SECURE_CLIENT_NONE
#if OTA_SECURE_CLIENT_INCLUDE_CA
#include "static/ota_client_trusted_root_ca.h"
#else
#include "static/digicert_evroot_pem.h"
#define _ota_client_trusted_root_ca _ssl_digicert_ev_root_ca
#endif
#endif // SECURE_CLIENT != SECURE_CLIENT_NONE
void _otaClientRunUpdater(WiFiClient* client, const String& url, const String& fp = "") {
UNUSED(client);
UNUSED(fp);
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Downloading %s ...\n"), url.c_str());
// TODO: support currentVersion (string arg after 'url')
// NOTE: ESPhttpUpdate.update(..., fp) will **always** fail with empty fingerprint
// NOTE: It is possible to support BearSSL with 2.4.2 by using uint8_t[20] instead of String for fingerprint argument
ESPhttpUpdate.rebootOnUpdate(false);
t_httpUpdate_return result = HTTP_UPDATE_NO_UPDATES;
// We expect both .update(url, "", String_fp) and .update(url) to survice until axTLS is removed from the Core
#if (SECURE_CLIENT == SECURE_CLIENT_AXTLS)
if (url.startsWith("https://")) {
result = ESPhttpUpdate.update(url, "", fp);
} else {
result = ESPhttpUpdate.update(url);
}
#elif OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE
result = ESPhttpUpdate.update(url);
#else
result = ESPhttpUpdate.update(*client, url);
#endif
switch (result) {
case HTTP_UPDATE_FAILED:
DEBUG_MSG_P(PSTR("[OTA] Update failed (error %d): %s\n"), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
eepromRotate(true);
break;
case HTTP_UPDATE_NO_UPDATES:
DEBUG_MSG_P(PSTR("[OTA] No updates"));
eepromRotate(true);
break;
case HTTP_UPDATE_OK:
DEBUG_MSG_P(PSTR("[OTA] Done, restarting..."));
deferredReset(500, CUSTOM_RESET_OTA); // wait a bit more than usual
break;
}
}
#if OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE
void _otaClientFromHttp(const String& url) {
_otaClientRunUpdater(nullptr, url, "");
}
#else
void _otaClientFromHttp(const String& url) {
auto client = std::make_unique<WiFiClient>();
_otaClientRunUpdater(client.get(), url, "");
}
#endif
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
void _otaClientFromHttps(const String& url) {
int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt();
bool settime = (check == SECURE_CLIENT_CHECK_CA);
if (!ntpSynced() && settime) {
DEBUG_MSG_P(PSTR("[OTA] Time not synced!\n"));
return;
}
// unique_ptr self-destructs after exiting function scope
// create WiFiClient on heap to use less stack space
auto client = std::make_unique<BearSSL::WiFiClientSecure>();
if (check == SECURE_CLIENT_CHECK_NONE) {
DEBUG_MSG_P(PSTR("[OTA] !!! Connection will not be validated !!!\n"));
client->setInsecure();
}
if (check == SECURE_CLIENT_CHECK_FINGERPRINT) {
String fp_string = getSetting("otafp", OTA_FINGERPRINT);
if (!fp_string.length()) {
DEBUG_MSG_P(PSTR("[OTA] Requested fingerprint auth, but 'otafp' is not set\n"));
return;
}
uint8_t fp_bytes[20] = {0};
sslFingerPrintArray(fp_string.c_str(), fp_bytes);
client->setFingerprint(fp_bytes);
}
BearSSL::X509List *ca = nullptr;
if (check == SECURE_CLIENT_CHECK_CA) {
ca = new BearSSL::X509List(_ota_client_trusted_root_ca);
// because we do not support libc methods of getting time, force client to use ntpclientlib's current time
// XXX: local2utc method use is detrimental when DST happening. now() should be utc
client->setX509Time(ntpLocal2UTC(now()));
client->setTrustAnchors(ca);
}
// TODO: RX and TX buffer sizes must be equal?
const uint16_t requested_mfln = getSetting("otaScMFLN", OTA_SECURE_CLIENT_MFLN).toInt();
switch (requested_mfln) {
// default, do nothing
case 0:
break;
// match valid sizes only
case 512:
case 1024:
case 2048:
case 4096:
{
client->setBufferSizes(requested_mfln, requested_mfln);
break;
}
default:
DEBUG_MSG_P(PSTR("[OTA] Warning: MFLN buffer size must be one of 512, 1024, 2048 or 4096\n"));
}
_otaClientRunUpdater(client.get(), url);
}
#endif // SECURE_CLIENT_BEARSSL
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
void _otaClientFromHttps(const String& url) {
const int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt();
String fp_string;
if (check == SECURE_CLIENT_CHECK_FINGERPRINT) {
fp_string = getSetting("otafp", OTA_FINGERPRINT);
if (!fp_string.length() || !sslCheckFingerPrint(fp_string.c_str())) {
DEBUG_MSG_P(PSTR("[OTA] Wrong fingerprint\n"));
return;
}
}
_otaClientRunUpdater(nullptr, url, fp_string);
}
#endif // SECURE_CLIENT_AXTLS
void _otaClientFrom(const String& url) {
if (url.startsWith("http://")) {
_otaClientFromHttp(url);
return;
}
#if SECURE_CLIENT != SECURE_CLIENT_NONE
if (url.startsWith("https://")) {
_otaClientFromHttps(url);
return;
}
#endif
DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n"));
}
#if TERMINAL_SUPPORT
void _otaClientInitCommands() {
terminalRegisterCommand(F("OTA"), [](Embedis* e) {
if (e->argc < 2) {
terminalError(F("OTA <url>"));
} else {
_otaClientFrom(String(e->argv[1]));
terminalOK();
}
});
}
#endif // TERMINAL_SUPPORT
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)
bool _ota_do_update = false;
String _ota_url;
void _otaClientLoop() {
if (_ota_do_update) {
_otaClientFrom(_ota_url);
_ota_do_update = false;
_ota_url = "";
}
}
void _otaClientMqttCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribe(MQTT_TOPIC_OTA);
}
if (type == MQTT_MESSAGE_EVENT) {
String t = mqttMagnitude((char *) topic);
if (t.equals(MQTT_TOPIC_OTA)) {
DEBUG_MSG_P(PSTR("[OTA] Queuing from URL: %s\n"), payload);
_ota_do_update = true;
_ota_url = payload;
}
}
}
#endif // MQTT_SUPPORT
// -----------------------------------------------------------------------------
void otaClientSetup() {
#if TERMINAL_SUPPORT
_otaClientInitCommands();
#endif
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)
mqttRegister(_otaClientMqttCallback);
espurnaRegisterLoop(_otaClientLoop);
#endif
}
#endif // OTA_CLIENT == OTA_CLIENT_HTTPUPDATE

+ 184
- 118
code/espurna/relay.ino View File

@ -28,6 +28,7 @@ typedef struct {
bool current_status; // Holds the current (physical) status of the relay bool current_status; // Holds the current (physical) status of the relay
bool target_status; // Holds the target status bool target_status; // Holds the target status
unsigned char lock; // Holds the value of target status, that cannot be changed afterwards. (0 for false, 1 for true, 2 to disable)
unsigned long fw_start; // Flood window start time unsigned long fw_start; // Flood window start time
unsigned char fw_count; // Number of changes within the current flood window unsigned char fw_count; // Number of changes within the current flood window
unsigned long change_time; // Scheduled time to change unsigned long change_time; // Scheduled time to change
@ -43,6 +44,44 @@ std::vector<relay_t> _relays;
bool _relayRecursive = false; bool _relayRecursive = false;
Ticker _relaySaveTicker; Ticker _relaySaveTicker;
#if MQTT_SUPPORT
String _relay_mqtt_payload_on;
String _relay_mqtt_payload_off;
String _relay_mqtt_payload_toggle;
#endif // MQTT_SUPPORT
// -----------------------------------------------------------------------------
// UTILITY
// -----------------------------------------------------------------------------
bool _relayHandlePayload(unsigned char relayID, const char* payload) {
auto value = relayParsePayload(payload);
if (value == RelayStatus::UNKNOWN) return false;
if (value == RelayStatus::OFF) {
relayStatus(relayID, false);
} else if (value == RelayStatus::ON) {
relayStatus(relayID, true);
} else if (value == RelayStatus::TOGGLE) {
relayToggle(relayID);
}
return true;
}
RelayStatus _relayStatusInvert(RelayStatus status) {
return (status == RelayStatus::ON) ? RelayStatus::OFF : status;
}
RelayStatus _relayStatusTyped(unsigned char id) {
if (id >= _relays.size()) return RelayStatus::OFF;
const bool status = _relays[id].current_status;
return (status) ? RelayStatus::ON : RelayStatus::OFF;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// RELAY PROVIDERS // RELAY PROVIDERS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -174,6 +213,23 @@ void _relayProcess(bool mode) {
// Only process the relays we have to change to the requested mode // Only process the relays we have to change to the requested mode
if (target != mode) continue; if (target != mode) continue;
// Only process the relays that can be changed
switch (_relays[id].lock) {
case RELAY_LOCK_ON:
case RELAY_LOCK_OFF:
{
bool lock = _relays[id].lock == 1;
if (lock != _relays[id].target_status) {
_relays[id].target_status = lock;
continue;
}
break;
}
case RELAY_LOCK_DISABLED:
default:
break;
}
// Only process if the change_time has arrived // Only process if the change_time has arrived
if (current_time < _relays[id].change_time) continue; if (current_time < _relays[id].change_time) continue;
@ -203,7 +259,7 @@ void _relayProcess(bool mode) {
_relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave, save_eeprom); _relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave, save_eeprom);
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend(_relayWebSocketUpdate);
wsPost(_relayWebSocketUpdate);
#endif #endif
} }
@ -352,10 +408,10 @@ bool relayStatus(unsigned char id, bool status) {
bool relayStatus(unsigned char id) { bool relayStatus(unsigned char id) {
// Check relay ID
// Check that relay ID is valid
if (id >= _relays.size()) return false; if (id >= _relays.size()) return false;
// Get status from storage
// Get status directly from storage
return _relays[id].current_status; return _relays[id].current_status;
} }
@ -459,37 +515,40 @@ unsigned char relayCount() {
return _relays.size(); return _relays.size();
} }
unsigned char relayParsePayload(const char * payload) {
// Payload could be "OFF", "ON", "TOGGLE"
// or its number equivalents: 0, 1 or 2
RelayStatus relayParsePayload(const char * payload) {
if (payload[0] == '0') return 0;
if (payload[0] == '1') return 1;
if (payload[0] == '2') return 2;
// Don't parse empty strings
const auto len = strlen(payload);
if (!len) return RelayStatus::UNKNOWN;
// trim payload
char * p = ltrim((char *)payload);
// to lower
unsigned int l = strlen(p);
if (l>6) l=6;
for (unsigned char i=0; i<l; i++) {
p[i] = tolower(p[i]);
// Check most commonly used payloads
if (len == 1) {
if (payload[0] == '0') return RelayStatus::OFF;
if (payload[0] == '1') return RelayStatus::ON;
if (payload[0] == '2') return RelayStatus::TOGGLE;
return RelayStatus::UNKNOWN;
} }
unsigned int value = 0xFF;
if (strcmp(p, "off") == 0) {
value = 0;
} else if (strcmp(p, "on") == 0) {
value = 1;
} else if (strcmp(p, "toggle") == 0) {
value = 2;
} else if (strcmp(p, "query") == 0) {
value = 3;
// If possible, compare to locally configured payload strings
#if MQTT_SUPPORT
if (_relay_mqtt_payload_off.equals(payload)) return RelayStatus::OFF;
if (_relay_mqtt_payload_on.equals(payload)) return RelayStatus::ON;
if (_relay_mqtt_payload_toggle.equals(payload)) return RelayStatus::TOGGLE;
#endif // MQTT_SUPPORT
// Finally, check for "OFF", "ON", "TOGGLE" (both lower and upper cases)
String temp(payload);
temp.trim();
if (temp.equalsIgnoreCase("off")) {
return RelayStatus::OFF;
} else if (temp.equalsIgnoreCase("on")) {
return RelayStatus::ON;
} else if (temp.equalsIgnoreCase("toggle")) {
return RelayStatus::TOGGLE;
} }
return value;
return RelayStatus::UNKNOWN;
} }
@ -502,21 +561,6 @@ void _relayBackwards() {
delSetting("mqttGroupInv", i); delSetting("mqttGroupInv", i);
} }
byte relayMode = getSetting("relayMode", RELAY_BOOT_MODE).toInt();
byte relayPulseMode = getSetting("relayPulseMode", RELAY_PULSE_MODE).toInt();
float relayPulseTime = getSetting("relayPulseTime", RELAY_PULSE_TIME).toFloat();
if (relayPulseMode == RELAY_PULSE_NONE) relayPulseTime = 0;
for (unsigned int i=0; i<_relays.size(); i++) {
if (!hasSetting("relayBoot", i)) setSetting("relayBoot", i, relayMode);
if (!hasSetting("relayPulse", i)) setSetting("relayPulse", i, relayPulseMode);
if (!hasSetting("relayTime", i)) setSetting("relayTime", i, relayPulseTime);
}
delSetting("relayMode");
delSetting("relayPulseMode");
delSetting("relayPulseTime");
} }
void _relayBoot() { void _relayBoot() {
@ -536,6 +580,7 @@ void _relayBoot() {
auto mask = std::bitset<RELAY_SAVE_MASK_MAX>(stored_mask); auto mask = std::bitset<RELAY_SAVE_MASK_MAX>(stored_mask);
// Walk the relays // Walk the relays
unsigned char lock;
bool status; bool status;
for (unsigned char i=0; i<relayCount(); ++i) { for (unsigned char i=0; i<relayCount(); ++i) {
@ -543,6 +588,7 @@ void _relayBoot() {
DEBUG_MSG_P(PSTR("[RELAY] Relay #%u boot mode %u\n"), i, boot_mode); DEBUG_MSG_P(PSTR("[RELAY] Relay #%u boot mode %u\n"), i, boot_mode);
status = false; status = false;
lock = RELAY_LOCK_DISABLED;
switch (boot_mode) { switch (boot_mode) {
case RELAY_BOOT_SAME: case RELAY_BOOT_SAME:
if (i < 8) { if (i < 8) {
@ -556,6 +602,13 @@ void _relayBoot() {
trigger_save = true; trigger_save = true;
} }
break; break;
case RELAY_BOOT_LOCKED_ON:
status = true;
lock = RELAY_LOCK_ON;
break;
case RELAY_BOOT_LOCKED_OFF:
lock = RELAY_LOCK_OFF;
break;
case RELAY_BOOT_ON: case RELAY_BOOT_ON:
status = true; status = true;
break; break;
@ -571,7 +624,10 @@ void _relayBoot() {
#else #else
_relays[i].change_time = millis(); _relays[i].change_time = millis();
#endif #endif
}
_relays[i].lock = lock;
}
// Save if there is any relay in the RELAY_BOOT_TOGGLE mode // Save if there is any relay in the RELAY_BOOT_TOGGLE mode
if (trigger_save) { if (trigger_save) {
@ -601,6 +657,14 @@ void _relayConfigure() {
digitalWrite(_relays[i].pin, HIGH); digitalWrite(_relays[i].pin, HIGH);
} }
} }
#if MQTT_SUPPORT
settingsProcessConfig({
{_relay_mqtt_payload_on, "relayPayloadOn", RELAY_MQTT_ON},
{_relay_mqtt_payload_off, "relayPayloadOff", RELAY_MQTT_OFF},
{_relay_mqtt_payload_toggle, "relayPayloadToggle", RELAY_MQTT_TOGGLE},
});
#endif // MQTT_SUPPORT
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -609,14 +673,20 @@ void _relayConfigure() {
#if WEB_SUPPORT #if WEB_SUPPORT
bool _relayWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _relayWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "relay", 5) == 0); return (strncmp(key, "relay", 5) == 0);
} }
void _relayWebSocketUpdate(JsonObject& root) { void _relayWebSocketUpdate(JsonObject& root) {
JsonArray& relay = root.createNestedArray("relayStatus");
JsonObject& state = root.createNestedObject("relayState");
state["size"] = relayCount();
JsonArray& status = state.createNestedArray("status");
JsonArray& lock = state.createNestedArray("lock");
for (unsigned char i=0; i<relayCount(); i++) { for (unsigned char i=0; i<relayCount(); i++) {
relay.add<uint8_t>(_relays[i].target_status);
status.add<uint8_t>(_relays[i].target_status);
lock.add(_relays[i].lock);
} }
} }
@ -649,9 +719,7 @@ String _relayFriendlyName(unsigned char i) {
return res; return res;
} }
void _relayWebSocketSendRelays() {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
void _relayWebSocketSendRelays(JsonObject& root) {
JsonObject& relays = root.createNestedObject("relayConfig"); JsonObject& relays = root.createNestedObject("relayConfig");
relays["size"] = relayCount(); relays["size"] = relayCount();
@ -686,69 +754,52 @@ void _relayWebSocketSendRelays() {
on_disconnect.add(getSetting("relayOnDisc", i, 0).toInt()); on_disconnect.add(getSetting("relayOnDisc", i, 0).toInt());
#endif #endif
} }
wsSend(root);
} }
void _relayWebSocketOnStart(JsonObject& root) {
void _relayWebSocketOnVisible(JsonObject& root) {
if (relayCount() == 0) return; if (relayCount() == 0) return;
// Per-relay configuration
_relayWebSocketSendRelays();
// Statuses
_relayWebSocketUpdate(root);
// Options
if (relayCount() > 1) { if (relayCount() > 1) {
root["multirelayVisible"] = 1; root["multirelayVisible"] = 1;
root["relaySync"] = getSetting("relaySync", RELAY_SYNC); root["relaySync"] = getSetting("relaySync", RELAY_SYNC);
} }
root["relayVisible"] = 1; root["relayVisible"] = 1;
} }
void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
void _relayWebSocketOnConnected(JsonObject& root) {
if (strcmp(action, "relay") != 0) return;
if (data.containsKey("status")) {
unsigned char value = relayParsePayload(data["status"]);
if (relayCount() == 0) return;
if (value == 3) {
// Per-relay configuration
_relayWebSocketSendRelays(root);
wsSend(_relayWebSocketUpdate);
}
} else if (value < 3) {
void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
unsigned int relayID = 0;
if (data.containsKey("id")) {
String value = data["id"];
relayID = value.toInt();
}
if (strcmp(action, "relay") != 0) return;
// Action to perform
if (value == 0) {
relayStatus(relayID, false);
} else if (value == 1) {
relayStatus(relayID, true);
} else if (value == 2) {
relayToggle(relayID);
}
if (data.containsKey("status")) {
unsigned int relayID = 0;
if (data.containsKey("id") && data.is<int>("id")) {
relayID = data["id"];
} }
_relayHandlePayload(relayID, data["status"]);
} }
} }
void relaySetupWS() { void relaySetupWS() {
wsOnSendRegister(_relayWebSocketOnStart);
wsOnActionRegister(_relayWebSocketOnAction);
wsOnReceiveRegister(_relayWebSocketOnReceive);
wsRegister()
.onVisible(_relayWebSocketOnVisible)
.onConnected(_relayWebSocketOnConnected)
.onData(_relayWebSocketUpdate)
.onAction(_relayWebSocketOnAction)
.onKeyCheck(_relayWebSocketOnKeyCheck);
} }
#endif // WEB_SUPPORT #endif // WEB_SUPPORT
@ -773,32 +824,22 @@ void relaySetupAPI() {
}, },
[relayID](const char * payload) { [relayID](const char * payload) {
unsigned char value = relayParsePayload(payload);
if (value == 0xFF) {
if (_relayHandlePayload(relayID, payload)) {
DEBUG_MSG_P(PSTR("[RELAY] Wrong payload (%s)\n"), payload); DEBUG_MSG_P(PSTR("[RELAY] Wrong payload (%s)\n"), payload);
return; return;
} }
if (value == 0) {
relayStatus(relayID, false);
} else if (value == 1) {
relayStatus(relayID, true);
} else if (value == 2) {
relayToggle(relayID);
}
} }
); );
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_PULSE, relayID); snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_PULSE, relayID);
apiRegister(key, apiRegister(key,
[relayID](char * buffer, size_t len) { [relayID](char * buffer, size_t len) {
dtostrf((double) _relays[relayID].pulse_ms / 1000, 1-len, 3, buffer);
dtostrf((double) _relays[relayID].pulse_ms / 1000, 1, 3, buffer);
}, },
[relayID](const char * payload) { [relayID](const char * payload) {
unsigned long pulse = 1000 * String(payload).toFloat();
unsigned long pulse = 1000 * atof(payload);
if (0 == pulse) return; if (0 == pulse) return;
if (RELAY_PULSE_NONE != _relays[relayID].pulse) { if (RELAY_PULSE_NONE != _relays[relayID].pulse) {
@ -837,6 +878,31 @@ void relaySetupAPI() {
#if MQTT_SUPPORT #if MQTT_SUPPORT
const String& relayPayloadOn() {
return _relay_mqtt_payload_on;
}
const String& relayPayloadOff() {
return _relay_mqtt_payload_off;
}
const String& relayPayloadToggle() {
return _relay_mqtt_payload_toggle;
}
const char* relayPayload(RelayStatus status) {
if (status == RelayStatus::OFF) {
return _relay_mqtt_payload_off.c_str();
} else if (status == RelayStatus::ON) {
return _relay_mqtt_payload_on.c_str();
} else if (status == RelayStatus::TOGGLE) {
return _relay_mqtt_payload_toggle.c_str();
}
return "";
}
void _relayMQTTGroup(unsigned char id) { void _relayMQTTGroup(unsigned char id) {
String topic = getSetting("mqttGroup", id, ""); String topic = getSetting("mqttGroup", id, "");
if (!topic.length()) return; if (!topic.length()) return;
@ -844,9 +910,9 @@ void _relayMQTTGroup(unsigned char id) {
unsigned char mode = getSetting("mqttGroupSync", id, RELAY_GROUP_SYNC_NORMAL).toInt(); unsigned char mode = getSetting("mqttGroupSync", id, RELAY_GROUP_SYNC_NORMAL).toInt();
if (mode == RELAY_GROUP_SYNC_RECEIVEONLY) return; if (mode == RELAY_GROUP_SYNC_RECEIVEONLY) return;
bool status = relayStatus(id);
if (mode == RELAY_GROUP_SYNC_INVERSE) status = !status;
mqttSendRaw(topic.c_str(), status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
auto status = _relayStatusTyped(id);
if (mode == RELAY_GROUP_SYNC_INVERSE) status = _relayStatusInvert(status);
mqttSendRaw(topic.c_str(), relayPayload(status));
} }
void relayMQTT(unsigned char id) { void relayMQTT(unsigned char id) {
@ -856,7 +922,7 @@ void relayMQTT(unsigned char id) {
// Send state topic // Send state topic
if (_relays[id].report) { if (_relays[id].report) {
_relays[id].report = false; _relays[id].report = false;
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
mqttSend(MQTT_TOPIC_RELAY, id, relayPayload(_relayStatusTyped(id)));
} }
// Check group topic // Check group topic
@ -876,19 +942,19 @@ void relayMQTT(unsigned char id) {
void relayMQTT() { void relayMQTT() {
for (unsigned int id=0; id < _relays.size(); id++) { for (unsigned int id=0; id < _relays.size(); id++) {
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
mqttSend(MQTT_TOPIC_RELAY, id, relayPayload(_relayStatusTyped(id)));
} }
} }
void relayStatusWrap(unsigned char id, unsigned char value, bool is_group_topic) {
void relayStatusWrap(unsigned char id, RelayStatus value, bool is_group_topic) {
switch (value) { switch (value) {
case 0:
case RelayStatus::OFF:
relayStatus(id, false, mqttForward(), !is_group_topic); relayStatus(id, false, mqttForward(), !is_group_topic);
break; break;
case 1:
case RelayStatus::ON:
relayStatus(id, true, mqttForward(), !is_group_topic); relayStatus(id, true, mqttForward(), !is_group_topic);
break; break;
case 2:
case RelayStatus::TOGGLE:
relayToggle(id, true, true); relayToggle(id, true, true);
break; break;
default: default:
@ -943,7 +1009,7 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
return; return;
} }
unsigned long pulse = 1000 * String(payload).toFloat();
unsigned long pulse = 1000 * atof(payload);
if (0 == pulse) return; if (0 == pulse) return;
if (RELAY_PULSE_NONE != _relays[id].pulse) { if (RELAY_PULSE_NONE != _relays[id].pulse) {
@ -969,8 +1035,8 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
} }
// Get value // Get value
unsigned char value = relayParsePayload(payload);
if (value == 0xFF) return;
auto value = relayParsePayload(payload);
if (value == RelayStatus::UNKNOWN) return;
relayStatusWrap(id, value, false); relayStatusWrap(id, value, false);
@ -985,12 +1051,12 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
if ((t.length() > 0) && t.equals(topic)) { if ((t.length() > 0) && t.equals(topic)) {
unsigned char value = relayParsePayload(payload);
if (value == 0xFF) return;
auto value = relayParsePayload(payload);
if (value == RelayStatus::UNKNOWN) return;
if (value < 2) {
if ((value == RelayStatus::ON) || (value == RelayStatus::OFF)) {
if (getSetting("mqttGroupSync", i, RELAY_GROUP_SYNC_NORMAL).toInt() == RELAY_GROUP_SYNC_INVERSE) { if (getSetting("mqttGroupSync", i, RELAY_GROUP_SYNC_NORMAL).toInt() == RELAY_GROUP_SYNC_INVERSE) {
value = 1 - value;
value = _relayStatusInvert(value);
} }
} }
@ -1014,10 +1080,10 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
int reaction = getSetting("relayOnDisc", i, 0).toInt(); int reaction = getSetting("relayOnDisc", i, 0).toInt();
if (1 == reaction) { // switch relay OFF if (1 == reaction) { // switch relay OFF
DEBUG_MSG_P(PSTR("[RELAY] Reset relay (%d) due to MQTT disconnection\n"), i); DEBUG_MSG_P(PSTR("[RELAY] Reset relay (%d) due to MQTT disconnection\n"), i);
relayStatusWrap(i, false, false);
relayStatusWrap(i, RelayStatus::OFF, false);
} else if(2 == reaction) { // switch relay ON } else if(2 == reaction) { // switch relay ON
DEBUG_MSG_P(PSTR("[RELAY] Set relay (%d) due to MQTT disconnection\n"), i); DEBUG_MSG_P(PSTR("[RELAY] Set relay (%d) due to MQTT disconnection\n"), i);
relayStatusWrap(i, true, false);
relayStatusWrap(i, RelayStatus::ON, false);
} }
} }


+ 24
- 47
code/espurna/rfbridge.ino View File

@ -69,10 +69,6 @@ bool _rfb_receive = false;
bool _rfb_transmit = false; bool _rfb_transmit = false;
unsigned char _rfb_repeat = RF_SEND_TIMES; unsigned char _rfb_repeat = RF_SEND_TIMES;
#if WEB_SUPPORT
Ticker _rfb_sendcodes;
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PRIVATES // PRIVATES
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -89,11 +85,7 @@ static bool _rfbToChar(byte * in, char * out, int n = RF_MESSAGE_SIZE) {
#if WEB_SUPPORT #if WEB_SUPPORT
void _rfbWebSocketSendCodeArray(unsigned char start, unsigned char size) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
void _rfbWebSocketSendCodeArray(JsonObject& root, unsigned char start, unsigned char size) {
JsonObject& rfb = root.createNestedObject("rfb"); JsonObject& rfb = root.createNestedObject("rfb");
rfb["size"] = size; rfb["size"] = size;
rfb["start"] = start; rfb["start"] = start;
@ -105,21 +97,13 @@ void _rfbWebSocketSendCodeArray(unsigned char start, unsigned char size) {
on.add(rfbRetrieve(id, true)); on.add(rfbRetrieve(id, true));
off.add(rfbRetrieve(id, false)); off.add(rfbRetrieve(id, false));
} }
wsSend(root);
} }
void _rfbWebSocketSendCode(unsigned char id) {
_rfbWebSocketSendCodeArray(id, 1);
}
void _rfbWebSocketSendCodes() {
_rfbWebSocketSendCodeArray(0, relayCount());
void _rfbWebSocketOnVisible(JsonObject& root) {
root["rfbVisible"] = 1;
} }
void _rfbWebSocketOnSend(JsonObject& root) {
root["rfbVisible"] = 1;
void _rfbWebSocketOnConnected(JsonObject& root) {
root["rfbRepeat"] = getSetting("rfbRepeat", RF_SEND_TIMES).toInt(); root["rfbRepeat"] = getSetting("rfbRepeat", RF_SEND_TIMES).toInt();
root["rfbCount"] = relayCount(); root["rfbCount"] = relayCount();
#if RFB_DIRECT #if RFB_DIRECT
@ -127,7 +111,6 @@ void _rfbWebSocketOnSend(JsonObject& root) {
root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN).toInt(); root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN).toInt();
root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN).toInt(); root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN).toInt();
#endif #endif
_rfb_sendcodes.once_ms(1000, _rfbWebSocketSendCodes);
} }
void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
@ -136,10 +119,14 @@ void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject&
if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as<const char*>()); if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as<const char*>());
} }
bool _rfbWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "rfb", 3) == 0); return (strncmp(key, "rfb", 3) == 0);
} }
void _rfbWebSocketOnData(JsonObject& root) {
_rfbWebSocketSendCodeArray(root, 0, relayCount());
}
#endif // WEB_SUPPORT #endif // WEB_SUPPORT
/* /*
@ -267,9 +254,6 @@ void _rfbDecode() {
if (action == RF_CODE_LEARN_KO) { if (action == RF_CODE_LEARN_KO) {
_rfbAck(); _rfbAck();
DEBUG_MSG_P(PSTR("[RF] Learn timeout\n")); DEBUG_MSG_P(PSTR("[RF] Learn timeout\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"action\": \"rfbTimeout\"}"));
#endif
} }
if (action == RF_CODE_LEARN_OK || action == RF_CODE_RFIN) { if (action == RF_CODE_LEARN_OK || action == RF_CODE_RFIN) {
@ -288,7 +272,9 @@ void _rfbDecode() {
// Websocket update // Websocket update
#if WEB_SUPPORT #if WEB_SUPPORT
_rfbWebSocketSendCode(_learnId);
wsPost([](JsonObject& root) {
_rfbWebSocketSendCodeArray(root, _learnId, 1);
});
#endif #endif
} }
@ -518,18 +504,6 @@ void _rfbReceive() {
#endif // RFB_DIRECT #endif // RFB_DIRECT
void _rfbLearn() {
_rfbLearnImpl();
#if WEB_SUPPORT
char buffer[100];
snprintf_P(buffer, sizeof(buffer), PSTR("{\"action\": \"rfbLearn\", \"data\":{\"id\": %d, \"status\": %d}}"), _learnId, _learnStatus ? 1 : 0);
wsSend(buffer);
#endif
}
#if MQTT_SUPPORT #if MQTT_SUPPORT
void _rfbMqttCallback(unsigned int type, const char * topic, const char * payload) { void _rfbMqttCallback(unsigned int type, const char * topic, const char * payload) {
@ -564,7 +538,7 @@ void _rfbMqttCallback(unsigned int type, const char * topic, const char * payloa
return; return;
} }
_learnStatus = (char)payload[0] != '0'; _learnStatus = (char)payload[0] != '0';
_rfbLearn();
_rfbLearnImpl();
return; return;
} }
@ -615,7 +589,7 @@ void _rfbAPISetup() {
tok = strtok(NULL, ","); tok = strtok(NULL, ",");
if (NULL == tok) return; if (NULL == tok) return;
_learnStatus = (char) tok[0] != '0'; _learnStatus = (char) tok[0] != '0';
_rfbLearn();
_rfbLearnImpl();
} }
); );
@ -735,7 +709,7 @@ void rfbStatus(unsigned char id, bool status) {
void rfbLearn(unsigned char id, bool status) { void rfbLearn(unsigned char id, bool status) {
_learnId = id; _learnId = id;
_learnStatus = status; _learnStatus = status;
_rfbLearn();
_rfbLearnImpl();
} }
void rfbForget(unsigned char id, bool status) { void rfbForget(unsigned char id, bool status) {
@ -746,9 +720,9 @@ void rfbForget(unsigned char id, bool status) {
// Websocket update // Websocket update
#if WEB_SUPPORT #if WEB_SUPPORT
char wsb[100];
snprintf_P(wsb, sizeof(wsb), PSTR("{\"rfb\":[{\"id\": %d, \"status\": %d, \"data\": \"\"}]}"), id, status ? 1 : 0);
wsSend(wsb);
wsPost([id](JsonObject& root) {
_rfbWebSocketSendCodeArray(root, id, 1);
});
#endif #endif
} }
@ -768,9 +742,12 @@ void rfbSetup() {
#endif #endif
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_rfbWebSocketOnSend);
wsOnActionRegister(_rfbWebSocketOnAction);
wsOnReceiveRegister(_rfbWebSocketOnReceive);
wsRegister()
.onVisible(_rfbWebSocketOnVisible)
.onConnected(_rfbWebSocketOnConnected)
.onData(_rfbWebSocketOnData)
.onAction(_rfbWebSocketOnAction)
.onKeyCheck(_rfbWebSocketOnKeyCheck);
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT


+ 6
- 5
code/espurna/rfm69.ino View File

@ -35,7 +35,7 @@ unsigned long _rfm69_packet_count;
#if WEB_SUPPORT #if WEB_SUPPORT
void _rfm69WebSocketOnSend(JsonObject& root) {
void _rfm69WebSocketOnConnected(JsonObject& root) {
root["rfm69Visible"] = 1; root["rfm69Visible"] = 1;
root["rfm69Topic"] = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC); root["rfm69Topic"] = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC);
@ -53,7 +53,7 @@ void _rfm69WebSocketOnSend(JsonObject& root) {
} }
bool _rfm69WebSocketOnReceive(const char * key, JsonVariant& value) {
bool _rfm69WebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "rfm69", 5) == 0) return true; if (strncmp(key, "rfm69", 5) == 0) return true;
if (strncmp(key, "node", 4) == 0) return true; if (strncmp(key, "node", 4) == 0) return true;
if (strncmp(key, "key", 3) == 0) return true; if (strncmp(key, "key", 3) == 0) return true;
@ -269,9 +269,10 @@ void rfm69Setup() {
DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode %s\n"), RFM69_PROMISCUOUS ? "ON" : "OFF"); DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode %s\n"), RFM69_PROMISCUOUS ? "ON" : "OFF");
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_rfm69WebSocketOnSend);
wsOnReceiveRegister(_rfm69WebSocketOnReceive);
wsOnActionRegister(_rfm69WebSocketOnAction);
wsRegister()
.onConnected(_rfm69WebSocketOnConnected)
.onAction(_rfm69WebSocketOnAction)
.onKeyCheck(_rfm69WebSocketOnKeyCheck);
#endif #endif
// Main callbacks // Main callbacks


+ 46
- 10
code/espurna/rtcmem.ino View File

@ -1,7 +1,22 @@
/*
RTMEM MODULE
*/
bool _rtcmem_status = false; bool _rtcmem_status = false;
void _rtcmemErase() {
auto ptr = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
const auto end = ptr + RTCMEM_BLOCKS;
DEBUG_MSG_P(PSTR("[RTCMEM] Erasing start=%p end=%p\n"), ptr, end);
do {
*ptr = 0;
} while (++ptr != end);
}
void _rtcmemInit() { void _rtcmemInit() {
memset((uint32_t*)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS);
_rtcmemErase();
Rtcmem->magic = RTCMEM_MAGIC; Rtcmem->magic = RTCMEM_MAGIC;
} }
@ -31,17 +46,38 @@ void _rtcmemInitCommands() {
_rtcmemInit(); _rtcmemInit();
}); });
terminalRegisterCommand(F("RTCMEM.TEST"), [](Embedis* e) {
});
terminalRegisterCommand(F("RTCMEM.DUMP"), [](Embedis* e) { terminalRegisterCommand(F("RTCMEM.DUMP"), [](Embedis* e) {
DEBUG_MSG_P(PSTR("[RTCMEM] status:%u blocks:%u addr:0x%p\n"),
_rtcmemStatus(), RtcmemSize, Rtcmem);
for (uint8_t block=0; block<RtcmemSize; ++block) {
DEBUG_MSG_P(PSTR("[RTCMEM] %02u: %u\n"),
block, reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR)[block]);
}
DEBUG_MSG_P(PSTR("[RTCMEM] boot_status=%u status=%u blocks_used=%u\n"),
_rtcmem_status, _rtcmemStatus(), RtcmemSize);
String line;
line.reserve(96);
char buffer[16] = {0};
auto addr = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
uint8_t block = 1;
uint8_t offset = 0;
uint8_t start = 0;
do {
offset = block - 1;
snprintf(buffer, sizeof(buffer), "%08x ", *(addr + offset));
line += buffer;
if ((block % 8) == 0) {
DEBUG_MSG_P(PSTR("%02u %p: %s\n"), start, addr+start, line.c_str());
start = block;
line = "";
}
++block;
} while (block<(RTCMEM_BLOCKS+1));
}); });
} }


+ 11
- 5
code/espurna/scheduler.ino View File

@ -15,15 +15,19 @@ Adapted by Xose Pérez <xose dot perez at gmail dot com>
#if WEB_SUPPORT #if WEB_SUPPORT
bool _schWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "sch", 3) == 0); return (strncmp(key, "sch", 3) == 0);
} }
void _schWebSocketOnSend(JsonObject &root){
void _schWebSocketOnVisible(JsonObject& root) {
if (!relayCount()) return;
root["schVisible"] = 1;
}
void _schWebSocketOnConnected(JsonObject &root){
if (!relayCount()) return; if (!relayCount()) return;
root["schVisible"] = 1;
root["maxSchedules"] = SCHEDULER_MAX_SCHEDULES; root["maxSchedules"] = SCHEDULER_MAX_SCHEDULES;
JsonObject &schedules = root.createNestedObject("schedules"); JsonObject &schedules = root.createNestedObject("schedules");
@ -229,8 +233,10 @@ void schSetup() {
// Update websocket clients // Update websocket clients
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_schWebSocketOnSend);
wsOnReceiveRegister(_schWebSocketOnReceive);
wsRegister()
.onVisible(_schWebSocketOnVisible)
.onConnected(_schWebSocketOnConnected)
.onKeyCheck(_schWebSocketOnKeyCheck);
#endif #endif
// Main callbacks // Main callbacks


+ 326
- 80
code/espurna/sensor.ino View File

@ -17,7 +17,7 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <float.h> #include <float.h>
typedef struct {
struct sensor_magnitude_t {
BaseSensor * sensor; // Sensor object BaseSensor * sensor; // Sensor object
BaseFilter * filter; // Filter object BaseFilter * filter; // Filter object
unsigned char local; // Local index in its provider unsigned char local; // Local index in its provider
@ -28,7 +28,7 @@ typedef struct {
double reported; // Last reported value double reported; // Last reported value
double min_change; // Minimum value change to report double min_change; // Minimum value change to report
double max_change; // Maximum value change to report double max_change; // Maximum value change to report
} sensor_magnitude_t;
};
std::vector<BaseSensor *> _sensors; std::vector<BaseSensor *> _sensors;
std::vector<sensor_magnitude_t> _magnitudes; std::vector<sensor_magnitude_t> _magnitudes;
@ -111,32 +111,46 @@ double _magnitudeProcess(unsigned char type, unsigned char decimals, double valu
#if WEB_SUPPORT #if WEB_SUPPORT
//void _sensorWebSocketMagnitudes(JsonObject& root, const String& ws_name, const String& conf_name) {
template<typename T> void _sensorWebSocketMagnitudes(JsonObject& root, T prefix) { template<typename T> void _sensorWebSocketMagnitudes(JsonObject& root, T prefix) {
// ws produces flat list <prefix>Magnitudes // ws produces flat list <prefix>Magnitudes
String ws_name = String(prefix);
ws_name.concat("Magnitudes");
const String ws_name = String(prefix) + "Magnitudes";
// config uses <prefix>Magnitude<index> (cut 's') // config uses <prefix>Magnitude<index> (cut 's')
String conf_name = ws_name.substring(0, ws_name.length() - 1);
const String conf_name = ws_name.substring(0, ws_name.length() - 1);
JsonObject& list = root.createNestedObject(ws_name); JsonObject& list = root.createNestedObject(ws_name);
list["size"] = magnitudeCount(); list["size"] = magnitudeCount();
JsonArray& name = list.createNestedArray("name");
//JsonArray& name = list.createNestedArray("name");
JsonArray& type = list.createNestedArray("type"); JsonArray& type = list.createNestedArray("type");
JsonArray& index = list.createNestedArray("index"); JsonArray& index = list.createNestedArray("index");
JsonArray& idx = list.createNestedArray("idx"); JsonArray& idx = list.createNestedArray("idx");
for (unsigned char i=0; i<magnitudeCount(); ++i) { for (unsigned char i=0; i<magnitudeCount(); ++i) {
name.add(magnitudeName(i));
//name.add(magnitudeName(i));
type.add(magnitudeType(i)); type.add(magnitudeType(i));
index.add(magnitudeIndex(i)); index.add(magnitudeIndex(i));
idx.add(getSetting(conf_name, i, 0).toInt()); idx.add(getSetting(conf_name, i, 0).toInt());
} }
} }
bool _sensorWebSocketOnReceive(const char * key, JsonVariant& value) {
/*
template<typename T> void _sensorWebSocketMagnitudes(JsonObject& root, T prefix) {
// ws produces flat list <prefix>Magnitudes
const String ws_name = String(prefix) + "Magnitudes";
// config uses <prefix>Magnitude<index> (cut 's')
const String conf_name = ws_name.substring(0, ws_name.length() - 1);
_sensorWebSocketMagnitudes(root, ws_name, conf_name);
}
*/
bool _sensorWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "pwr", 3) == 0) return true; if (strncmp(key, "pwr", 3) == 0) return true;
if (strncmp(key, "sns", 3) == 0) return true; if (strncmp(key, "sns", 3) == 0) return true;
if (strncmp(key, "tmp", 3) == 0) return true; if (strncmp(key, "tmp", 3) == 0) return true;
@ -146,21 +160,28 @@ bool _sensorWebSocketOnReceive(const char * key, JsonVariant& value) {
return false; return false;
} }
void _sensorWebSocketSendData(JsonObject& root) {
void _sensorWebSocketOnVisible(JsonObject& root) {
char buffer[10];
bool hasTemperature = false;
bool hasHumidity = false;
bool hasMICS = false;
root["snsVisible"] = 1;
JsonObject& magnitudes = root.createNestedObject("magnitudes");
for (auto& magnitude : _magnitudes) {
if (magnitude.type == MAGNITUDE_TEMPERATURE) root["temperatureVisible"] = 1;
if (magnitude.type == MAGNITUDE_HUMIDITY) root["humidityVisible"] = 1;
#if MICS2710_SUPPORT || MICS5525_SUPPORT
if (magnitude.type == MAGNITUDE_CO || magnitude.type == MAGNITUDE_NO2) root["micsVisible"] = 1;
#endif
}
}
void _sensorWebSocketMagnitudesConfig(JsonObject& root) {
JsonObject& magnitudes = root.createNestedObject("magnitudesConfig");
uint8_t size = 0; uint8_t size = 0;
JsonArray& index = magnitudes.createNestedArray("index"); JsonArray& index = magnitudes.createNestedArray("index");
JsonArray& type = magnitudes.createNestedArray("type"); JsonArray& type = magnitudes.createNestedArray("type");
JsonArray& value = magnitudes.createNestedArray("value");
JsonArray& units = magnitudes.createNestedArray("units"); JsonArray& units = magnitudes.createNestedArray("units");
JsonArray& error = magnitudes.createNestedArray("error");
JsonArray& description = magnitudes.createNestedArray("description"); JsonArray& description = magnitudes.createNestedArray("description");
for (unsigned char i=0; i<magnitudeCount(); i++) { for (unsigned char i=0; i<magnitudeCount(); i++) {
@ -169,14 +190,9 @@ void _sensorWebSocketSendData(JsonObject& root) {
if (magnitude.type == MAGNITUDE_EVENT) continue; if (magnitude.type == MAGNITUDE_EVENT) continue;
++size; ++size;
double value_show = _magnitudeProcess(magnitude.type, magnitude.decimals, magnitude.last);
dtostrf(value_show, 1-sizeof(buffer), magnitude.decimals, buffer);
index.add<uint8_t>(magnitude.global); index.add<uint8_t>(magnitude.global);
type.add<uint8_t>(magnitude.type); type.add<uint8_t>(magnitude.type);
value.add(buffer);
units.add(magnitudeUnits(magnitude.type)); units.add(magnitudeUnits(magnitude.type));
error.add(magnitude.sensor->error());
if (magnitude.type == MAGNITUDE_ENERGY) { if (magnitude.type == MAGNITUDE_ENERGY) {
if (_sensor_energy_reset_ts.length() == 0) _sensorResetTS(); if (_sensor_energy_reset_ts.length() == 0) _sensorResetTS();
@ -185,22 +201,39 @@ void _sensorWebSocketSendData(JsonObject& root) {
description.add(magnitude.sensor->slot(magnitude.local)); description.add(magnitude.sensor->slot(magnitude.local));
} }
if (magnitude.type == MAGNITUDE_TEMPERATURE) hasTemperature = true;
if (magnitude.type == MAGNITUDE_HUMIDITY) hasHumidity = true;
#if MICS2710_SUPPORT || MICS5525_SUPPORT
if (magnitude.type == MAGNITUDE_CO || magnitude.type == MAGNITUDE_NO2) hasMICS = true;
#endif
} }
magnitudes["size"] = size; magnitudes["size"] = size;
if (hasTemperature) root["temperatureVisible"] = 1;
if (hasHumidity) root["humidityVisible"] = 1;
if (hasMICS) root["micsVisible"] = 1;
}
void _sensorWebSocketSendData(JsonObject& root) {
char buffer[64];
JsonObject& magnitudes = root.createNestedObject("magnitudes");
uint8_t size = 0;
JsonArray& value = magnitudes.createNestedArray("value");
JsonArray& error = magnitudes.createNestedArray("error");
for (unsigned char i=0; i<magnitudeCount(); i++) {
sensor_magnitude_t magnitude = _magnitudes[i];
if (magnitude.type == MAGNITUDE_EVENT) continue;
++size;
double value_show = _magnitudeProcess(magnitude.type, magnitude.decimals, magnitude.last);
dtostrf(value_show, 1, magnitude.decimals, buffer);
value.add(buffer);
error.add(magnitude.sensor->error());
}
magnitudes["size"] = size;
} }
void _sensorWebSocketStart(JsonObject& root) {
void _sensorWebSocketOnConnected(JsonObject& root) {
for (unsigned char i=0; i<_sensors.size(); i++) { for (unsigned char i=0; i<_sensors.size(); i++) {
@ -257,7 +290,6 @@ void _sensorWebSocketStart(JsonObject& root) {
} }
if (magnitudeCount()) { if (magnitudeCount()) {
root["snsVisible"] = 1;
//root["apiRealTime"] = _sensor_realtime; //root["apiRealTime"] = _sensor_realtime;
root["pwrUnits"] = _sensor_power_units; root["pwrUnits"] = _sensor_power_units;
root["eneUnits"] = _sensor_energy_units; root["eneUnits"] = _sensor_energy_units;
@ -267,6 +299,7 @@ void _sensorWebSocketStart(JsonObject& root) {
root["snsRead"] = _sensor_read_interval / 1000; root["snsRead"] = _sensor_read_interval / 1000;
root["snsReport"] = _sensor_report_every; root["snsReport"] = _sensor_report_every;
root["snsSave"] = _sensor_save_every; root["snsSave"] = _sensor_save_every;
_sensorWebSocketMagnitudesConfig(root);
} }
/* /*
@ -304,7 +337,7 @@ void _sensorAPISetup() {
apiRegister(topic.c_str(), [magnitude_id](char * buffer, size_t len) { apiRegister(topic.c_str(), [magnitude_id](char * buffer, size_t len) {
sensor_magnitude_t magnitude = _magnitudes[magnitude_id]; sensor_magnitude_t magnitude = _magnitudes[magnitude_id];
double value = _sensor_realtime ? magnitude.last : magnitude.reported; double value = _sensor_realtime ? magnitude.last : magnitude.reported;
dtostrf(value, 1-len, magnitude.decimals, buffer);
dtostrf(value, 1, magnitude.decimals, buffer);
}); });
} }
@ -360,7 +393,7 @@ void _sensorInitCommands() {
DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n")); DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n"));
for(unsigned char dev = init; dev < limit; dev++) { for(unsigned char dev = init; dev < limit; dev++) {
float offset = pzem004t_sensor->resetEnergy(dev); float offset = pzem004t_sensor->resetEnergy(dev);
setSetting("pzemEneTotal", dev, offset);
_sensorEnergyTotal(dev, offset);
DEBUG_MSG_P(PSTR("Device %d/%s - Offset: %s\n"), dev, pzem004t_sensor->getAddress(dev).c_str(), String(offset).c_str()); DEBUG_MSG_P(PSTR("Device %d/%s - Offset: %s\n"), dev, pzem004t_sensor->getAddress(dev).c_str(), String(offset).c_str());
} }
terminalOK(); terminalOK();
@ -429,33 +462,38 @@ void _sensorResetTS() {
#endif #endif
} }
double _sensorEnergyTotal() {
double _sensorEnergyTotal(unsigned int index) {
double value = 0; double value = 0;
if (rtcmemStatus()) {
value = Rtcmem->energy;
if (rtcmemStatus() && (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy)))) {
value = Rtcmem->energy[index];
} else { } else {
value = (_sensor_save_every > 0) ? getSetting("eneTotal", 0).toInt() : 0;
value = (_sensor_save_every > 0) ? getSetting("eneTotal", index, 0).toInt() : 0;
} }
return value; return value;
} }
double _sensorEnergyTotal() {
return _sensorEnergyTotal(0);
}
void _sensorEnergyTotal(double value) {
void _sensorEnergyTotal(unsigned int index, double value) {
static unsigned long save_count = 0; static unsigned long save_count = 0;
// Save to EEPROM every '_sensor_save_every' readings // Save to EEPROM every '_sensor_save_every' readings
if (_sensor_save_every > 0) { if (_sensor_save_every > 0) {
save_count = (save_count + 1) % _sensor_save_every; save_count = (save_count + 1) % _sensor_save_every;
if (0 == save_count) { if (0 == save_count) {
setSetting("eneTotal", value);
setSetting("eneTotal", index, value);
saveSettings(); saveSettings();
} }
} }
// Always save to RTCMEM // Always save to RTCMEM
Rtcmem->energy = value;
if (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy))) {
Rtcmem->energy[index] = value;
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -574,11 +612,85 @@ void _sensorLoad() {
#if DIGITAL_SUPPORT #if DIGITAL_SUPPORT
{ {
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL_PIN);
sensor->setMode(DIGITAL_PIN_MODE);
sensor->setDefault(DIGITAL_DEFAULT_STATE);
_sensors.push_back(sensor);
#if (DIGITAL1_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL1_PIN);
sensor->setMode(DIGITAL1_PIN_MODE);
sensor->setDefault(DIGITAL1_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL2_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL2_PIN);
sensor->setMode(DIGITAL2_PIN_MODE);
sensor->setDefault(DIGITAL2_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL3_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL3_PIN);
sensor->setMode(DIGITAL3_PIN_MODE);
sensor->setDefault(DIGITAL3_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL4_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL4_PIN);
sensor->setMode(DIGITAL4_PIN_MODE);
sensor->setDefault(DIGITAL4_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL5_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL5_PIN);
sensor->setMode(DIGITAL5_PIN_MODE);
sensor->setDefault(DIGITAL5_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL6_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL6_PIN);
sensor->setMode(DIGITAL6_PIN_MODE);
sensor->setDefault(DIGITAL6_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL7_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL7_PIN);
sensor->setMode(DIGITAL7_PIN_MODE);
sensor->setDefault(DIGITAL7_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
#if (DIGITAL8_PIN != GPIO_NONE)
{
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(DIGITAL8_PIN);
sensor->setMode(DIGITAL8_PIN_MODE);
sensor->setDefault(DIGITAL8_DEFAULT_STATE);
_sensors.push_back(sensor);
}
#endif
} }
#endif #endif
@ -631,13 +743,101 @@ void _sensorLoad() {
#if EVENTS_SUPPORT #if EVENTS_SUPPORT
{ {
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS_PIN);
sensor->setTrigger(EVENTS_TRIGGER);
sensor->setPinMode(EVENTS_PIN_MODE);
sensor->setDebounceTime(EVENTS_DEBOUNCE);
sensor->setInterruptMode(EVENTS_INTERRUPT_MODE);
_sensors.push_back(sensor);
#if (EVENTS1_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS1_PIN);
sensor->setTrigger(EVENTS1_TRIGGER);
sensor->setPinMode(EVENTS1_PIN_MODE);
sensor->setDebounceTime(EVENTS1_DEBOUNCE);
sensor->setInterruptMode(EVENTS1_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS2_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS2_PIN);
sensor->setTrigger(EVENTS2_TRIGGER);
sensor->setPinMode(EVENTS2_PIN_MODE);
sensor->setDebounceTime(EVENTS2_DEBOUNCE);
sensor->setInterruptMode(EVENTS2_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS3_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS3_PIN);
sensor->setTrigger(EVENTS3_TRIGGER);
sensor->setPinMode(EVENTS3_PIN_MODE);
sensor->setDebounceTime(EVENTS3_DEBOUNCE);
sensor->setInterruptMode(EVENTS3_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS4_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS4_PIN);
sensor->setTrigger(EVENTS4_TRIGGER);
sensor->setPinMode(EVENTS4_PIN_MODE);
sensor->setDebounceTime(EVENTS4_DEBOUNCE);
sensor->setInterruptMode(EVENTS4_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS5_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS5_PIN);
sensor->setTrigger(EVENTS5_TRIGGER);
sensor->setPinMode(EVENTS5_PIN_MODE);
sensor->setDebounceTime(EVENTS5_DEBOUNCE);
sensor->setInterruptMode(EVENTS5_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS6_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS6_PIN);
sensor->setTrigger(EVENTS6_TRIGGER);
sensor->setPinMode(EVENTS6_PIN_MODE);
sensor->setDebounceTime(EVENTS6_DEBOUNCE);
sensor->setInterruptMode(EVENTS6_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS7_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS7_PIN);
sensor->setTrigger(EVENTS7_TRIGGER);
sensor->setPinMode(EVENTS7_PIN_MODE);
sensor->setDebounceTime(EVENTS7_DEBOUNCE);
sensor->setInterruptMode(EVENTS7_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if (EVENTS8_PIN != GPIO_NONE)
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS8_PIN);
sensor->setTrigger(EVENTS8_TRIGGER);
sensor->setPinMode(EVENTS8_PIN_MODE);
sensor->setDebounceTime(EVENTS8_DEBOUNCE);
sensor->setInterruptMode(EVENTS8_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
} }
#endif #endif
@ -756,9 +956,11 @@ void _sensorLoad() {
#if PULSEMETER_SUPPORT #if PULSEMETER_SUPPORT
{ {
PulseMeterSensor * sensor = new PulseMeterSensor(); PulseMeterSensor * sensor = new PulseMeterSensor();
sensor->setGPIO(PULSEMETER_PIN); sensor->setGPIO(PULSEMETER_PIN);
sensor->setEnergyRatio(PULSEMETER_ENERGY_RATIO); sensor->setEnergyRatio(PULSEMETER_ENERGY_RATIO);
sensor->setInterruptMode(PULSEMETER_INTERRUPT_ON);
sensor->setDebounceTime(PULSEMETER_DEBOUNCE); sensor->setDebounceTime(PULSEMETER_DEBOUNCE);
_sensors.push_back(sensor); _sensors.push_back(sensor);
} }
@ -785,7 +987,7 @@ void _sensorLoad() {
// Read saved energy offset // Read saved energy offset
unsigned char dev_count = sensor->getAddressesCount(); unsigned char dev_count = sensor->getAddressesCount();
for(unsigned char dev = 0; dev < dev_count; dev++) { for(unsigned char dev = 0; dev < dev_count; dev++) {
float value = getSetting("pzemEneTotal", dev, 0).toFloat();
float value = _sensorEnergyTotal(dev);
if (value > 0) sensor->resetEnergy(dev, value); if (value > 0) sensor->resetEnergy(dev, value);
} }
_sensors.push_back(sensor); _sensors.push_back(sensor);
@ -880,6 +1082,14 @@ void _sensorLoad() {
_sensors.push_back(sensor); _sensors.push_back(sensor);
} }
#endif #endif
#if ADE7953_SUPPORT
{
ADE7953Sensor * sensor = new ADE7953Sensor();
sensor->setAddress(ADE7953_ADDRESS);
_sensors.push_back(sensor);
}
#endif
} }
void _sensorCallback(unsigned char i, unsigned char type, double value) { void _sensorCallback(unsigned char i, unsigned char type, double value) {
@ -1019,6 +1229,19 @@ void _sensorInit() {
#endif // HLW8012_SUPPORT #endif // HLW8012_SUPPORT
#if ADE7953_SUPPORT
if (_sensors[i]->getID() == SENSOR_ADE7953_ID) {
ADE7953Sensor * sensor = (ADE7953Sensor *) _sensors[i];
unsigned int dev_count = sensor->getTotalDevices();
for(unsigned char dev = 0; dev < dev_count; dev++) {
double value = _sensorEnergyTotal(dev);
if (value > 0) sensor->resetEnergy(dev, value);
}
}
#endif // ADE7953_SUPPORT
#if CSE7766_SUPPORT #if CSE7766_SUPPORT
if (_sensors[i]->getID() == SENSOR_CSE7766_ID) { if (_sensors[i]->getID() == SENSOR_CSE7766_ID) {
@ -1046,7 +1269,7 @@ void _sensorInit() {
#if PULSEMETER_SUPPORT #if PULSEMETER_SUPPORT
if (_sensors[i]->getID() == SENSOR_PULSEMETER_ID) { if (_sensors[i]->getID() == SENSOR_PULSEMETER_ID) {
PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i]; PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i];
sensor->setEnergyRatio(getSetting("pwrRatioE", PULSEMETER_ENERGY_RATIO).toInt());
sensor->setEnergyRatio(getSetting("pwrRatioE", sensor->getEnergyRatio()).toInt());
} }
#endif // PULSEMETER_SUPPORT #endif // PULSEMETER_SUPPORT
@ -1115,7 +1338,7 @@ void _sensorConfigure() {
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
@ -1130,7 +1353,7 @@ void _sensorConfigure() {
EmonADC121Sensor * sensor = (EmonADC121Sensor *) _sensors[i]; EmonADC121Sensor * sensor = (EmonADC121Sensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
} }
@ -1141,7 +1364,7 @@ void _sensorConfigure() {
EmonADS1X15Sensor * sensor = (EmonADS1X15Sensor *) _sensors[i]; EmonADS1X15Sensor * sensor = (EmonADS1X15Sensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
} }
@ -1172,7 +1395,7 @@ void _sensorConfigure() {
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
@ -1211,7 +1434,7 @@ void _sensorConfigure() {
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
@ -1231,11 +1454,11 @@ void _sensorConfigure() {
PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i]; PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) { if (getSetting("pwrResetE", 0).toInt() == 1) {
sensor->resetEnergy(); sensor->resetEnergy();
delSetting("eneTotal");
delSetting("eneTotal", 0);
_sensorResetTS(); _sensorResetTS();
} }
sensor->setEnergyRatio(getSetting("pwrRatioE", PULSEMETER_ENERGY_RATIO).toInt());
sensor->setEnergyRatio(getSetting("pwrRatioE", sensor->getEnergyRatio()).toInt());
} }
#endif // PULSEMETER_SUPPORT #endif // PULSEMETER_SUPPORT
@ -1247,7 +1470,7 @@ void _sensorConfigure() {
unsigned char dev_count = sensor->getAddressesCount(); unsigned char dev_count = sensor->getAddressesCount();
for(unsigned char dev = 0; dev < dev_count; dev++) { for(unsigned char dev = 0; dev < dev_count; dev++) {
sensor->resetEnergy(dev, 0); sensor->resetEnergy(dev, 0);
delSetting("pzemEneTotal", dev);
delSetting("eneTotal", dev);
} }
_sensorResetTS(); _sensorResetTS();
} }
@ -1255,19 +1478,37 @@ void _sensorConfigure() {
#endif // PZEM004T_SUPPORT #endif // PZEM004T_SUPPORT
}
#if ADE7953_SUPPORT
if (_sensors[i]->getID() == SENSOR_ADE7953_ID) {
ADE7953Sensor * sensor = (ADE7953Sensor *) _sensors[i];
if (getSetting("pwrResetE", 0).toInt() == 1) {
unsigned char dev_count = sensor->getTotalDevices();
for(unsigned char dev = 0; dev < dev_count; dev++) {
sensor->resetEnergy(dev);
delSetting("eneTotal", dev);
}
_sensorResetTS();
}
}
#endif // ADE7953_SUPPORT
// Update filter sizes
for (unsigned char i=0; i<_magnitudes.size(); i++) {
_magnitudes[i].filter->resize(_sensor_report_every);
} }
// General processing
if (0 == _sensor_save_every) {
delSetting("eneTotal");
// Update filter sizes and reset energy if needed
{
const bool reset_saved_energy = 0 == _sensor_save_every;
for (unsigned char i=0; i<_magnitudes.size(); i++) {
_magnitudes[i].filter->resize(_sensor_report_every);
if ((_magnitudes[i].type == MAGNITUDE_ENERGY) && reset_saved_energy) {
delSetting("eneTotal", _magnitudes[i].global);
}
}
} }
// Save settings
// Remove calibration values
// TODO: do not use settings for one-shot calibration
delSetting("snsResetCalibration"); delSetting("snsResetCalibration");
delSetting("pwrExpectedP"); delSetting("pwrExpectedP");
delSetting("pwrExpectedC"); delSetting("pwrExpectedC");
@ -1283,8 +1524,11 @@ void _sensorReport(unsigned char index, double value) {
sensor_magnitude_t magnitude = _magnitudes[index]; sensor_magnitude_t magnitude = _magnitudes[index];
unsigned char decimals = magnitude.decimals; unsigned char decimals = magnitude.decimals;
char buffer[10];
dtostrf(value, 1-sizeof(buffer), decimals, buffer);
// XXX: ensure that the received 'value' will fit here
// dtostrf 2nd arg only controls leading zeroes and the
// 3rd is only for the part after the dot
char buffer[64];
dtostrf(value, 1, decimals, buffer);
#if BROKER_SUPPORT #if BROKER_SUPPORT
#if not BROKER_REAL_TIME #if not BROKER_REAL_TIME
@ -1424,11 +1668,12 @@ String magnitudeUnits(unsigned char type) {
void sensorSetup() { void sensorSetup() {
// Backwards compatibility // Backwards compatibility
moveSetting("eneTotal", "eneTotal0");
moveSetting("powerUnits", "pwrUnits"); moveSetting("powerUnits", "pwrUnits");
moveSetting("energyUnits", "eneUnits"); moveSetting("energyUnits", "eneUnits");
// Update PZEM004T energy total across multiple devices // Update PZEM004T energy total across multiple devices
moveSettings("pzEneTotal", "pzemEneTotal");
moveSettings("pzEneTotal", "eneTotal");
// Load sensors // Load sensors
_sensorLoad(); _sensorLoad();
@ -1439,9 +1684,11 @@ void sensorSetup() {
// Websockets // Websockets
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_sensorWebSocketStart);
wsOnReceiveRegister(_sensorWebSocketOnReceive);
wsOnSendRegister(_sensorWebSocketSendData);
wsRegister()
.onVisible(_sensorWebSocketOnVisible)
.onConnected(_sensorWebSocketOnConnected)
.onData(_sensorWebSocketSendData)
.onKeyCheck(_sensorWebSocketOnKeyCheck);
#endif #endif
// API // API
@ -1558,7 +1805,7 @@ void sensorLoop() {
#if SENSOR_DEBUG #if SENSOR_DEBUG
{ {
char buffer[64]; char buffer[64];
dtostrf(value_show, 1-sizeof(buffer), magnitude.decimals, buffer);
dtostrf(value_show, 1, magnitude.decimals, buffer);
DEBUG_MSG_P(PSTR("[SENSOR] %s - %s: %s%s\n"), DEBUG_MSG_P(PSTR("[SENSOR] %s - %s: %s%s\n"),
magnitude.sensor->slot(magnitude.local).c_str(), magnitude.sensor->slot(magnitude.local).c_str(),
magnitudeTopic(magnitude.type).c_str(), magnitudeTopic(magnitude.type).c_str(),
@ -1591,10 +1838,9 @@ void sensorLoop() {
_sensorReport(i, value_filtered); _sensorReport(i, value_filtered);
} // if (fabs(value_filtered - magnitude.reported) >= magnitude.min_change) } // if (fabs(value_filtered - magnitude.reported) >= magnitude.min_change)
// Persist total energy value // Persist total energy value
if (MAGNITUDE_ENERGY == magnitude.type) { if (MAGNITUDE_ENERGY == magnitude.type) {
_sensorEnergyTotal(value_raw);
_sensorEnergyTotal(magnitude.global, value_raw);
} }
} // if (report_count == 0) } // if (report_count == 0)
@ -1606,7 +1852,7 @@ void sensorLoop() {
_sensorPost(); _sensorPost();
#if WEB_SUPPORT #if WEB_SUPPORT
wsSend(_sensorWebSocketSendData);
wsPost(_sensorWebSocketSendData);
#endif #endif
#if THINGSPEAK_SUPPORT #if THINGSPEAK_SUPPORT


+ 249
- 0
code/espurna/sensors/ADE7953Sensor.h View File

@ -0,0 +1,249 @@
// -----------------------------------------------------------------------------
// ADE7853 Sensor over I2C
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// Implemented by Antonio López <tonilopezmr at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && ADE7953_SUPPORT
#pragma once
#undef I2C_SUPPORT
#define I2C_SUPPORT 1 // Explicitly request I2C support.
#include "Arduino.h"
#include "I2CSensor.h"
#include <Wire.h>
// -----------------------------------------------------------------------------
// ADE7953 - Energy (Shelly 2.5)
//
// Based on datasheet from https://www.analog.com/en/products/ade7953.html
// Based on Tasmota code https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/xnrg_07_ade7953.ino
//
// I2C Address: 0x38
// -----------------------------------------------------------------------------
#define ADE7953_PREF 1540
#define ADE7953_UREF 26000
#define ADE7953_IREF 10000
#define ADE7953_ALL_RELAYS 0
#define ADE7953_RELAY_1 1
#define ADE7953_RELAY_2 2
#define ADE7953_VOLTAGE 1
#define ADE7953_TOTAL_DEVICES 3
class ADE7953Sensor : public I2CSensor {
protected:
struct reading_t {
float current = 0.0;
float power = 0.0;
float energy = 0.0;
};
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
ADE7953Sensor(): I2CSensor() {
_sensor_id = SENSOR_ADE7953_ID;
_readings.resize(ADE7953_TOTAL_DEVICES);
_energy_offsets.resize(ADE7953_TOTAL_DEVICES);
_count = _readings.size() * ADE7953_TOTAL_DEVICES + ADE7953_VOLTAGE; //10
}
// ---------------------------------------------------------------------
// Sensors API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
if (!_dirty) return;
_init();
_dirty = !_ready;
}
// Descriptive name of the sensor
String description() {
char buffer[25];
snprintf(buffer, sizeof(buffer), "ADE7953 @ I2C (0x%02X)", _address);
return String(buffer);
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_VOLTAGE;
index = index % ADE7953_TOTAL_DEVICES;
if (index == 0) return MAGNITUDE_ENERGY;
if (index == 1) return MAGNITUDE_CURRENT;
if (index == 2) return MAGNITUDE_POWER_ACTIVE;
return MAGNITUDE_NONE;
}
// Pre-read hook (usually to populate registers with up-to-date data)
void pre() {
uint32_t active_power1 = 0;
uint32_t active_power2 = 0;
uint32_t current_rms = 0;
uint32_t current_rms1 = 0;
uint32_t current_rms2 = 0;
uint32_t voltage_rms = 0;
voltage_rms = read(_address, 0x31C); // Both relays
current_rms1 = read(_address, 0x31B); // Relay 1
if (current_rms1 < 2000) { // No load threshold (20mA)
current_rms1 = 0;
active_power1 = 0;
} else {
active_power1 = (int32_t)read(_address, 0x313) * -1; // Relay 1
active_power1 = (active_power1 > 0) ? active_power1 : 0;
}
current_rms2 = read(_address, 0x31A); // Relay 2
if (current_rms2 < 2000) { // No load threshold (20mA)
current_rms2 = 0;
active_power2 = 0;
} else {
active_power2 = (int32_t)read(_address, 0x312); // Relay 2
active_power2 = (active_power2 > 0) ? active_power2 : 0;
}
_voltage = (float) voltage_rms / ADE7953_UREF;
storeReading(
ADE7953_ALL_RELAYS,
(float)(current_rms1 + current_rms2) / (ADE7953_IREF * 10),
(float)(active_power1 + active_power2) / (ADE7953_PREF / 10)
);
storeReading(
ADE7953_RELAY_1,
(float) current_rms1 / (ADE7953_IREF * 10),
(float) active_power1 / (ADE7953_PREF / 10)
);
storeReading(
ADE7953_RELAY_2,
(float)current_rms2 / (ADE7953_IREF * 10),
(float)active_power2 / (ADE7953_PREF / 10)
);
}
inline void storeReading(unsigned int relay, float current, float power) {
auto& reading_ref = _readings.at(relay);
reading_ref.current = current;
reading_ref.power = power;
static unsigned long last = 0;
if (last > 0) {
reading_ref.energy += (power * (millis() - last) / 1000);
}
last = millis();
}
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) return _voltage;
int relay = (index - 1) / ADE7953_TOTAL_DEVICES;
index = index % ADE7953_TOTAL_DEVICES;
if (index == 0) return _energy_offsets[relay] + _readings[relay].energy;
if (index == 1) return _readings[relay].current;
if (index == 2) return _readings[relay].power;
return 0;
}
unsigned int getTotalDevices() {
return ADE7953_TOTAL_DEVICES;
}
void resetEnergy(int relay, double value = 0) {
_energy_offsets[relay] = value;
}
protected:
void _init() {
nice_delay(100); // Need 100mS to init ADE7953
write(_address, 0x102, 0x0004); // Locking the communication interface (Clear bit COMM_LOCK), Enable HPF
write(_address, 0x0FE, 0x00AD); // Unlock register 0x120
write(_address, 0x120, 0x0030); // Configure optimum setting
_ready = true;
}
#if 0
static int reg_size(uint16_t reg) {
int size = 0;
switch ((reg >> 8) & 0x0F) {
case 0x03:
size++;
case 0x02:
size++;
case 0x01:
size++;
case 0x00:
case 0x07:
case 0x08:
size++;
}
return size;
}
#else
// Optimized version of the function above, -80 bytes of code
// Use the known property of register addresses to calculate their size
static const int reg_size(const uint16_t reg) {
const uint8_t mask = ((reg >> 8) & 0b1111);
if (!mask || (mask & 0b1100)) {
return 1;
} else if (mask & 0b0011) {
return mask + 1;
}
return 0;
}
#endif
void write(unsigned char address, uint16_t reg, uint32_t val) {
int size = reg_size(reg);
if (size) {
Wire.beginTransmission(address);
Wire.write((reg >> 8) & 0xFF);
Wire.write(reg & 0xFF);
while (size--) {
Wire.write((val >> (8 * size)) & 0xFF); // Write data, MSB first
}
Wire.endTransmission();
delayMicroseconds(5); // Bus-free time minimum 4.7us
}
}
static uint32_t read(int address, uint16_t reg) {
uint32_t response = 0;
const int size = reg_size(reg);
if (size) {
Wire.beginTransmission(address);
Wire.write((reg >> 8) & 0xFF);
Wire.write(reg & 0xFF);
Wire.endTransmission(0);
Wire.requestFrom(address, size);
if (size <= Wire.available()) {
for (int i = 0; i < size; i++) {
response = response << 8 | Wire.read(); // receive DATA (MSB first)
}
}
}
return response;
}
std::vector<reading_t> _readings;
float _voltage = 0;
std::vector<double> _energy_offsets;
};
#endif // SENSOR_SUPPORT && ADE7953_SUPPORT

+ 2
- 3
code/espurna/sensors/AnalogSensor.h View File

@ -3,7 +3,7 @@
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com> // Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT || LDR_SENSOR)
#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT || LDR_SUPPORT)
#pragma once #pragma once
@ -68,8 +68,7 @@ class AnalogSensor : public BaseSensor {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Initialization method, must be idempotent // Initialization method, must be idempotent
void begin() {
pinMode(0, INPUT);
void begin() {
_ready = true; _ready = true;
} }


+ 43
- 18
code/espurna/sensors/EventSensor.h View File

@ -48,8 +48,8 @@ class EventSensor : public BaseSensor {
_interrupt_mode = interrupt_mode; _interrupt_mode = interrupt_mode;
} }
void setDebounceTime(unsigned long debounce) {
_debounce = debounce;
void setDebounceTime(unsigned long ms) {
_debounce = microsecondsToClockCycles(ms * 1000);
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@ -87,6 +87,16 @@ class EventSensor : public BaseSensor {
_ready = true; _ready = true;
} }
void tick() {
if (!_trigger || !_callback) return;
if (!_trigger_flag) return;
noInterrupts();
_callback(MAGNITUDE_EVENT, _trigger_value);
_trigger_flag = false;
interrupts();
}
// Descriptive name of the sensor // Descriptive name of the sensor
String description() { String description() {
char buffer[20]; char buffer[20];
@ -114,33 +124,42 @@ class EventSensor : public BaseSensor {
// Current value for slot # index // Current value for slot # index
double value(unsigned char index) { double value(unsigned char index) {
if (index == 0) { if (index == 0) {
double value = _events;
_events = 0;
double value = _counter;
_counter = 0;
return value; return value;
}; };
if (index == 1) {
return _value;
}
return 0; return 0;
} }
// Handle interrupt calls
// Handle interrupt calls from isr[GPIO] functions
void ICACHE_RAM_ATTR handleInterrupt(unsigned char gpio) { void ICACHE_RAM_ATTR handleInterrupt(unsigned char gpio) {
UNUSED(gpio); UNUSED(gpio);
static unsigned long last = 0;
// clock count in 32bit value, overflowing: // clock count in 32bit value, overflowing:
// ~53s when F_CPU is 80MHz // ~53s when F_CPU is 80MHz
// ~26s when F_CPU is 160MHz // ~26s when F_CPU is 160MHz
// see: cores/esp8266/Arduino.h definitions // see: cores/esp8266/Arduino.h definitions
unsigned long ms = clockCyclesToMicroseconds(ESP.getCycleCount()) / 1000u;
if (ms - last > _debounce) {
last = ms;
_events = _events + 1;
//
// Note:
// To convert to / from normal time values, use:
// - microsecondsToClockCycles(microseconds)
// - clockCyclesToMicroseconds(cycles)
// Since the division operation on this chip is pretty slow,
// avoid doing the conversion here
unsigned long cycles = ESP.getCycleCount();
if (cycles - _last > _debounce) {
_last = cycles;
_counter += 1;
// we are handling callbacks in tick()
if (_trigger) { if (_trigger) {
if (_callback) _callback(MAGNITUDE_EVENT, digitalRead(gpio));
_trigger_value = digitalRead(gpio);
_trigger_flag = true;
} }
} }
} }
@ -168,10 +187,16 @@ class EventSensor : public BaseSensor {
// Protected // Protected
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
volatile unsigned long _events = 0;
unsigned long _debounce = EVENTS_DEBOUNCE;
unsigned char _gpio = GPIO_NONE;
volatile unsigned long _counter = 0;
unsigned char _value = 0;
unsigned long _last = 0;
unsigned long _debounce = microsecondsToClockCycles(EVENTS1_DEBOUNCE * 1000);
bool _trigger = false; bool _trigger = false;
bool _trigger_flag = false;
unsigned char _trigger_value = false;
unsigned char _gpio = GPIO_NONE;
unsigned char _pin_mode = INPUT; unsigned char _pin_mode = INPUT;
unsigned char _interrupt_mode = RISING; unsigned char _interrupt_mode = RISING;


+ 4
- 4
code/espurna/sensors/GeigerSensor.h View File

@ -138,8 +138,8 @@ double value(unsigned char index) {
double value = _events * 60000; double value = _events * 60000;
value = value / (_lastreport_cpm-_period_begin); value = value / (_lastreport_cpm-_period_begin);
#if SENSOR_DEBUG #if SENSOR_DEBUG
char data[128]; char buffer[10];
dtostrf(value, 1-sizeof(buffer), 4, buffer);
char data[128]; char buffer[32];
dtostrf(value, 1, 4, buffer);
snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | CPM: %s", _ticks, (_lastreport_cpm-_period_begin), buffer); snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | CPM: %s", _ticks, (_lastreport_cpm-_period_begin), buffer);
DEBUG_MSG("[GEIGER] %s\n", data); DEBUG_MSG("[GEIGER] %s\n", data);
#endif #endif
@ -154,8 +154,8 @@ double value(unsigned char index) {
double value = _ticks * 60000 / _cpm2sievert; double value = _ticks * 60000 / _cpm2sievert;
value = value / (_lastreport_sv-_period_begin); value = value / (_lastreport_sv-_period_begin);
#if SENSOR_DEBUG #if SENSOR_DEBUG
char data[128]; char buffer[10];
dtostrf(value, 1-sizeof(buffer), 4, buffer);
char data[128]; char buffer[32];
dtostrf(value, 1, 4, buffer);
snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | µSievert: %s", _ticks, (_lastreport_sv-_period_begin), buffer); snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | µSievert: %s", _ticks, (_lastreport_sv-_period_begin), buffer);
DEBUG_MSG("[GEIGER] %s\n", data); DEBUG_MSG("[GEIGER] %s\n", data);
#endif #endif


+ 2
- 2
code/espurna/sensors/HLW8012Sensor.h View File

@ -158,7 +158,7 @@ class HLW8012Sensor : public BaseSensor {
// Descriptive name of the sensor // Descriptive name of the sensor
String description() { String description() {
char buffer[25];
char buffer[28];
snprintf(buffer, sizeof(buffer), "HLW8012 @ GPIO(%u,%u,%u)", _sel, _cf, _cf1); snprintf(buffer, sizeof(buffer), "HLW8012 @ GPIO(%u,%u,%u)", _sel, _cf, _cf1);
return String(buffer); return String(buffer);
} }
@ -170,7 +170,7 @@ class HLW8012Sensor : public BaseSensor {
// Address of the sensor (it could be the GPIO or I2C address) // Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) { String address(unsigned char index) {
char buffer[10];
char buffer[12];
snprintf(buffer, sizeof(buffer), "%u:%u:%u", _sel, _cf, _cf1); snprintf(buffer, sizeof(buffer), "%u:%u:%u", _sel, _cf, _cf1);
return String(buffer); return String(buffer);
} }


+ 25
- 9
code/espurna/sensors/PMSX003Sensor.h View File

@ -276,8 +276,6 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
return; return;
} }
_error = SENSOR_ERROR_OK;
#if PMS_SMART_SLEEP #if PMS_SMART_SLEEP
unsigned int readCycle; unsigned int readCycle;
if (_readCount++ > 30) { if (_readCount++ > 30) {
@ -304,22 +302,40 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
uint16_t data[PMS_DATA_MAX]; uint16_t data[PMS_DATA_MAX];
if (readData(data, pms_specs[_type].data_count)) { if (readData(data, pms_specs[_type].data_count)) {
if (_type == PMS_TYPE_5003ST) { if (_type == PMS_TYPE_5003ST) {
_slot_values[0] = data[4];
_slot_values[1] = (double)data[13] / 10;
_slot_values[2] = (double)data[14] / 10;
_slot_values[3] = (double)data[12] / 1000;
if (data[14] > 10 && data[14] < 1000 && data[13] < 1000) {
_slot_values[0] = data[4];
_slot_values[1] = (double)data[13] / 10;
_slot_values[2] = (double)data[14] / 10;
_slot_values[3] = (double)data[12] / 1000;
_error = SENSOR_ERROR_OK;
} else {
_error = SENSOR_ERROR_OUT_OF_RANGE;
#if SENSOR_DEBUG
DEBUG_MSG("[SENSOR] %s: Invalid temperature=%d humidity=%d.\n", pms_specs[_type].name, (int)data[13], (int)data[14]);
#endif
}
} else if (_type == PMS_TYPE_5003S) { } else if (_type == PMS_TYPE_5003S) {
_slot_values[0] = data[4]; _slot_values[0] = data[4];
_slot_values[1] = data[5]; _slot_values[1] = data[5];
_slot_values[2] = (double)data[12] / 1000; _slot_values[2] = (double)data[12] / 1000;
_error = SENSOR_ERROR_OK;
} else if (_type == PMS_TYPE_5003T) { } else if (_type == PMS_TYPE_5003T) {
_slot_values[0] = data[4];
_slot_values[1] = (double)data[10] / 10;
_slot_values[2] = (double)data[11] / 10;
if (data[11] > 10 && data[11] < 1000 && data[10] < 1000) {
_slot_values[0] = data[4];
_slot_values[1] = (double)data[10] / 10;
_slot_values[2] = (double)data[11] / 10;
_error = SENSOR_ERROR_OK;
} else {
_error = SENSOR_ERROR_OUT_OF_RANGE;
#if SENSOR_DEBUG
DEBUG_MSG("[SENSOR] %s: Invalid temperature=%d humidity=%d.\n", pms_specs[_type].name, (int)data[10], (int)data[11]);
#endif
}
} else { } else {
_slot_values[0] = data[3]; _slot_values[0] = data[3];
_slot_values[1] = data[4]; _slot_values[1] = data[4];
_slot_values[2] = data[5]; _slot_values[2] = data[5];
_error = SENSOR_ERROR_OK;
} }
} }


+ 11
- 1
code/espurna/sensors/PulseMeterSensor.h View File

@ -43,6 +43,10 @@ class PulseMeterSensor : public BaseSensor {
if (ratio > 0) _ratio = ratio; if (ratio > 0) _ratio = ratio;
} }
void setInterruptMode(unsigned char interrupt_mode) {
_interrupt_mode = interrupt_mode;
}
void setDebounceTime(unsigned long debounce) { void setDebounceTime(unsigned long debounce) {
_debounce = debounce; _debounce = debounce;
} }
@ -57,6 +61,10 @@ class PulseMeterSensor : public BaseSensor {
return _ratio; return _ratio;
} }
unsigned char getInterruptMode() {
return _interrupt_mode;
}
unsigned long getDebounceTime() { unsigned long getDebounceTime() {
return _debounce; return _debounce;
} }
@ -144,7 +152,7 @@ class PulseMeterSensor : public BaseSensor {
if (_gpio != _previous) { if (_gpio != _previous) {
if (_previous != GPIO_NONE) _detach(_previous); if (_previous != GPIO_NONE) _detach(_previous);
_attach(this, _gpio, PULSEMETER_INTERRUPT_ON);
_attach(this, _gpio, _interrupt_mode);
_previous = _gpio; _previous = _gpio;
} }
@ -171,6 +179,8 @@ class PulseMeterSensor : public BaseSensor {
unsigned long _previous_pulses = 0; unsigned long _previous_pulses = 0;
unsigned long _previous_time = 0; unsigned long _previous_time = 0;
unsigned char _interrupt_mode = FALLING;
}; };


+ 2
- 2
code/espurna/sensors/SenseAirSensor.h View File

@ -202,17 +202,17 @@ class SenseAirSensor : public BaseSensor, SenseAir {
return; return;
} }
_error = SENSOR_ERROR_OK;
unsigned int co2 = readCo2(); unsigned int co2 = readCo2();
if (co2 >= 5000 || co2 < 100) if (co2 >= 5000 || co2 < 100)
{ {
_co2 = _lastCo2; _co2 = _lastCo2;
_error = SENSOR_ERROR_OUT_OF_RANGE;
} }
else else
{ {
_co2 = (co2 > _lastCo2 + 2000) ? _lastCo2 : co2; _co2 = (co2 > _lastCo2 + 2000) ? _lastCo2 : co2;
_lastCo2 = co2; _lastCo2 = co2;
_error = SENSOR_ERROR_OK;
} }
} }


+ 28
- 0
code/espurna/settings.ino View File

@ -215,6 +215,23 @@ bool settingsRestoreJson(JsonObject& data) {
} }
bool settingsRestoreJson(char* json_string, size_t json_buffer_size = 1024) {
// XXX: as of right now, arduinojson cannot trigger callbacks for each key individually
// Manually separating kv pairs can allow to parse only a small chunk, since we know that there is only string type used (even with bools / ints). Can be problematic when parsing data that was not generated by us.
// Current parsing method is limited only by keys (~sizeof(uintptr_t) bytes per key, data is not copied when string is non-const)
DynamicJsonBuffer jsonBuffer(json_buffer_size);
JsonObject& root = jsonBuffer.parseObject((char *) json_string);
if (!root.success()) {
DEBUG_MSG_P(PSTR("[SETTINGS] JSON parsing error\n"));
return false;
}
return settingsRestoreJson(root);
}
void settingsGetJson(JsonObject& root) { void settingsGetJson(JsonObject& root) {
// Get sorted list of keys // Get sorted list of keys
@ -228,6 +245,17 @@ void settingsGetJson(JsonObject& root) {
} }
void settingsProcessConfig(const settings_cfg_list_t& config, settings_filter_t filter) {
for (auto& entry : config) {
String value = getSetting(entry.key, entry.default_value);
if (filter) {
value = filter(value);
}
if (value.equals(entry.setting)) continue;
entry.setting = std::move(value);
}
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Initialization // Initialization
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------


+ 42
- 0
code/espurna/static/digicert_evroot_pem.h View File

@ -0,0 +1,42 @@
// https://github.com root issuer
// Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA
// Validity
// Not Before: Oct 22 12:00:00 2013 GMT
// Not After : Oct 22 12:00:00 2028 GMT
// Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 Extended Validation Server CA
#pragma once
#include <pgmspace.h>
const char PROGMEM _ssl_digicert_ev_root_ca[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW
YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY
uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/
LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy
/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh
cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k
8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy
dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2
MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW
gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh
hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg
4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa
2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs
1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1
oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn
8TUoE6smftX3eg==
-----END CERTIFICATE-----
)EOF";

+ 94
- 0
code/espurna/static/letsencrypt_isrgroot_pem.h View File

@ -0,0 +1,94 @@
// ISRG Root X1 (self-signed)
// from https://letsencrypt.org/certs/isrgrootx1.pem.txt
// Note: LetsEncrypt will only start using this root certificate to sign
// certificates after July 8, 2020. Any certificate issued before this date
// uses the X3 intermediate certificate down below.
// See: https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html
// Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
// Validity
// Not Before: Jun 4 11:04:38 2015 GMT
// Not After : Jun 4 11:04:38 2035 GMT
// Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
#pragma once
#include <pgmspace.h>
const char PROGMEM _ssl_letsencrypt_isrg_x1_ca[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";
// Lets Encrypt Authority X3 (Signed by ISRG Root X1)
// from https://letsencrypt.org/certs/letsencryptauthorityx3.pem.txt
// Issuer: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
// Validity
// Not Before: Oct 6 15:43:55 2016 GMT
// Not After : Oct 6 15:43:55 2021 GMT
const char PROGMEM _ssl_letsencrypt_isrg_x3_ca[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
-----END CERTIFICATE-----
)EOF";

+ 5
- 3
code/espurna/system.ino View File

@ -163,8 +163,10 @@ void _systemSetupHeartbeat() {
} }
#if WEB_SUPPORT #if WEB_SUPPORT
bool _systemWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "hb", 2) == 0);
bool _systemWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "sys", 3) == 0) return true;
if (strncmp(key, "hb", 2) == 0) return true;
return false;
} }
#endif #endif
@ -259,7 +261,7 @@ void systemSetup() {
#endif #endif
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnReceiveRegister(_systemWebSocketOnReceive);
wsRegister().onKeyCheck(_systemWebSocketOnKeyCheck);
#endif #endif
// Init device-specific hardware // Init device-specific hardware


+ 136
- 56
code/espurna/telnet.ino View File

@ -10,10 +10,16 @@ Parts of the code have been borrowed from Thomas Sarlandie's NetServer
#if TELNET_SUPPORT #if TELNET_SUPPORT
#include <ESPAsyncTCP.h>
#if TELNET_SERVER == TELNET_SERVER_WIFISERVER
#include <ESP8266WiFi.h>
WiFiServer _telnetServer = WiFiServer(TELNET_PORT);
std::unique_ptr<WiFiClient> _telnetClients[TELNET_MAX_CLIENTS];
#else
#include <ESPAsyncTCP.h>
AsyncServer _telnetServer = AsyncServer(TELNET_PORT);
std::unique_ptr<AsyncClient> _telnetClients[TELNET_MAX_CLIENTS];
#endif
AsyncServer * _telnetServer;
AsyncClient * _telnetClients[TELNET_MAX_CLIENTS];
bool _telnetFirst = true; bool _telnetFirst = true;
bool _telnetAuth = TELNET_AUTHENTICATION; bool _telnetAuth = TELNET_AUTHENTICATION;
@ -25,12 +31,11 @@ bool _telnetClientsAuth[TELNET_MAX_CLIENTS];
#if WEB_SUPPORT #if WEB_SUPPORT
bool _telnetWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _telnetWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "telnet", 6) == 0); return (strncmp(key, "telnet", 6) == 0);
} }
void _telnetWebSocketOnSend(JsonObject& root) {
root["telnetVisible"] = 1;
void _telnetWebSocketOnConnected(JsonObject& root) {
root["telnetSTA"] = getSetting("telnetSTA", TELNET_STA).toInt() == 1; root["telnetSTA"] = getSetting("telnetSTA", TELNET_STA).toInt() == 1;
root["telnetAuth"] = getSetting("telnetAuth", TELNET_AUTHENTICATION).toInt() == 1; root["telnetAuth"] = getSetting("telnetAuth", TELNET_AUTHENTICATION).toInt() == 1;
} }
@ -38,9 +43,11 @@ void _telnetWebSocketOnSend(JsonObject& root) {
#endif #endif
void _telnetDisconnect(unsigned char clientId) { void _telnetDisconnect(unsigned char clientId) {
_telnetClients[clientId]->free();
delete _telnetClients[clientId];
_telnetClients[clientId] = NULL;
// ref: we are called from onDisconnect, async is already stopped
#if TELNET_SERVER == TELNET_SERVER_WIFISERVER
_telnetClients[clientId]->stop();
#endif
_telnetClients[clientId] = nullptr;
wifiReconnectCheck(); wifiReconnectCheck();
DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId); DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId);
} }
@ -74,7 +81,6 @@ bool _telnetWrite(unsigned char clientId, const char * message) {
} }
void _telnetData(unsigned char clientId, void *data, size_t len) { void _telnetData(unsigned char clientId, void *data, size_t len) {
// Skip first message since it's always garbage // Skip first message since it's always garbage
if (_telnetFirst) { if (_telnetFirst) {
_telnetFirst = false; _telnetFirst = false;
@ -87,13 +93,13 @@ void _telnetData(unsigned char clientId, void *data, size_t len) {
// C-d is sent as two bytes (sometimes repeating) // C-d is sent as two bytes (sometimes repeating)
if (len >= 2) { if (len >= 2) {
if ((p[0] == 0xFF) && (p[1] == 0xEC)) { if ((p[0] == 0xFF) && (p[1] == 0xEC)) {
_telnetClients[clientId]->close(true);
_telnetDisconnect(clientId);
return; return;
} }
} }
if ((strncmp(p, "close", 5) == 0) || (strncmp(p, "quit", 4) == 0)) { if ((strncmp(p, "close", 5) == 0) || (strncmp(p, "quit", 4) == 0)) {
_telnetClients[clientId]->close();
_telnetDisconnect(clientId);
return; return;
} }
@ -108,10 +114,10 @@ void _telnetData(unsigned char clientId, void *data, size_t len) {
String password = getAdminPass(); String password = getAdminPass();
if (strncmp(p, password.c_str(), password.length()) == 0) { if (strncmp(p, password.c_str(), password.length()) == 0) {
DEBUG_MSG_P(PSTR("[TELNET] Client #%d authenticated\n"), clientId); DEBUG_MSG_P(PSTR("[TELNET] Client #%d authenticated\n"), clientId);
_telnetWrite(clientId, "Welcome!\n");
_telnetWrite(clientId, "Password correct, welcome!\n");
_telnetClientsAuth[clientId] = true; _telnetClientsAuth[clientId] = true;
} else { } else {
_telnetWrite(clientId, "Password: ");
_telnetWrite(clientId, "Password (try again): ");
} }
return; return;
} }
@ -120,13 +126,101 @@ void _telnetData(unsigned char clientId, void *data, size_t len) {
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
terminalInject(data, len); terminalInject(data, len);
#endif #endif
}
void _telnetNotifyConnected(unsigned char i) {
DEBUG_MSG_P(PSTR("[TELNET] Client #%u connected\n"), i);
// If there is no terminal support automatically dump info and crash data
#if TERMINAL_SUPPORT == 0
info();
wifiDebug();
crashDump();
crashClear();
#endif
#ifdef ESPURNA_CORE
_telnetClientsAuth[i] = true;
#else
_telnetClientsAuth[i] = !_telnetAuth;
if (_telnetAuth) {
if (getAdminPass().length()) {
_telnetWrite(i, "Password: ");
} else {
_telnetClientsAuth[i] = true;
}
}
#endif
_telnetFirst = true;
wifiReconnectCheck();
} }
void _telnetNewClient(AsyncClient *client) {
#if TELNET_SERVER == TELNET_SERVER_WIFISERVER
if (client->localIP() != WiFi.softAPIP()) {
void _telnetLoop() {
if (_telnetServer.hasClient()) {
int i;
for (i = 0; i < TELNET_MAX_CLIENTS; i++) {
if (!_telnetClients[i] || !_telnetClients[i]->connected()) {
_telnetClients[i] = std::unique_ptr<WiFiClient>(new WiFiClient(_telnetServer.available()));
if (_telnetClients[i]->localIP() != WiFi.softAPIP()) {
// Telnet is always available for the ESPurna Core image
#ifdef ESPURNA_CORE
bool telnetSTA = true;
#else
bool telnetSTA = getSetting("telnetSTA", TELNET_STA).toInt() == 1;
#endif
if (!telnetSTA) {
DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n"));
_telnetDisconnect(i);
return;
}
}
_telnetNotifyConnected(i);
break;
}
}
//no free/disconnected spot so reject
if (i == TELNET_MAX_CLIENTS) {
DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n"));
_telnetServer.available().stop();
return;
}
}
for (int i = 0; i < TELNET_MAX_CLIENTS; i++) {
if (_telnetClients[i]) {
// Handle client timeouts
if (!_telnetClients[i]->connected()) {
_telnetDisconnect(i);
} else {
// Read data from clients
while (_telnetClients[i] && _telnetClients[i]->available()) {
char data[TERMINAL_BUFFER_SIZE];
size_t len = _telnetClients[i]->available();
unsigned int r = _telnetClients[i]->readBytes(data, min(sizeof(data), len));
_telnetData(i, data, r);
}
}
}
}
}
#else // TELNET_SERVER_ASYNC
void _telnetNewClient(AsyncClient* client) {
if (client->localIP() != WiFi.softAPIP()) {
// Telnet is always available for the ESPurna Core image // Telnet is always available for the ESPurna Core image
#ifdef ESPURNA_CORE #ifdef ESPURNA_CORE
bool telnetSTA = true; bool telnetSTA = true;
@ -137,76 +231,54 @@ void _telnetNewClient(AsyncClient *client) {
if (!telnetSTA) { if (!telnetSTA) {
DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n")); DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n"));
client->onDisconnect([](void *s, AsyncClient *c) { client->onDisconnect([](void *s, AsyncClient *c) {
c->free();
delete c; delete c;
}); });
client->close(true); client->close(true);
return; return;
} }
} }
for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) { for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) {
if (!_telnetClients[i] || !_telnetClients[i]->connected()) { if (!_telnetClients[i] || !_telnetClients[i]->connected()) {
_telnetClients[i] = client;
_telnetClients[i] = std::unique_ptr<AsyncClient>(client);
client->onAck([i](void *s, AsyncClient *c, size_t len, uint32_t time) {
_telnetClients[i]->onAck([i](void *s, AsyncClient *c, size_t len, uint32_t time) {
}, 0); }, 0);
client->onData([i](void *s, AsyncClient *c, void *data, size_t len) {
_telnetClients[i]->onData([i](void *s, AsyncClient *c, void *data, size_t len) {
_telnetData(i, data, len); _telnetData(i, data, len);
}, 0); }, 0);
client->onDisconnect([i](void *s, AsyncClient *c) {
_telnetClients[i]->onDisconnect([i](void *s, AsyncClient *c) {
_telnetDisconnect(i); _telnetDisconnect(i);
}, 0); }, 0);
client->onError([i](void *s, AsyncClient *c, int8_t error) {
_telnetClients[i]->onError([i](void *s, AsyncClient *c, int8_t error) {
DEBUG_MSG_P(PSTR("[TELNET] Error %s (%d) on client #%u\n"), c->errorToString(error), error, i); DEBUG_MSG_P(PSTR("[TELNET] Error %s (%d) on client #%u\n"), c->errorToString(error), error, i);
}, 0); }, 0);
client->onTimeout([i](void *s, AsyncClient *c, uint32_t time) {
_telnetClients[i]->onTimeout([i](void *s, AsyncClient *c, uint32_t time) {
DEBUG_MSG_P(PSTR("[TELNET] Timeout on client #%u at %lu\n"), i, time); DEBUG_MSG_P(PSTR("[TELNET] Timeout on client #%u at %lu\n"), i, time);
c->close(); c->close();
}, 0); }, 0);
DEBUG_MSG_P(PSTR("[TELNET] Client #%u connected\n"), i);
// If there is no terminal support automatically dump info and crash data
#if TERMINAL_SUPPORT == 0
info();
wifiDebug();
crashDump();
crashClear();
#endif
#ifdef ESPURNA_CORE
_telnetClientsAuth[i] = true;
#else
_telnetClientsAuth[i] = !_telnetAuth;
if (_telnetAuth) _telnetWrite(i, "Password: ");
#endif
_telnetFirst = true;
wifiReconnectCheck();
_telnetNotifyConnected(i);
return; return;
} }
} }
DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n")); DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n"));
client->onDisconnect([](void *s, AsyncClient *c) { client->onDisconnect([](void *s, AsyncClient *c) {
c->free();
delete c; delete c;
}); });
client->close(true); client->close(true);
} }
#endif // TELNET_SERVER == TELNET_SERVER_WIFISERVER
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Public API // Public API
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -228,22 +300,30 @@ void _telnetConfigure() {
} }
void telnetSetup() { void telnetSetup() {
_telnetServer = new AsyncServer(TELNET_PORT);
_telnetServer->onClient([](void *s, AsyncClient* c) {
_telnetNewClient(c);
}, 0);
_telnetServer->begin();
#if TELNET_SERVER == TELNET_SERVER_WIFISERVER
espurnaRegisterLoop(_telnetLoop);
_telnetServer.setNoDelay(true);
_telnetServer.begin();
#else
_telnetServer.onClient([](void *s, AsyncClient* c) {
_telnetNewClient(c);
}, 0);
_telnetServer.begin();
#endif
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_telnetWebSocketOnSend);
wsOnReceiveRegister(_telnetWebSocketOnReceive);
wsRegister()
.onVisible([](JsonObject& root) { root["telnetVisible"] = 1; })
.onConnected(_telnetWebSocketOnConnected)
.onKeyCheck(_telnetWebSocketOnKeyCheck);
#endif #endif
espurnaRegisterReload(_telnetConfigure); espurnaRegisterReload(_telnetConfigure);
_telnetConfigure(); _telnetConfigure();
DEBUG_MSG_P(PSTR("[TELNET] Listening on port %d\n"), TELNET_PORT);
DEBUG_MSG_P(PSTR("[TELNET] %s server, Listening on port %d\n"),
(TELNET_SERVER == TELNET_SERVER_WIFISERVER) ? "Sync" : "Async",
TELNET_PORT);
} }


+ 167
- 22
code/espurna/terminal.ino View File

@ -12,6 +12,7 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "libs/EmbedisWrap.h" #include "libs/EmbedisWrap.h"
#include <Stream.h> #include <Stream.h>
#include "libs/StreamInjector.h" #include "libs/StreamInjector.h"
#include "libs/HeapStats.h"
StreamInjector _serial = StreamInjector(TERMINAL_BUFFER_SIZE); StreamInjector _serial = StreamInjector(TERMINAL_BUFFER_SIZE);
EmbedisWrap embedis(_serial, TERMINAL_BUFFER_SIZE); EmbedisWrap embedis(_serial, TERMINAL_BUFFER_SIZE);
@ -78,15 +79,92 @@ void _terminalKeysCommand() {
} }
void _terminalInitCommand() {
#if LWIP_VERSION_MAJOR != 1
// not yet CONNECTING or LISTENING
extern struct tcp_pcb *tcp_bound_pcbs;
// accepting or sending data
extern struct tcp_pcb *tcp_active_pcbs;
// // TIME-WAIT status
extern struct tcp_pcb *tcp_tw_pcbs;
String _terminalPcbStateToString(const unsigned char state) {
switch (state) {
case 0: return F("CLOSED");
case 1: return F("LISTEN");
case 2: return F("SYN_SENT");
case 3: return F("SYN_RCVD");
case 4: return F("ESTABLISHED");
case 5: return F("FIN_WAIT_1");
case 6: return F("FIN_WAIT_2");
case 7: return F("CLOSE_WAIT");
case 8: return F("CLOSING");
case 9: return F("LAST_ACK");
case 10: return F("TIME_WAIT");
default: return String(int(state));
};
}
#if DEBUG_SUPPORT
terminalRegisterCommand(F("CRASH"), [](Embedis* e) {
crashDump();
crashClear();
terminalOK();
});
void _terminalPrintTcpPcb(tcp_pcb* pcb) {
char remote_ip[32] = {0};
char local_ip[32] = {0};
inet_ntoa_r((pcb->local_ip), local_ip, sizeof(local_ip));
inet_ntoa_r((pcb->remote_ip), remote_ip, sizeof(remote_ip));
DEBUG_MSG_P(PSTR("state=%s local=%s:%u remote=%s:%u snd_queuelen=%u lastack=%u send_wnd=%u rto=%u\n"),
_terminalPcbStateToString(pcb->state).c_str(),
local_ip, pcb->local_port,
remote_ip, pcb->remote_port,
pcb->snd_queuelen, pcb->lastack,
pcb->snd_wnd, pcb->rto
);
}
void _terminalPrintTcpPcbs() {
tcp_pcb *pcb;
//DEBUG_MSG_P(PSTR("Active PCB states:\n"));
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) {
_terminalPrintTcpPcb(pcb);
}
//DEBUG_MSG_P(PSTR("TIME-WAIT PCB states:\n"));
for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) {
_terminalPrintTcpPcb(pcb);
}
//DEBUG_MSG_P(PSTR("BOUND PCB states:\n"));
for (pcb = tcp_bound_pcbs; pcb != NULL; pcb = pcb->next) {
_terminalPrintTcpPcb(pcb);
}
}
void _terminalPrintDnsResult(const char* name, const ip_addr_t* address) {
// TODO fix asynctcp building with lwip-ipv6
/*
#if LWIP_IPV6
if (IP_IS_V6(address)) {
DEBUG_MSG_P(PSTR("[DNS] %s has IPV6 address %s\n"), name, ip6addr_ntoa(ip_2_ip6(address)));
}
#endif #endif
*/
DEBUG_MSG_P(PSTR("[DNS] %s has address %s\n"), name, ipaddr_ntoa(address));
}
void _terminalDnsFound(const char* name, const ip_addr_t* result, void*) {
if (!result) {
DEBUG_MSG_P(PSTR("[DNS] %s not found\n"), name);
return;
}
_terminalPrintDnsResult(name, result);
}
#endif // LWIP_VERSION_MAJOR != 1
void _terminalInitCommand() {
terminalRegisterCommand(F("COMMANDS"), [](Embedis* e) { terminalRegisterCommand(F("COMMANDS"), [](Embedis* e) {
_terminalHelpCommand(); _terminalHelpCommand();
@ -106,30 +184,39 @@ void _terminalInitCommand() {
}); });
terminalRegisterCommand(F("GPIO"), [](Embedis* e) { terminalRegisterCommand(F("GPIO"), [](Embedis* e) {
int pin = -1;
if (e->argc < 2) { if (e->argc < 2) {
terminalError(F("Wrong arguments"));
return;
DEBUG_MSG("Printing all GPIO pins:\n");
} else {
pin = String(e->argv[1]).toInt();
if (!gpioValid(pin)) {
terminalError(F("Invalid GPIO pin"));
return;
}
if (e->argc > 2) {
bool state = String(e->argv[2]).toInt() == 1;
digitalWrite(pin, state);
}
} }
int pin = String(e->argv[1]).toInt();
//if (!gpioValid(pin)) {
// terminalError(F("Invalid GPIO"));
// return;
//}
if (e->argc > 2) {
bool state = String(e->argv[2]).toInt() == 1;
digitalWrite(pin, state);
for (int i = 0; i <= 15; i++) {
if (gpioValid(i) && (pin == -1 || pin == i)) {
DEBUG_MSG_P(PSTR("GPIO %s pin %d is %s\n"), GPEP(i) ? "output" : "input", i, digitalRead(i) == HIGH ? "HIGH" : "LOW");
}
} }
DEBUG_MSG_P(PSTR("GPIO %d is %s\n"), pin, digitalRead(pin) == HIGH ? "HIGH" : "LOW");
terminalOK(); terminalOK();
}); });
terminalRegisterCommand(F("HEAP"), [](Embedis* e) { terminalRegisterCommand(F("HEAP"), [](Embedis* e) {
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
infoHeapStats();
terminalOK(); terminalOK();
}); });
terminalRegisterCommand(F("STACK"), [](Embedis* e) { terminalRegisterCommand(F("STACK"), [](Embedis* e) {
infoMemory("Stack", 4096, getFreeStack());
infoMemory("Stack", CONT_STACKSIZE, getFreeStack());
terminalOK(); terminalOK();
}); });
@ -190,9 +277,10 @@ void _terminalInitCommand() {
}); });
terminalRegisterCommand(F("CONFIG"), [](Embedis* e) { terminalRegisterCommand(F("CONFIG"), [](Embedis* e) {
DynamicJsonBuffer jsonBuffer;
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
settingsGetJson(root); settingsGetJson(root);
// XXX: replace with streaming
String output; String output;
root.printTo(output); root.printTo(output);
DEBUG_MSG(output.c_str()); DEBUG_MSG(output.c_str());
@ -202,9 +290,56 @@ void _terminalInitCommand() {
#if not SETTINGS_AUTOSAVE #if not SETTINGS_AUTOSAVE
terminalRegisterCommand(F("SAVE"), [](Embedis* e) { terminalRegisterCommand(F("SAVE"), [](Embedis* e) {
eepromCommit(); eepromCommit();
DEBUG_MSG_P(PSTR("\n+OK\n"));
terminalOK();
});
#endif
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
terminalRegisterCommand(F("MFLN.PROBE"), [](Embedis* e) {
if (e->argc != 3) {
terminalError(F("[url] [value]"));
return;
}
URL _url(e->argv[1]);
uint16_t requested_mfln = atol(e->argv[2]);
auto client = std::make_unique<BearSSL::WiFiClientSecure>();
client->setInsecure();
if (client->probeMaxFragmentLength(_url.host.c_str(), _url.port, requested_mfln)) {
terminalOK();
} else {
terminalError(F("Buffer size not supported"));
}
}); });
#endif #endif
#if LWIP_VERSION_MAJOR != 1
terminalRegisterCommand(F("HOST"), [](Embedis* e) {
if (e->argc != 2) {
terminalError(F("HOST [hostname]"));
return;
}
ip_addr_t result;
auto error = dns_gethostbyname(e->argv[1], &result, _terminalDnsFound, nullptr);
if (error == ERR_OK) {
_terminalPrintDnsResult(e->argv[1], &result);
terminalOK();
return;
} else if (error != ERR_INPROGRESS) {
DEBUG_MSG_P(PSTR("[DNS] dns_gethostbyname error: %s\n"), lwip_strerr(error));
return;
}
});
terminalRegisterCommand(F("NETSTAT"), [](Embedis*) {
_terminalPrintTcpPcbs();
});
#endif // LWIP_VERSION_MAJOR != 1
} }
@ -241,6 +376,11 @@ void terminalInject(void *data, size_t len) {
_serial.inject((char *) data, len); _serial.inject((char *) data, len);
} }
void terminalInject(char ch) {
_serial.inject(ch);
}
Stream & terminalSerial() { Stream & terminalSerial() {
return (Stream &) _serial; return (Stream &) _serial;
} }
@ -268,6 +408,11 @@ void terminalSetup() {
#endif #endif
}); });
#if WEB_SUPPORT
wsRegister()
.onVisible([](JsonObject& root) { root["cmdVisible"] = 1; });
#endif
_terminalInitCommand(); _terminalInitCommand();
#if SERIAL_RX_ENABLED #if SERIAL_RX_ENABLED


+ 16
- 15
code/espurna/thermostat.ino View File

@ -142,13 +142,13 @@ void updateOperationMode() {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
void updateRemoteTemp(bool remote_temp_actual) { void updateRemoteTemp(bool remote_temp_actual) {
#if WEB_SUPPORT #if WEB_SUPPORT
char tmp_str[6];
char tmp_str[16];
if (remote_temp_actual) { if (remote_temp_actual) {
dtostrf(_remote_temp.temp, 1-sizeof(tmp_str), 1, tmp_str);
dtostrf(_remote_temp.temp, 1, 1, tmp_str);
} else { } else {
strcpy(tmp_str, "\"?\""); strcpy(tmp_str, "\"?\"");
} }
char buffer[100];
char buffer[128];
snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str); snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str);
wsSend(buffer); wsSend(buffer);
#endif #endif
@ -298,7 +298,7 @@ void _thermostatReload() {
#if WEB_SUPPORT #if WEB_SUPPORT
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
void _thermostatWebSocketOnSend(JsonObject& root) {
void _thermostatWebSocketOnConnected(JsonObject& root) {
root["thermostatEnabled"] = thermostatEnabled(); root["thermostatEnabled"] = thermostatEnabled();
root["thermostatMode"] = thermostatModeCooler(); root["thermostatMode"] = thermostatModeCooler();
root["thermostatVisible"] = 1; root["thermostatVisible"] = 1;
@ -328,7 +328,7 @@ void _thermostatWebSocketOnSend(JsonObject& root) {
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
bool _thermostatWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _thermostatWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, NAME_THERMOSTAT_ENABLED, strlen(NAME_THERMOSTAT_ENABLED)) == 0) return true; if (strncmp(key, NAME_THERMOSTAT_ENABLED, strlen(NAME_THERMOSTAT_ENABLED)) == 0) return true;
if (strncmp(key, NAME_THERMOSTAT_MODE, strlen(NAME_THERMOSTAT_MODE)) == 0) return true; if (strncmp(key, NAME_THERMOSTAT_MODE, strlen(NAME_THERMOSTAT_MODE)) == 0) return true;
if (strncmp(key, NAME_TEMP_RANGE_MIN, strlen(NAME_TEMP_RANGE_MIN)) == 0) return true; if (strncmp(key, NAME_TEMP_RANGE_MIN, strlen(NAME_TEMP_RANGE_MIN)) == 0) return true;
@ -358,9 +358,10 @@ void thermostatSetup() {
// Websockets // Websockets
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_thermostatWebSocketOnSend);
wsOnReceiveRegister(_thermostatWebSocketOnReceive);
wsOnActionRegister(_thermostatWebSocketOnAction);
wsRegister()
.onConnected(_thermostatWebSocketOnConnected)
.onKeyCheck(_thermostatWebSocketOnKeyCheck)
.onAction(_thermostatWebSocketOnAction);
#endif #endif
espurnaRegisterLoop(thermostatLoop); espurnaRegisterLoop(thermostatLoop);
@ -386,8 +387,8 @@ void setThermostatState(bool state) {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
void debugPrintSwitch(bool state, double temp) { void debugPrintSwitch(bool state, double temp) {
char tmp_str[6];
dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str);
char tmp_str[16];
dtostrf(temp, 1, 1, tmp_str);
DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"), DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"),
state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, _thermostat_mode_cooler ? "COOLER" : "HEATER", relayStatus(THERMOSTAT_RELAY) ? "ON" : "OFF", millis() - _thermostat.last_switch); state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, _thermostat_mode_cooler ? "COOLER" : "HEATER", relayStatus(THERMOSTAT_RELAY) ? "ON" : "OFF", millis() - _thermostat.last_switch);
} }
@ -485,8 +486,8 @@ double getLocalTemperature() {
for (byte i=0; i<magnitudeCount(); i++) { for (byte i=0; i<magnitudeCount(); i++) {
if (magnitudeType(i) == MAGNITUDE_TEMPERATURE) { if (magnitudeType(i) == MAGNITUDE_TEMPERATURE) {
double temp = magnitudeValue(i); double temp = magnitudeValue(i);
char tmp_str[6];
dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str);
char tmp_str[16];
dtostrf(temp, 1, 1, tmp_str);
DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalTemperature temp: %s\n"), tmp_str); DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalTemperature temp: %s\n"), tmp_str);
return temp > -0.1 && temp < 0.1 ? DBL_MIN : temp; return temp > -0.1 && temp < 0.1 ? DBL_MIN : temp;
} }
@ -501,8 +502,8 @@ double getLocalHumidity() {
for (byte i=0; i<magnitudeCount(); i++) { for (byte i=0; i<magnitudeCount(); i++) {
if (magnitudeType(i) == MAGNITUDE_HUMIDITY) { if (magnitudeType(i) == MAGNITUDE_HUMIDITY) {
double hum = magnitudeValue(i); double hum = magnitudeValue(i);
char tmp_str[4];
dtostrf(hum, 1-sizeof(tmp_str), 0, tmp_str);
char tmp_str[16];
dtostrf(hum, 1, 0, tmp_str);
DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalHumidity hum: %s\%\n"), tmp_str); DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalHumidity hum: %s\%\n"), tmp_str);
return hum > -0.1 && hum < 0.1 ? DBL_MIN : hum; return hum > -0.1 && hum < 0.1 ? DBL_MIN : hum;
} }
@ -822,4 +823,4 @@ void displayLoop() {
} }
} }
#endif // THERMOSTAT_DISPLAY_SUPPORT
#endif // THERMOSTAT_DISPLAY_SUPPORT

+ 157
- 77
code/espurna/thinkspeak.ino View File

@ -10,28 +10,35 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
#if THINGSPEAK_USE_ASYNC #if THINGSPEAK_USE_ASYNC
#include <ESPAsyncTCP.h> #include <ESPAsyncTCP.h>
AsyncClient * _tspk_client;
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#endif #endif
#define THINGSPEAK_DATA_BUFFER_SIZE 256
const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM = const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM =
"POST %s HTTP/1.1\r\n" "POST %s HTTP/1.1\r\n"
"Host: %s\r\n" "Host: %s\r\n"
"User-Agent: ESPurna\r\n" "User-Agent: ESPurna\r\n"
"Connection: close\r\n" "Connection: close\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n" "Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: %d\r\n\r\n"
"%s\r\n";
"Content-Length: %d\r\n\r\n";
bool _tspk_enabled = false; bool _tspk_enabled = false;
bool _tspk_clear = false; bool _tspk_clear = false;
char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL}; char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL};
String _tspk_data;
bool _tspk_flush = false; bool _tspk_flush = false;
unsigned long _tspk_last_flush = 0; unsigned long _tspk_last_flush = 0;
unsigned char _tspk_tries = 0;
unsigned char _tspk_tries = THINGSPEAK_TRIES;
#if THINGSPEAK_USE_ASYNC
AsyncClient * _tspk_client;
bool _tspk_connecting = false;
bool _tspk_connected = false;
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -56,13 +63,15 @@ void _tspkBrokerCallback(const unsigned char type, const char * topic, unsigned
#if WEB_SUPPORT #if WEB_SUPPORT
bool _tspkWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _tspkWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
return (strncmp(key, "tspk", 4) == 0); return (strncmp(key, "tspk", 4) == 0);
} }
void _tspkWebSocketOnSend(JsonObject& root) {
void _tspkWebSocketOnVisible(JsonObject& root) {
root["tspkVisible"] = static_cast<unsigned char>(haveRelaysOrSensors());
}
unsigned char visible = 0;
void _tspkWebSocketOnConnected(JsonObject& root) {
root["tspkEnabled"] = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1; root["tspkEnabled"] = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
root["tspkKey"] = getSetting("tspkKey"); root["tspkKey"] = getSetting("tspkKey");
@ -72,15 +81,11 @@ void _tspkWebSocketOnSend(JsonObject& root) {
for (byte i=0; i<relayCount(); i++) { for (byte i=0; i<relayCount(); i++) {
relays.add(getSetting("tspkRelay", i, 0).toInt()); relays.add(getSetting("tspkRelay", i, 0).toInt());
} }
if (relayCount() > 0) visible = 1;
#if SENSOR_SUPPORT #if SENSOR_SUPPORT
_sensorWebSocketMagnitudes(root, "tspk"); _sensorWebSocketMagnitudes(root, "tspk");
visible = visible || (magnitudeCount() > 0);
#endif #endif
root["tspkVisible"] = visible;
} }
#endif #endif
@ -92,50 +97,106 @@ void _tspkConfigure() {
_tspk_enabled = false; _tspk_enabled = false;
setSetting("tspkEnabled", 0); setSetting("tspkEnabled", 0);
} }
if (_tspk_enabled && !_tspk_client) _tspkInitClient();
} }
#if THINGSPEAK_USE_ASYNC #if THINGSPEAK_USE_ASYNC
void _tspkPost(String data) {
enum class tspk_state_t : uint8_t {
NONE,
HEADERS,
BODY
};
if (_tspk_client == NULL) {
_tspk_client = new AsyncClient();
}
_tspk_client->onDisconnect([](void *s, AsyncClient *c) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n"));
_tspk_client->free();
delete _tspk_client;
_tspk_client = NULL;
}, 0);
tspk_state_t _tspk_client_state = tspk_state_t::NONE;
unsigned long _tspk_client_ts = 0;
constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000;
_tspk_client->onTimeout([](void *s, AsyncClient *c, uint32_t time) {
_tspk_client->close(true);
}, 0);
void _tspkInitClient() {
_tspk_client->onData([](void * arg, AsyncClient * c, void * response, size_t len) {
char * b = (char *) response;
b[len] = 0;
char * p = strstr((char *)response, "\r\n\r\n");
unsigned int code = (p != NULL) ? atoi(&p[4]) : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code);
_tspk_client = new AsyncClient();
_tspk_client->onDisconnect([](void * s, AsyncClient * client) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n"));
_tspk_data = "";
_tspk_client_ts = 0;
_tspk_last_flush = millis(); _tspk_last_flush = millis();
if ((0 == code) && (--_tspk_tries > 0)) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n"));
} else {
_tspkClearQueue();
_tspk_connected = false;
_tspk_connecting = false;
_tspk_client_state = tspk_state_t::NONE;
}, nullptr);
_tspk_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Network timeout after %ums\n"), time);
client->close(true);
}, nullptr);
_tspk_client->onPoll([](void * s, AsyncClient * client) {
uint32_t ts = millis() - _tspk_client_ts;
if (ts > THINGSPEAK_CLIENT_TIMEOUT) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] No response after %ums\n"), ts);
client->close(true);
} }
}, nullptr);
_tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) {
char * p = nullptr;
do {
p = nullptr;
switch (_tspk_client_state) {
case tspk_state_t::NONE:
{
p = strnstr(reinterpret_cast<const char *>(response), "HTTP/1.1 200 OK", len);
if (!p) {
client->close(true);
return;
}
_tspk_client_state = tspk_state_t::HEADERS;
continue;
}
case tspk_state_t::HEADERS:
{
p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
if (!p) return;
_tspk_client_state = tspk_state_t::BODY;
}
case tspk_state_t::BODY:
{
if (!p) {
p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
if (!p) return;
}
unsigned int code = (p) ? atoi(&p[4]) : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
if ((0 == code) && _tspk_tries) {
_tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
} else {
_tspkClearQueue();
}
client->close(true);
_tspk_client_state = tspk_state_t::NONE;
}
}
_tspk_client->close(true);
} while (_tspk_client_state != tspk_state_t::NONE);
}, NULL);
}, nullptr);
_tspk_client->onConnect([data](void * arg, AsyncClient * client) {
_tspk_client->onConnect([](void * arg, AsyncClient * client) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%d\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
_tspk_connected = true;
_tspk_connecting = false;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
#if THINGSPEAK_USE_SSL #if THINGSPEAK_USE_SSL
uint8_t fp[20] = {0}; uint8_t fp[20] = {0};
@ -146,27 +207,36 @@ void _tspkPost(String data) {
} }
#endif #endif
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str());
char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()];
snprintf_P(buffer, sizeof(buffer),
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE, THINGSPEAK_REQUEST_TEMPLATE,
THINGSPEAK_URL, THINGSPEAK_URL,
THINGSPEAK_HOST, THINGSPEAK_HOST,
data.length(),
data.c_str()
_tspk_data.length()
); );
client->write(buffer);
client->write(headers);
client->write(_tspk_data.c_str());
}, NULL);
}, nullptr);
}
#if ASYNC_TCP_SSL_ENABLED
void _tspkPost() {
if (_tspk_connected || _tspk_connecting) return;
_tspk_client_ts = millis();
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL); bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL);
#else #else
bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT); bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT);
#endif #endif
_tspk_connecting = connected;
if (!connected) { if (!connected) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
_tspk_client->close(true); _tspk_client->close(true);
@ -176,7 +246,7 @@ void _tspkPost(String data) {
#else // THINGSPEAK_USE_ASYNC #else // THINGSPEAK_USE_ASYNC
void _tspkPost(String data) {
void _tspkPost() {
#if THINGSPEAK_USE_SSL #if THINGSPEAK_USE_SSL
WiFiClientSecure _tspk_client; WiFiClientSecure _tspk_client;
@ -186,35 +256,36 @@ void _tspkPost(String data) {
if (_tspk_client.connect(THINGSPEAK_HOST, THINGSPEAK_PORT)) { if (_tspk_client.connect(THINGSPEAK_HOST, THINGSPEAK_PORT)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%d\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, THINGSPEAK_HOST)) { if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, THINGSPEAK_HOST)) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
} }
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str());
char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()];
snprintf_P(buffer, sizeof(buffer),
DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
snprintf_P(headers, sizeof(headers),
THINGSPEAK_REQUEST_TEMPLATE, THINGSPEAK_REQUEST_TEMPLATE,
THINGSPEAK_URL, THINGSPEAK_URL,
THINGSPEAK_HOST, THINGSPEAK_HOST,
data.length(),
data.c_str()
_tspk_data.length()
); );
_tspk_client.print(buffer);
_tspk_client.print(headers);
_tspk_client.print(_tspk_data);
nice_delay(100); nice_delay(100);
String response = _tspk_client.readString(); String response = _tspk_client.readString();
int pos = response.indexOf("\r\n\r\n"); int pos = response.indexOf("\r\n\r\n");
unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0; unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code);
DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
_tspk_client.stop(); _tspk_client.stop();
_tspk_last_flush = millis(); _tspk_last_flush = millis();
if ((0 == code) && (--_tspk_tries > 0)) {
if ((0 == code) && _tspk_tries) {
_tspk_flush = true; _tspk_flush = true;
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n"));
DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
} else { } else {
_tspkClearQueue(); _tspkClearQueue();
} }
@ -229,14 +300,15 @@ void _tspkPost(String data) {
#endif // THINGSPEAK_USE_ASYNC #endif // THINGSPEAK_USE_ASYNC
void _tspkEnqueue(unsigned char index, char * payload) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%d with value %s\n"), index, payload);
void _tspkEnqueue(unsigned char index, const char * payload) {
DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload);
--index; --index;
if (_tspk_queue[index] != NULL) free(_tspk_queue[index]); if (_tspk_queue[index] != NULL) free(_tspk_queue[index]);
_tspk_queue[index] = strdup(payload); _tspk_queue[index] = strdup(payload);
} }
void _tspkClearQueue() { void _tspkClearQueue() {
_tspk_tries = THINGSPEAK_TRIES;
if (_tspk_clear) { if (_tspk_clear) {
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) { for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) { if (_tspk_queue[id] != NULL) {
@ -249,22 +321,30 @@ void _tspkClearQueue() {
void _tspkFlush() { void _tspkFlush() {
if (!_tspk_flush) return;
if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return;
if (_tspk_connected || _tspk_connecting) return;
_tspk_last_flush = millis();
_tspk_flush = false; _tspk_flush = false;
_tspk_data.reserve(THINGSPEAK_DATA_BUFFER_SIZE);
// Walk the fields
String data;
// Walk the fields, numbered 1...THINGSPEAK_FIELDS
for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) { for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
if (_tspk_queue[id] != NULL) { if (_tspk_queue[id] != NULL) {
if (data.length() > 0) data = data + String("&");
data = data + String("field") + String(id+1) + String("=") + String(_tspk_queue[id]);
if (_tspk_data.length() > 0) _tspk_data.concat("&");
char buf[32] = {0};
snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]);
_tspk_data.concat(buf);
} }
} }
// POST data if any // POST data if any
if (data.length() > 0) {
data = data + String("&api_key=") + getSetting("tspkKey");
_tspk_tries = THINGSPEAK_TRIES;
_tspkPost(data);
if (_tspk_data.length()) {
_tspk_data.concat("&api_key=");
_tspk_data.concat(getSetting("tspkKey"));
--_tspk_tries;
_tspkPost();
} }
} }
@ -281,7 +361,7 @@ bool tspkEnqueueRelay(unsigned char index, char * payload) {
return false; return false;
} }
bool tspkEnqueueMeasurement(unsigned char index, char * payload) {
bool tspkEnqueueMeasurement(unsigned char index, const char * payload) {
if (!_tspk_enabled) return true; if (!_tspk_enabled) return true;
unsigned char id = getSetting("tspkMagnitude", index, 0).toInt(); unsigned char id = getSetting("tspkMagnitude", index, 0).toInt();
if (id > 0) { if (id > 0) {
@ -304,8 +384,10 @@ void tspkSetup() {
_tspkConfigure(); _tspkConfigure();
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_tspkWebSocketOnSend);
wsOnReceiveRegister(_tspkWebSocketOnReceive);
wsRegister()
.onVisible(_tspkWebSocketOnVisible)
.onConnected(_tspkWebSocketOnConnected)
.onKeyCheck(_tspkWebSocketOnKeyCheck);
#endif #endif
#if BROKER_SUPPORT #if BROKER_SUPPORT
@ -326,9 +408,7 @@ void tspkSetup() {
void tspkLoop() { void tspkLoop() {
if (!_tspk_enabled) return; if (!_tspk_enabled) return;
if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return; if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return;
if (_tspk_flush && (millis() - _tspk_last_flush > THINGSPEAK_MIN_INTERVAL)) {
_tspkFlush();
}
_tspkFlush();
} }
#endif #endif

+ 61
- 65
code/espurna/utils.ino View File

@ -7,6 +7,7 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/ */
#include <Ticker.h> #include <Ticker.h>
#include "libs/HeapStats.h"
String getIdentifier() { String getIdentifier() {
char buffer[20]; char buffer[20];
@ -49,7 +50,7 @@ String getCoreVersion() {
String getCoreRevision() { String getCoreRevision() {
#ifdef ARDUINO_ESP8266_GIT_VER #ifdef ARDUINO_ESP8266_GIT_VER
return String(ARDUINO_ESP8266_GIT_VER);
return String(ARDUINO_ESP8266_GIT_VER, 16);
#else #else
return String(""); return String("");
#endif #endif
@ -63,30 +64,14 @@ unsigned char getHeartbeatInterval() {
return getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt(); return getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt();
} }
// WTF
// Calling ESP.getFreeHeap() is making the system crash on a specific
// AiLight bulb, but anywhere else...
unsigned int getFreeHeap() {
if (getSetting("wtfHeap", 0).toInt() == 1) return 9999;
return ESP.getFreeHeap();
}
unsigned int getInitialFreeHeap() {
static unsigned int _heap = 0;
if (0 == _heap) {
_heap = getFreeHeap();
}
return _heap;
}
unsigned int getUsedHeap() {
return getInitialFreeHeap() - getFreeHeap();
}
String getEspurnaModules() { String getEspurnaModules() {
return FPSTR(espurna_modules); return FPSTR(espurna_modules);
} }
String getEspurnaOTAModules() {
return FPSTR(espurna_ota_modules);
}
#if SENSOR_SUPPORT #if SENSOR_SUPPORT
String getEspurnaSensors() { String getEspurnaSensors() {
return FPSTR(espurna_sensors); return FPSTR(espurna_sensors);
@ -98,35 +83,19 @@ String getEspurnaWebUI() {
} }
String buildTime() { String buildTime() {
const char time_now[] = __TIME__; // hh:mm:ss
unsigned int hour = atoi(&time_now[0]);
unsigned int minute = atoi(&time_now[3]);
unsigned int second = atoi(&time_now[6]);
const char date_now[] = __DATE__; // Mmm dd yyyy
const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
unsigned int month = 0;
for ( int i = 0; i < 12; i++ ) {
if (strncmp(date_now, months[i], 3) == 0 ) {
month = i + 1;
break;
}
}
unsigned int day = atoi(&date_now[3]);
unsigned int year = atoi(&date_now[7]);
char buffer[20];
snprintf_P(
buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
year, month, day, hour, minute, second
);
return String(buffer);
#if NTP_SUPPORT
return ntpDateTime(__UNIX_TIMESTAMP__);
#else
char buffer[20];
snprintf_P(
buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
__TIME_YEAR__, __TIME_MONTH__, __TIME_DAY__,
__TIME_HOUR__, __TIME_MINUTE__, __TIME_SECOND__
);
return String(buffer);
#endif
} }
unsigned long getUptime() { unsigned long getUptime() {
static unsigned long last_uptime = 0; static unsigned long last_uptime = 0;
@ -140,6 +109,15 @@ unsigned long getUptime() {
} }
bool haveRelaysOrSensors() {
bool result = false;
result = (relayCount() > 0);
#if SENSOR_SUPPORT
result = result || (magnitudeCount() > 0);
#endif
return result;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Heartbeat helper // Heartbeat helper
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -202,10 +180,10 @@ namespace Heartbeat {
void heartbeat() { void heartbeat() {
unsigned long uptime_seconds = getUptime(); unsigned long uptime_seconds = getUptime();
unsigned int free_heap = getFreeHeap();
heap_stats_t heap_stats = getHeapStats();
UNUSED(uptime_seconds); UNUSED(uptime_seconds);
UNUSED(free_heap);
UNUSED(heap_stats);
#if MQTT_SUPPORT #if MQTT_SUPPORT
unsigned char _heartbeat_mode = getHeartbeatMode(); unsigned char _heartbeat_mode = getHeartbeatMode();
@ -220,7 +198,7 @@ void heartbeat() {
if (serial) { if (serial) {
DEBUG_MSG_P(PSTR("[MAIN] Uptime: %lu seconds\n"), uptime_seconds); DEBUG_MSG_P(PSTR("[MAIN] Uptime: %lu seconds\n"), uptime_seconds);
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
infoHeapStats();
#if ADC_MODE_VALUE == ADC_VCC #if ADC_MODE_VALUE == ADC_VCC
DEBUG_MSG_P(PSTR("[MAIN] Power: %lu mV\n"), ESP.getVcc()); DEBUG_MSG_P(PSTR("[MAIN] Power: %lu mV\n"), ESP.getVcc());
#endif #endif
@ -280,7 +258,7 @@ void heartbeat() {
#endif #endif
if (hb_cfg & Heartbeat::Freeheap) if (hb_cfg & Heartbeat::Freeheap)
mqttSend(MQTT_TOPIC_FREEHEAP, String(free_heap).c_str());
mqttSend(MQTT_TOPIC_FREEHEAP, String(heap_stats.available).c_str());
if (hb_cfg & Heartbeat::Relay) if (hb_cfg & Heartbeat::Relay)
relayMQTT(); relayMQTT();
@ -294,7 +272,7 @@ void heartbeat() {
mqttSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str()); mqttSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str());
if (hb_cfg & Heartbeat::Status) if (hb_cfg & Heartbeat::Status)
mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true);
mqttSendStatus();
if (hb_cfg & Heartbeat::Loadavg) if (hb_cfg & Heartbeat::Loadavg)
mqttSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str()); mqttSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str());
@ -306,14 +284,14 @@ void heartbeat() {
} }
if (hb_cfg & Heartbeat::Remote_temp) { if (hb_cfg & Heartbeat::Remote_temp) {
char remote_temp[6];
dtostrf(_remote_temp.temp, 1-sizeof(remote_temp), 1, remote_temp);
mqttSend(MQTT_TOPIC_REMOTE_TEMP, String(remote_temp).c_str());
char remote_temp[16];
dtostrf(_remote_temp.temp, 1, 1, remote_temp);
mqttSend(MQTT_TOPIC_REMOTE_TEMP, remote_temp);
} }
#endif #endif
} else if (!serial && _heartbeat_mode == HEARTBEAT_REPEAT_STATUS) { } else if (!serial && _heartbeat_mode == HEARTBEAT_REPEAT_STATUS) {
mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true);
mqttSendStatus();
} }
#endif #endif
@ -327,7 +305,7 @@ void heartbeat() {
idbSend(MQTT_TOPIC_UPTIME, String(uptime_seconds).c_str()); idbSend(MQTT_TOPIC_UPTIME, String(uptime_seconds).c_str());
if (hb_cfg & Heartbeat::Freeheap) if (hb_cfg & Heartbeat::Freeheap)
idbSend(MQTT_TOPIC_FREEHEAP, String(free_heap).c_str());
idbSend(MQTT_TOPIC_FREEHEAP, String(heap_stats.available).c_str());
if (hb_cfg & Heartbeat::Rssi) if (hb_cfg & Heartbeat::Rssi)
idbSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str()); idbSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str());
@ -422,6 +400,7 @@ void info() {
DEBUG_MSG_P(PSTR("[MAIN] SDK version: %s\n"), ESP.getSdkVersion()); DEBUG_MSG_P(PSTR("[MAIN] SDK version: %s\n"), ESP.getSdkVersion());
DEBUG_MSG_P(PSTR("[MAIN] Core version: %s\n"), getCoreVersion().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Core version: %s\n"), getCoreVersion().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Core revision: %s\n"), getCoreRevision().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Core revision: %s\n"), getCoreRevision().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Build time: %lu\n"), __UNIX_TIMESTAMP__);
DEBUG_MSG_P(PSTR("\n")); DEBUG_MSG_P(PSTR("\n"));
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -470,11 +449,15 @@ void info() {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
static bool show_frag_stats = false;
infoMemory("EEPROM", SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE - settingsSize()); infoMemory("EEPROM", SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE - settingsSize());
infoMemory("Heap", getInitialFreeHeap(), getFreeHeap());
infoMemory("Stack", 4096, getFreeStack());
infoHeapStats(show_frag_stats);
infoMemory("Stack", CONT_STACKSIZE, getFreeStack());
DEBUG_MSG_P(PSTR("\n")); DEBUG_MSG_P(PSTR("\n"));
show_frag_stats = true;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[MAIN] Boot version: %d\n"), ESP.getBootVersion()); DEBUG_MSG_P(PSTR("[MAIN] Boot version: %d\n"), ESP.getBootVersion());
@ -494,6 +477,7 @@ void info() {
DEBUG_MSG_P(PSTR("[MAIN] Board: %s\n"), getBoardName().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Board: %s\n"), getBoardName().c_str());
DEBUG_MSG_P(PSTR("[MAIN] Support: %s\n"), getEspurnaModules().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Support: %s\n"), getEspurnaModules().c_str());
DEBUG_MSG_P(PSTR("[MAIN] OTA: %s\n"), getEspurnaOTAModules().c_str());
#if SENSOR_SUPPORT #if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("[MAIN] Sensors: %s\n"), getEspurnaSensors().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Sensors: %s\n"), getEspurnaSensors().c_str());
#endif // SENSOR_SUPPORT #endif // SENSOR_SUPPORT
@ -534,8 +518,6 @@ void info() {
// SSL // SSL
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if ASYNC_TCP_SSL_ENABLED
bool sslCheckFingerPrint(const char * fingerprint) { bool sslCheckFingerPrint(const char * fingerprint) {
return (strlen(fingerprint) == 59); return (strlen(fingerprint) == 59);
} }
@ -571,8 +553,6 @@ bool sslFingerPrintChar(const char * fingerprint, char * destination) {
} }
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Reset // Reset
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -642,3 +622,19 @@ bool isNumber(const char * s) {
} }
return digit; return digit;
} }
// ref: lwip2 lwip_strnstr with strnlen
char* strnstr(const char* buffer, const char* token, size_t n) {
size_t token_len = strnlen(token, n);
if (token_len == 0) {
return const_cast<char*>(buffer);
}
for (const char* p = buffer; *p && (p + token_len <= buffer + n); p++) {
if ((*p == *token) && (strncmp(p, token, token_len) == 0)) {
return const_cast<char*>(p);
}
}
return nullptr;
}

+ 79
- 23
code/espurna/web.ino View File

@ -37,10 +37,10 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#endif // WEB_EMBEDDED #endif // WEB_EMBEDDED
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED
#include "static/server.cer.h" #include "static/server.cer.h"
#include "static/server.key.h" #include "static/server.key.h"
#endif // ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
#endif // SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -52,11 +52,19 @@ bool _webConfigSuccess = false;
std::vector<web_request_callback_f> _web_request_callbacks; std::vector<web_request_callback_f> _web_request_callbacks;
std::vector<web_body_callback_f> _web_body_callbacks; std::vector<web_body_callback_f> _web_body_callbacks;
constexpr const size_t WEB_CONFIG_BUFFER_MAX = 4096;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// HOOKS // HOOKS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void _onReset(AsyncWebServerRequest *request) { void _onReset(AsyncWebServerRequest *request) {
webLog(request);
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
deferredReset(100, CUSTOM_RESET_HTTP); deferredReset(100, CUSTOM_RESET_HTTP);
request->send(200); request->send(200);
} }
@ -65,14 +73,17 @@ void _onDiscover(AsyncWebServerRequest *request) {
webLog(request); webLog(request);
AsyncResponseStream *response = request->beginResponseStream("text/json");
AsyncResponseStream *response = request->beginResponseStream("application/json");
const String device = getBoardName();
const String hostname = getSetting("hostname");
DynamicJsonBuffer jsonBuffer;
StaticJsonBuffer<JSON_OBJECT_SIZE(4)> jsonBuffer;
JsonObject &root = jsonBuffer.createObject(); JsonObject &root = jsonBuffer.createObject();
root["app"] = APP_NAME; root["app"] = APP_NAME;
root["version"] = APP_VERSION; root["version"] = APP_VERSION;
root["hostname"] = getSetting("hostname");
root["device"] = getBoardName();
root["device"] = device.c_str();
root["hostname"] = hostname.c_str();
root.printTo(*response); root.printTo(*response);
request->send(response); request->send(response);
@ -125,11 +136,13 @@ void _onPostConfig(AsyncWebServerRequest *request) {
void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
// No buffer // No buffer
if (final && (index == 0)) { if (final && (index == 0)) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char *) data);
if (root.success()) _webConfigSuccess = settingsRestoreJson(root);
_webConfigSuccess = settingsRestoreJson((char*) data);
return; return;
} }
@ -144,6 +157,12 @@ void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t i
// Copy // Copy
if (len > 0) { if (len > 0) {
if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector<uint8_t>))) {
delete _webConfigBuffer;
_webConfigBuffer = nullptr;
request->send(500);
return;
}
_webConfigBuffer->reserve(_webConfigBuffer->size() + len); _webConfigBuffer->reserve(_webConfigBuffer->size() + len);
_webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len); _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len);
} }
@ -152,11 +171,7 @@ void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t i
if (final) { if (final) {
_webConfigBuffer->push_back(0); _webConfigBuffer->push_back(0);
// Parse JSON
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char *) _webConfigBuffer->data());
if (root.success()) _webConfigSuccess = settingsRestoreJson(root);
_webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data());
delete _webConfigBuffer; delete _webConfigBuffer;
} }
@ -177,7 +192,7 @@ void _onHome(AsyncWebServerRequest *request) {
} else { } else {
#if ASYNC_TCP_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS
// Chunked response, we calculate the chunks based on free heap (in multiples of 32) // Chunked response, we calculate the chunks based on free heap (in multiples of 32)
// This is necessary when a TLS connection is open since it sucks too much memory // This is necessary when a TLS connection is open since it sucks too much memory
@ -218,7 +233,7 @@ void _onHome(AsyncWebServerRequest *request) {
} }
#endif #endif
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED
int _onCertificate(void * arg, const char *filename, uint8_t **buf) { int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
@ -297,7 +312,11 @@ void _onUpgrade(AsyncWebServerRequest *request) {
} }
void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
void _onUpgradeFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
if (!index) { if (!index) {
@ -331,32 +350,69 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde
#endif #endif
} }
} else { } else {
//Removed to avoid websocket ping back during upgrade (see #1574)
//DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), index + len);
// Removed to avoid websocket ping back during upgrade (see #1574)
// TODO: implement as separate from debugging message
if (wsConnected()) return;
DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), index + len);
}
}
bool _onAPModeRequest(AsyncWebServerRequest *request) {
if ((WiFi.getMode() & WIFI_AP) > 0) {
const String domain = getSetting("hostname") + ".";
const String host = request->header("Host");
const String ip = WiFi.softAPIP().toString();
// Only allow requests that use our hostname or ip
if (host.equals(ip)) return true;
if (host.startsWith(domain)) return true;
// Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
// Not doing so will cause memory exhaustion, because the connection will linger
request->send(404);
request->client()->close();
return false;
} }
return true;
} }
void _onRequest(AsyncWebServerRequest *request){ void _onRequest(AsyncWebServerRequest *request){
if (!_onAPModeRequest(request)) return;
// Send request to subscribers // Send request to subscribers
for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) { for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
bool response = (_web_request_callbacks[i])(request); bool response = (_web_request_callbacks[i])(request);
if (response) return; if (response) return;
} }
// No subscriber handled the request, return a 404
// No subscriber handled the request, return a 404 with implicit "Connection: close"
request->send(404); request->send(404);
// And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
// Not doing so will cause memory exhaustion, because the connection will linger
request->client()->close();
} }
void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (!_onAPModeRequest(request)) return;
// Send request to subscribers // Send request to subscribers
for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) { for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) {
bool response = (_web_body_callbacks[i])(request, data, len, index, total); bool response = (_web_body_callbacks[i])(request, data, len, index, total);
if (response) return; if (response) return;
} }
// Same as _onAPModeRequest(...)
request->send(404);
request->client()->close();
} }
@ -388,7 +444,7 @@ void webRequestRegister(web_request_callback_f callback) {
} }
unsigned int webPort() { unsigned int webPort() {
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED
return 443; return 443;
#else #else
return getSetting("webPort", WEB_PORT).toInt(); return getSetting("webPort", WEB_PORT).toInt();
@ -420,7 +476,7 @@ void webSetup() {
_server->on("/reset", HTTP_GET, _onReset); _server->on("/reset", HTTP_GET, _onReset);
_server->on("/config", HTTP_GET, _onGetConfig); _server->on("/config", HTTP_GET, _onGetConfig);
_server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigData); _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigData);
_server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeData);
_server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeFile);
_server->on("/discover", HTTP_GET, _onDiscover); _server->on("/discover", HTTP_GET, _onDiscover);
// Serve static files // Serve static files
@ -439,7 +495,7 @@ void webSetup() {
_server->onNotFound(_onRequest); _server->onNotFound(_onRequest);
// Run server // Run server
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED
_server->onSslFileRequest(_onCertificate, NULL); _server->onSslFileRequest(_onCertificate, NULL);
_server->beginSecure("server.cer", "server.key", NULL); _server->beginSecure("server.cer", "server.key", NULL);
#else #else


+ 224
- 103
code/espurna/wifi.ino View File

@ -6,40 +6,61 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/ */
#include "JustWifi.h"
#include <JustWifi.h>
#include <Ticker.h> #include <Ticker.h>
uint32_t _wifi_scan_client_id = 0;
bool _wifi_wps_running = false; bool _wifi_wps_running = false;
bool _wifi_smartconfig_running = false; bool _wifi_smartconfig_running = false;
bool _wifi_smartconfig_initial = false;
uint8_t _wifi_ap_mode = WIFI_AP_FALLBACK; uint8_t _wifi_ap_mode = WIFI_AP_FALLBACK;
#if WIFI_GRATUITOUS_ARP_SUPPORT
unsigned long _wifi_gratuitous_arp_interval = 0;
unsigned long _wifi_gratuitous_arp_last = 0;
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PRIVATE // PRIVATE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
struct wifi_scan_info_t {
String ssid_scan;
int32_t rssi_scan;
uint8_t sec_scan;
uint8_t* BSSID_scan;
int32_t chan_scan;
bool hidden_scan;
char buffer[128];
};
void _wifiUpdateSoftAP() {
if (WiFi.softAPgetStationNum() == 0) {
#if USE_PASSWORD
jw.setSoftAP(getSetting("hostname").c_str(), getAdminPass().c_str());
#else
jw.setSoftAP(getSetting("hostname").c_str());
#endif
}
}
void _wifiCheckAP() { void _wifiCheckAP() {
if ((WIFI_AP_FALLBACK == _wifi_ap_mode) &&
(jw.connected()) &&
((WiFi.getMode() & WIFI_AP) > 0) &&
(WiFi.softAPgetStationNum() == 0)
if (
(WIFI_AP_FALLBACK == _wifi_ap_mode)
&& ((WiFi.getMode() & WIFI_AP) > 0)
&& jw.connected()
&& (WiFi.softAPgetStationNum() == 0)
) { ) {
jw.enableAP(false);
jw.enableAP(false);
} }
} }
void _wifiConfigure() { void _wifiConfigure() {
jw.setHostname(getSetting("hostname").c_str()); jw.setHostname(getSetting("hostname").c_str());
#if USE_PASSWORD
jw.setSoftAP(getSetting("hostname").c_str(), getAdminPass().c_str());
#else
jw.setSoftAP(getSetting("hostname").c_str());
#endif
_wifiUpdateSoftAP();
jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
wifiReconnectCheck(); wifiReconnectCheck();
jw.enableAPFallback(WIFI_FALLBACK_APMODE); jw.enableAPFallback(WIFI_FALLBACK_APMODE);
jw.cleanNetworks(); jw.cleanNetworks();
@ -55,94 +76,86 @@ void _wifiConfigure() {
int i; int i;
for (i = 0; i< WIFI_MAX_NETWORKS; i++) { for (i = 0; i< WIFI_MAX_NETWORKS; i++) {
if (getSetting("ssid" + String(i)).length() == 0) break;
if (getSetting("ip" + String(i)).length() == 0) {
if (!hasSetting("ssid", i)) break;
if (!hasSetting("ip", i)) {
jw.addNetwork( jw.addNetwork(
getSetting("ssid" + String(i)).c_str(),
getSetting("pass" + String(i)).c_str()
getSetting("ssid", i, "").c_str(),
getSetting("pass", i, "").c_str()
); );
} else { } else {
jw.addNetwork( jw.addNetwork(
getSetting("ssid" + String(i)).c_str(),
getSetting("pass" + String(i)).c_str(),
getSetting("ip" + String(i)).c_str(),
getSetting("gw" + String(i)).c_str(),
getSetting("mask" + String(i)).c_str(),
getSetting("dns" + String(i)).c_str()
getSetting("ssid", i, "").c_str(),
getSetting("pass", i, "").c_str(),
getSetting("ip", i, "").c_str(),
getSetting("gw", i, "").c_str(),
getSetting("mask", i, "").c_str(),
getSetting("dns", i, "").c_str()
); );
} }
} }
#if JUSTWIFI_ENABLE_SMARTCONFIG
if (i == 0) _wifi_smartconfig_initial = true;
#endif
jw.enableScan(getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1); jw.enableScan(getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1);
unsigned char sleep_mode = getSetting("wifiSleep", WIFI_SLEEP_MODE).toInt(); unsigned char sleep_mode = getSetting("wifiSleep", WIFI_SLEEP_MODE).toInt();
sleep_mode = constrain(sleep_mode, 0, 2); sleep_mode = constrain(sleep_mode, 0, 2);
WiFi.setSleepMode(static_cast<WiFiSleepType_t>(sleep_mode)); WiFi.setSleepMode(static_cast<WiFiSleepType_t>(sleep_mode));
#if WIFI_GRATUITOUS_ARP_SUPPORT
_wifi_gratuitous_arp_last = millis();
_wifi_gratuitous_arp_interval = getSetting("wifiGarpIntvl", secureRandom(
WIFI_GRATUITOUS_ARP_INTERVAL_MIN, WIFI_GRATUITOUS_ARP_INTERVAL_MAX
)).toInt();
#endif
const auto tx_power = getSetting("wifiTxPwr", WIFI_OUTPUT_POWER_DBM).toFloat();
WiFi.setOutputPower(tx_power);
} }
void _wifiScan(uint32_t client_id = 0) {
void _wifiScan(wifi_scan_f callback = nullptr) {
DEBUG_MSG_P(PSTR("[WIFI] Start scanning\n")); DEBUG_MSG_P(PSTR("[WIFI] Start scanning\n"));
#if WEB_SUPPORT
String output;
#endif
unsigned char result = WiFi.scanNetworks(); unsigned char result = WiFi.scanNetworks();
if (result == WIFI_SCAN_FAILED) { if (result == WIFI_SCAN_FAILED) {
DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n"));
#if WEB_SUPPORT
output = String("Failed scan");
#endif
return;
} else if (result == 0) { } else if (result == 0) {
DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); DEBUG_MSG_P(PSTR("[WIFI] No networks found\n"));
#if WEB_SUPPORT
output = String("No networks found");
#endif
} else {
DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), result);
// Populate defined networks with scan data
for (int8_t i = 0; i < result; ++i) {
String ssid_scan;
int32_t rssi_scan;
uint8_t sec_scan;
uint8_t* BSSID_scan;
int32_t chan_scan;
bool hidden_scan;
char buffer[128];
WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan, hidden_scan);
snprintf_P(buffer, sizeof(buffer),
PSTR("BSSID: %02X:%02X:%02X:%02X:%02X:%02X SEC: %s RSSI: %3d CH: %2d SSID: %s"),
BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], BSSID_scan[3], BSSID_scan[4], BSSID_scan[5],
(sec_scan != ENC_TYPE_NONE ? "YES" : "NO "),
rssi_scan,
chan_scan,
(char *) ssid_scan.c_str()
);
return;
}
DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), buffer);
DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), result);
#if WEB_SUPPORT
if (client_id > 0) output = output + String(buffer) + String("<br />");
#endif
// Populate defined networks with scan data
wifi_scan_info_t info;
}
for (unsigned char i = 0; i < result; ++i) {
}
WiFi.getNetworkInfo(i, info.ssid_scan, info.sec_scan, info.rssi_scan, info.BSSID_scan, info.chan_scan, info.hidden_scan);
#if WEB_SUPPORT
if (client_id > 0) {
output = String("{\"scanResult\": \"") + output + String("\"}");
wsSend(client_id, output.c_str());
snprintf_P(info.buffer, sizeof(info.buffer),
PSTR("BSSID: %02X:%02X:%02X:%02X:%02X:%02X SEC: %s RSSI: %3d CH: %2d SSID: %s"),
info.BSSID_scan[0], info.BSSID_scan[1], info.BSSID_scan[2], info.BSSID_scan[3], info.BSSID_scan[4], info.BSSID_scan[5],
(info.sec_scan != ENC_TYPE_NONE ? "YES" : "NO "),
info.rssi_scan,
info.chan_scan,
info.ssid_scan.c_str()
);
if (callback) {
callback(info);
} else {
DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), info.buffer);
} }
#endif
}
WiFi.scanDelete(); WiFi.scanDelete();
@ -195,25 +208,46 @@ void _wifiInject() {
if (strlen(WIFI1_SSID)) { if (strlen(WIFI1_SSID)) {
if (!hasSetting("ssid", 0)) { if (!hasSetting("ssid", 0)) {
setSetting("ssid", 0, WIFI1_SSID);
setSetting("pass", 0, WIFI1_PASS);
setSetting("ip", 0, WIFI1_IP);
setSetting("gw", 0, WIFI1_GW);
setSetting("mask", 0, WIFI1_MASK);
setSetting("dns", 0, WIFI1_DNS);
setSetting("ssid", 0, F(WIFI1_SSID));
setSetting("pass", 0, F(WIFI1_PASS));
setSetting("ip", 0, F(WIFI1_IP));
setSetting("gw", 0, F(WIFI1_GW));
setSetting("mask", 0, F(WIFI1_MASK));
setSetting("dns", 0, F(WIFI1_DNS));
} }
if (strlen(WIFI2_SSID)) { if (strlen(WIFI2_SSID)) {
if (!hasSetting("ssid", 1)) { if (!hasSetting("ssid", 1)) {
setSetting("ssid", 1, WIFI2_SSID);
setSetting("pass", 1, WIFI2_PASS);
setSetting("ip", 1, WIFI2_IP);
setSetting("gw", 1, WIFI2_GW);
setSetting("mask", 1, WIFI2_MASK);
setSetting("dns", 1, WIFI2_DNS);
setSetting("ssid", 1, F(WIFI2_SSID));
setSetting("pass", 1, F(WIFI2_PASS));
setSetting("ip", 1, F(WIFI2_IP));
setSetting("gw", 1, F(WIFI2_GW));
setSetting("mask", 1, F(WIFI2_MASK));
setSetting("dns", 1, F(WIFI2_DNS));
} }
}
if (strlen(WIFI3_SSID)) {
if (!hasSetting("ssid", 2)) {
setSetting("ssid", 2, F(WIFI3_SSID));
setSetting("pass", 2, F(WIFI3_PASS));
setSetting("ip", 2, F(WIFI3_IP));
setSetting("gw", 2, F(WIFI3_GW));
setSetting("mask", 2, F(WIFI3_MASK));
setSetting("dns", 2, F(WIFI3_DNS));
}
if (strlen(WIFI4_SSID)) {
if (!hasSetting("ssid", 3)) {
setSetting("ssid", 3, F(WIFI4_SSID));
setSetting("pass", 3, F(WIFI4_PASS));
setSetting("ip", 3, F(WIFI4_IP));
setSetting("gw", 3, F(WIFI4_GW));
setSetting("mask", 3, F(WIFI4_MASK));
setSetting("dns", 3, F(WIFI4_DNS));
}
}
}
}
} }
} }
@ -230,7 +264,6 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) {
if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) { if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) {
_wifi_wps_running = false; _wifi_wps_running = false;
_wifi_smartconfig_running = false; _wifi_smartconfig_running = false;
jw.enableAP(true);
} }
if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) { if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) {
@ -254,7 +287,6 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) {
_wifi_wps_running = false; _wifi_wps_running = false;
_wifi_smartconfig_running = false; _wifi_smartconfig_running = false;
jw.enableAP(true);
} }
@ -346,6 +378,7 @@ void _wifiDebugCallback(justwifi_messages_t code, char * parameter) {
} }
if (code == MESSAGE_ACCESSPOINT_DESTROYED) { if (code == MESSAGE_ACCESSPOINT_DESTROYED) {
_wifiUpdateSoftAP();
DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n")); DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n"));
} }
@ -400,6 +433,11 @@ void _wifiInitCommands() {
terminalOK(); terminalOK();
}); });
terminalRegisterCommand(F("WIFI.STA"), [](Embedis* e) {
wifiStartSTA();
terminalOK();
});
terminalRegisterCommand(F("WIFI.AP"), [](Embedis* e) { terminalRegisterCommand(F("WIFI.AP"), [](Embedis* e) {
wifiStartAP(); wifiStartAP();
terminalOK(); terminalOK();
@ -434,7 +472,7 @@ void _wifiInitCommands() {
#if WEB_SUPPORT #if WEB_SUPPORT
bool _wifiWebSocketOnReceive(const char * key, JsonVariant& value) {
bool _wifiWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "wifi", 4) == 0) return true; if (strncmp(key, "wifi", 4) == 0) return true;
if (strncmp(key, "ssid", 4) == 0) return true; if (strncmp(key, "ssid", 4) == 0) return true;
if (strncmp(key, "pass", 4) == 0) return true; if (strncmp(key, "pass", 4) == 0) return true;
@ -445,7 +483,7 @@ bool _wifiWebSocketOnReceive(const char * key, JsonVariant& value) {
return false; return false;
} }
void _wifiWebSocketOnSend(JsonObject& root) {
void _wifiWebSocketOnConnected(JsonObject& root) {
root["maxNetworks"] = WIFI_MAX_NETWORKS; root["maxNetworks"] = WIFI_MAX_NETWORKS;
root["wifiScan"] = getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1; root["wifiScan"] = getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1;
JsonArray& wifi = root.createNestedArray("wifi"); JsonArray& wifi = root.createNestedArray("wifi");
@ -461,16 +499,90 @@ void _wifiWebSocketOnSend(JsonObject& root) {
} }
} }
void _wifiWebSocketScan(JsonObject& root) {
JsonArray& scanResult = root.createNestedArray("scanResult");
_wifiScan([&scanResult](wifi_scan_info_t& info) {
scanResult.add(info.buffer);
});
}
void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (strcmp(action, "scan") == 0) _wifi_scan_client_id = client_id;
if (strcmp(action, "scan") == 0) wsPost(client_id, _wifiWebSocketScan);
} }
#endif #endif
// -----------------------------------------------------------------------------
// SUPPORT
// -----------------------------------------------------------------------------
#if WIFI_GRATUITOUS_ARP_SUPPORT
// ref: lwip src/core/netif.c netif_issue_reports(...)
// ref: esp-lwip/core/ipv4/etharp.c garp_tmr()
// TODO: only for ipv4, need (?) a different method with ipv6
bool _wifiSendGratuitousArp() {
bool result = false;
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
if (
(interface->flags & NETIF_FLAG_ETHARP)
&& (interface->hwaddr_len == ETHARP_HWADDR_LEN)
#if LWIP_VERSION_MAJOR == 1
&& (!ip_addr_isany(&interface->ip_addr))
#else
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
#endif
&& (interface->flags & NETIF_FLAG_LINK_UP)
&& (interface->flags & NETIF_FLAG_UP)
) {
etharp_gratuitous(interface);
result = true;
}
}
return result;
}
void _wifiSendGratuitousArp(unsigned long interval) {
if (millis() - _wifi_gratuitous_arp_last > interval) {
_wifi_gratuitous_arp_last = millis();
_wifiSendGratuitousArp();
}
}
#endif // WIFI_GRATUITOUS_ARP_SUPPORT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// INFO // INFO
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// backported WiFiAPClass methods
String _wifiSoftAPSSID() {
struct softap_config config;
wifi_softap_get_config(&config);
char* name = reinterpret_cast<char*>(config.ssid);
char ssid[sizeof(config.ssid) + 1];
memcpy(ssid, name, sizeof(config.ssid));
ssid[sizeof(config.ssid)] = '\0';
return String(ssid);
}
String _wifiSoftAPPSK() {
struct softap_config config;
wifi_softap_get_config(&config);
char* pass = reinterpret_cast<char*>(config.password);
char psk[sizeof(config.password) + 1];
memcpy(psk, pass, sizeof(config.password));
psk[sizeof(config.password)] = '\0';
return String(psk);
}
void wifiDebug(WiFiMode_t modes) { void wifiDebug(WiFiMode_t modes) {
#if DEBUG_SUPPORT #if DEBUG_SUPPORT
@ -478,7 +590,6 @@ void wifiDebug(WiFiMode_t modes) {
if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) { if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) {
uint8_t * bssid = WiFi.BSSID();
DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE STA\n")); DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE STA\n"));
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str()); DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str());
@ -487,9 +598,7 @@ void wifiDebug(WiFiMode_t modes) {
DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str()); DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str());
DEBUG_MSG_P(PSTR("[WIFI] BSSID %02X:%02X:%02X:%02X:%02X:%02X\n"),
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], bssid[6]
);
DEBUG_MSG_P(PSTR("[WIFI] BSSID %s\n"), WiFi.BSSIDstr().c_str());
DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel()); DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel());
DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI()); DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI());
footer = true; footer = true;
@ -498,8 +607,8 @@ void wifiDebug(WiFiMode_t modes) {
if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) { if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) {
DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n")); DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n"));
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), getSetting("hostname").c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getAdminPass().c_str());
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), _wifiSoftAPSSID().c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), _wifiSoftAPPSK().c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str());
footer = true; footer = true;
@ -548,6 +657,12 @@ void wifiDisconnect() {
jw.disconnect(); jw.disconnect();
} }
void wifiStartSTA() {
jw.disconnect();
jw.enableSTA(true);
jw.enableAP(false);
}
void wifiStartAP(bool only) { void wifiStartAP(bool only) {
if (only) { if (only) {
jw.enableSTA(false); jw.enableSTA(false);
@ -610,6 +725,10 @@ void wifiSetup() {
_wifiInject(); _wifiInject();
_wifiConfigure(); _wifiConfigure();
#if JUSTWIFI_ENABLE_SMARTCONFIG
if (_wifi_smartconfig_initial) jw.startSmartConfig();
#endif
// Message callbacks // Message callbacks
wifiRegister(_wifiCallback); wifiRegister(_wifiCallback);
#if WIFI_AP_CAPTIVE #if WIFI_AP_CAPTIVE
@ -620,9 +739,10 @@ void wifiSetup() {
#endif #endif
#if WEB_SUPPORT #if WEB_SUPPORT
wsOnSendRegister(_wifiWebSocketOnSend);
wsOnReceiveRegister(_wifiWebSocketOnReceive);
wsOnActionRegister(_wifiWebSocketOnAction);
wsRegister()
.onAction(_wifiWebSocketOnAction)
.onConnected(_wifiWebSocketOnConnected)
.onKeyCheck(_wifiWebSocketOnKeyCheck);
#endif #endif
#if TERMINAL_SUPPORT #if TERMINAL_SUPPORT
@ -647,11 +767,12 @@ void wifiLoop() {
} }
#endif #endif
// Do we have a pending scan?
if (_wifi_scan_client_id > 0) {
_wifiScan(_wifi_scan_client_id);
_wifi_scan_client_id = 0;
}
// Only send out gra arp when in STA mode
#if WIFI_GRATUITOUS_ARP_SUPPORT
if (_wifi_gratuitous_arp_interval) {
_wifiSendGratuitousArp(_wifi_gratuitous_arp_interval);
}
#endif
// Check if we should disable AP // Check if we should disable AP
static unsigned long last = 0; static unsigned long last = 0;


+ 368
- 131
code/espurna/ws.ino View File

@ -16,21 +16,146 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "libs/WebSocketIncommingBuffer.h" #include "libs/WebSocketIncommingBuffer.h"
AsyncWebSocket _ws("/ws"); AsyncWebSocket _ws("/ws");
Ticker _web_defer;
Ticker _ws_defer;
uint32_t _ws_last_update = 0;
std::vector<ws_on_send_callback_f> _ws_on_send_callbacks;
std::vector<ws_on_action_callback_f> _ws_on_action_callbacks;
std::vector<ws_on_receive_callback_f> _ws_on_receive_callbacks;
// -----------------------------------------------------------------------------
// WS callbacks
// -----------------------------------------------------------------------------
ws_callbacks_t& ws_callbacks_t::onVisible(ws_on_send_callback_f cb) {
on_visible.push_back(cb);
return *this;
}
ws_callbacks_t& ws_callbacks_t::onConnected(ws_on_send_callback_f cb) {
on_connected.push_back(cb);
return *this;
}
ws_callbacks_t& ws_callbacks_t::onData(ws_on_send_callback_f cb) {
on_data.push_back(cb);
return *this;
}
ws_callbacks_t& ws_callbacks_t::onAction(ws_on_action_callback_f cb) {
on_action.push_back(cb);
return *this;
}
ws_callbacks_t& ws_callbacks_t::onKeyCheck(ws_on_keycheck_callback_f cb) {
on_keycheck.push_back(cb);
return *this;
}
ws_callbacks_t _ws_callbacks;
struct ws_counter_t {
ws_counter_t() : current(0), start(0), stop(0) {}
ws_counter_t(uint32_t start, uint32_t stop) :
current(start), start(start), stop(stop) {}
void reset() {
current = start;
}
void next() {
if (current < stop) {
++current;
}
}
bool done() {
return (current >= stop);
}
uint32_t current;
uint32_t start;
uint32_t stop;
};
struct ws_data_t {
enum mode_t {
SEQUENCE,
ALL
};
ws_data_t(const ws_on_send_callback_f& cb) :
storage(new ws_on_send_callback_list_t {cb}),
client_id(0),
mode(ALL),
callbacks(*storage.get()),
counter(0, 1)
{}
ws_data_t(uint32_t client_id, const ws_on_send_callback_f& cb) :
storage(new ws_on_send_callback_list_t {cb}),
client_id(client_id),
mode(ALL),
callbacks(*storage.get()),
counter(0, 1)
{}
ws_data_t(const uint32_t client_id, ws_on_send_callback_list_t&& callbacks, mode_t mode = SEQUENCE) :
storage(new ws_on_send_callback_list_t(std::move(callbacks))),
client_id(client_id),
mode(mode),
callbacks(*storage.get()),
counter(0, (storage.get())->size())
{}
ws_data_t(const uint32_t client_id, const ws_on_send_callback_list_t& callbacks, mode_t mode = SEQUENCE) :
client_id(client_id),
mode(mode),
callbacks(callbacks),
counter(0, callbacks.size())
{}
bool done() {
return counter.done();
}
void sendAll(JsonObject& root) {
while (!counter.done()) counter.next();
for (auto& callback : callbacks) {
callback(root);
}
}
void sendCurrent(JsonObject& root) {
callbacks[counter.current](root);
counter.next();
}
void send(JsonObject& root) {
switch (mode) {
case SEQUENCE: sendCurrent(root); break;
case ALL: sendAll(root); break;
}
}
std::unique_ptr<ws_on_send_callback_list_t> storage;
const uint32_t client_id;
const mode_t mode;
const ws_on_send_callback_list_t& callbacks;
ws_counter_t counter;
};
std::queue<ws_data_t> _ws_client_data;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Private methods
// WS authentication
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
typedef struct {
struct ws_ticket_t {
IPAddress ip; IPAddress ip;
unsigned long timestamp = 0; unsigned long timestamp = 0;
} ws_ticket_t;
ws_ticket_t _ticket[WS_BUFFER_SIZE];
};
ws_ticket_t _ws_tickets[WS_BUFFER_SIZE];
void _onAuth(AsyncWebServerRequest *request) { void _onAuth(AsyncWebServerRequest *request) {
@ -41,15 +166,15 @@ void _onAuth(AsyncWebServerRequest *request) {
unsigned long now = millis(); unsigned long now = millis();
unsigned short index; unsigned short index;
for (index = 0; index < WS_BUFFER_SIZE; index++) { for (index = 0; index < WS_BUFFER_SIZE; index++) {
if (_ticket[index].ip == ip) break;
if (_ticket[index].timestamp == 0) break;
if (now - _ticket[index].timestamp > WS_TIMEOUT) break;
if (_ws_tickets[index].ip == ip) break;
if (_ws_tickets[index].timestamp == 0) break;
if (now - _ws_tickets[index].timestamp > WS_TIMEOUT) break;
} }
if (index == WS_BUFFER_SIZE) { if (index == WS_BUFFER_SIZE) {
request->send(429); request->send(429);
} else { } else {
_ticket[index].ip = ip;
_ticket[index].timestamp = now;
_ws_tickets[index].ip = ip;
_ws_tickets[index].timestamp = now;
request->send(200, "text/plain", "OK"); request->send(200, "text/plain", "OK");
} }
@ -62,7 +187,7 @@ bool _wsAuth(AsyncWebSocketClient * client) {
unsigned short index = 0; unsigned short index = 0;
for (index = 0; index < WS_BUFFER_SIZE; index++) { for (index = 0; index < WS_BUFFER_SIZE; index++) {
if ((_ticket[index].ip == ip) && (now - _ticket[index].timestamp < WS_TIMEOUT)) break;
if ((_ws_tickets[index].ip == ip) && (now - _ws_tickets[index].timestamp < WS_TIMEOUT)) break;
} }
if (index == WS_BUFFER_SIZE) { if (index == WS_BUFFER_SIZE) {
@ -73,39 +198,97 @@ bool _wsAuth(AsyncWebSocketClient * client) {
} }
// -----------------------------------------------------------------------------
// Debug
// -----------------------------------------------------------------------------
#if DEBUG_WEB_SUPPORT #if DEBUG_WEB_SUPPORT
bool wsDebugSend(const char* prefix, const char* message) {
if (!wsConnected()) return false;
if (getFreeHeap() < (strlen(message) * 3)) return false;
struct ws_debug_msg_t {
ws_debug_msg_t(const char* prefix, const char* message) :
prefix(prefix), message(message)
{}
String prefix;
String message;
};
struct ws_debug_t {
ws_debug_t(size_t capacity) :
flush(false),
current(0),
capacity(capacity)
{
messages.reserve(capacity);
}
DynamicJsonBuffer jsonBuffer;
JsonObject &root = jsonBuffer.createObject();
JsonObject &weblog = root.createNestedObject("weblog");
void clear() {
messages.clear();
current = 0;
flush = false;
}
weblog.set("message", message);
if (prefix && (prefix[0] != '\0')) {
weblog.set("prefix", prefix);
void add(const char* prefix, const char* message) {
if (current >= capacity) {
flush = true;
send(wsConnected());
}
messages.emplace(messages.begin() + current, prefix, message);
flush = true;
++current;
} }
wsSend(root);
void send(const bool connected) {
if (!connected && flush) {
clear();
return;
}
return true;
}
#endif
if (!flush) return;
// ref: http://arduinojson.org/v5/assistant/
// {"weblog": {"msg":[...],"pre":[...]}}
DynamicJsonBuffer jsonBuffer(2*JSON_ARRAY_SIZE(messages.size()) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2));
// -----------------------------------------------------------------------------
JsonObject& root = jsonBuffer.createObject();
JsonObject& weblog = root.createNestedObject("weblog");
JsonArray& msg = weblog.createNestedArray("msg");
JsonArray& pre = weblog.createNestedArray("pre");
#if MQTT_SUPPORT
void _wsMQTTCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) wsSend_P(PSTR("{\"mqttStatus\": true}"));
if (type == MQTT_DISCONNECT_EVENT) wsSend_P(PSTR("{\"mqttStatus\": false}"));
for (auto& message : messages) {
pre.add(message.prefix.c_str());
msg.add(message.message.c_str());
}
wsSend(root);
clear();
}
bool flush;
size_t current;
const size_t capacity;
std::vector<ws_debug_msg_t> messages;
};
// TODO: move to the headers?
constexpr const size_t WS_DEBUG_MSG_BUFFER = 8;
ws_debug_t _ws_debug(WS_DEBUG_MSG_BUFFER);
bool wsDebugSend(const char* prefix, const char* message) {
if (!wsConnected()) return false;
_ws_debug.add(prefix, message);
return true;
} }
#endif #endif
bool _wsStore(String key, String value) {
// Check the existing setting before saving it
// TODO: this should know of the default values, somehow?
// TODO: move webPort handling somewhere else?
bool _wsStore(const String& key, const String& value) {
// HTTP port
if (key == "webPort") { if (key == "webPort") {
if ((value.toInt() == 0) || (value.toInt() == 80)) { if ((value.toInt() == 0) || (value.toInt() == 80)) {
return delSetting(key); return delSetting(key);
@ -120,7 +303,11 @@ bool _wsStore(String key, String value) {
} }
bool _wsStore(String key, JsonArray& value) {
// -----------------------------------------------------------------------------
// Store indexed key (key0, key1, etc.) from array
// -----------------------------------------------------------------------------
bool _wsStore(const String& key, JsonArray& value) {
bool changed = false; bool changed = false;
@ -140,6 +327,15 @@ bool _wsStore(String key, JsonArray& value) {
} }
bool _wsCheckKey(const String& key, JsonVariant& value) {
for (auto& callback : _ws_callbacks.on_keycheck) {
if (callback(key.c_str(), value)) return true;
// TODO: remove this to call all OnKeyCheckCallbacks with the
// current key/value
}
return false;
}
void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
//DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : ""); //DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : "");
@ -147,11 +343,22 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
// Get client ID // Get client ID
uint32_t client_id = client->id(); uint32_t client_id = client->id();
// Check early for empty object / nothing
if ((length == 0) || (length == 1)) {
return;
}
if ((length == 3) && (strcmp((char*) payload, "{}") == 0)) {
return;
}
// Parse JSON input // Parse JSON input
DynamicJsonBuffer jsonBuffer;
// TODO: json buffer should be pretty efficient with the non-const payload,
// most of the space is taken by the object key references
DynamicJsonBuffer jsonBuffer(512);
JsonObject& root = jsonBuffer.parseObject((char *) payload); JsonObject& root = jsonBuffer.parseObject((char *) payload);
if (!root.success()) { if (!root.success()) {
DEBUG_MSG_P(PSTR("[WEBSOCKET] Error parsing data\n"));
DEBUG_MSG_P(PSTR("[WEBSOCKET] JSON parsing error\n"));
wsSend_P(client_id, PSTR("{\"message\": 3}")); wsSend_P(client_id, PSTR("{\"message\": 3}"));
return; return;
} }
@ -169,7 +376,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
} }
if (strcmp(action, "reconnect") == 0) { if (strcmp(action, "reconnect") == 0) {
_web_defer.once_ms(100, wifiDisconnect);
_ws_defer.once_ms(100, wifiDisconnect);
return; return;
} }
@ -184,8 +391,8 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
if (data.success()) { if (data.success()) {
// Callbacks // Callbacks
for (unsigned char i = 0; i < _ws_on_action_callbacks.size(); i++) {
(_ws_on_action_callbacks[i])(client_id, action, data);
for (auto& callback : _ws_callbacks.on_action) {
callback(client_id, action, data);
} }
// Restore configuration via websockets // Restore configuration via websockets
@ -212,9 +419,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
String adminPass; String adminPass;
bool save = false; bool save = false;
#if MQTT_SUPPORT
bool changedMQTT = false;
#endif
for (auto kv: config) { for (auto kv: config) {
@ -240,15 +444,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
continue; continue;
} }
// Check if key has to be processed
bool found = false;
for (unsigned char i = 0; i < _ws_on_receive_callbacks.size(); i++) {
found |= (_ws_on_receive_callbacks[i])(key.c_str(), value);
// TODO: remove this to call all OnReceiveCallbacks with the
// current key/value
if (found) break;
}
if (!found) {
if (!_wsCheckKey(key, value)) {
delSetting(key); delSetting(key);
continue; continue;
} }
@ -263,9 +459,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
// Update flags if value has changed // Update flags if value has changed
if (changed) { if (changed) {
save = true; save = true;
#if MQTT_SUPPORT
if (key.startsWith("mqtt")) changedMQTT = true;
#endif
} }
} }
@ -276,12 +469,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
// Callbacks // Callbacks
espurnaReload(); espurnaReload();
// This should got to callback as well
// but first change management has to be in place
#if MQTT_SUPPORT
if (changedMQTT) mqttReset();
#endif
// Persist settings // Persist settings
saveSettings(); saveSettings();
@ -310,21 +497,19 @@ void _wsUpdate(JsonObject& root) {
#endif #endif
} }
void _wsDoUpdate(bool reset = false) {
static unsigned long last = millis();
if (reset) {
last = millis() + WS_UPDATE_INTERVAL;
return;
}
void _wsResetUpdateTimer() {
_ws_last_update = millis() + WS_UPDATE_INTERVAL;
}
if (millis() - last > WS_UPDATE_INTERVAL) {
last = millis();
void _wsDoUpdate(const bool connected) {
if (!connected) return;
if (millis() - _ws_last_update > WS_UPDATE_INTERVAL) {
_ws_last_update = millis();
wsSend(_wsUpdate); wsSend(_wsUpdate);
} }
} }
bool _wsOnReceive(const char * key, JsonVariant& value) {
bool _wsOnKeyCheck(const char * key, JsonVariant& value) {
if (strncmp(key, "ws", 2) == 0) return true; if (strncmp(key, "ws", 2) == 0) return true;
if (strncmp(key, "admin", 5) == 0) return true; if (strncmp(key, "admin", 5) == 0) return true;
if (strncmp(key, "hostname", 8) == 0) return true; if (strncmp(key, "hostname", 8) == 0) return true;
@ -333,15 +518,9 @@ bool _wsOnReceive(const char * key, JsonVariant& value) {
return false; return false;
} }
void _wsOnStart(JsonObject& root) {
void _wsOnConnected(JsonObject& root) {
char chipid[7]; char chipid[7];
snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId());
uint8_t * bssid = WiFi.BSSID();
char bssid_str[20];
snprintf_P(bssid_str, sizeof(bssid_str),
PSTR("%02X:%02X:%02X:%02X:%02X:%02X"),
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]
);
root["webMode"] = WEB_MODE_NORMAL; root["webMode"] = WEB_MODE_NORMAL;
@ -354,7 +533,7 @@ void _wsOnStart(JsonObject& root) {
root["manufacturer"] = MANUFACTURER; root["manufacturer"] = MANUFACTURER;
root["chipid"] = String(chipid); root["chipid"] = String(chipid);
root["mac"] = WiFi.macAddress(); root["mac"] = WiFi.macAddress();
root["bssid"] = String(bssid_str);
root["bssid"] = WiFi.BSSIDstr();
root["channel"] = WiFi.channel(); root["channel"] = WiFi.channel();
root["device"] = DEVICE; root["device"] = DEVICE;
root["hostname"] = getSetting("hostname"); root["hostname"] = getSetting("hostname");
@ -369,17 +548,12 @@ void _wsOnStart(JsonObject& root) {
root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt();
root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); root["webPort"] = getSetting("webPort", WEB_PORT).toInt();
root["wsAuth"] = getSetting("wsAuth", WS_AUTHENTICATION).toInt() == 1; root["wsAuth"] = getSetting("wsAuth", WS_AUTHENTICATION).toInt() == 1;
#if TERMINAL_SUPPORT
root["cmdVisible"] = 1;
#endif
root["hbMode"] = getSetting("hbMode", HEARTBEAT_MODE).toInt(); root["hbMode"] = getSetting("hbMode", HEARTBEAT_MODE).toInt();
root["hbInterval"] = getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt(); root["hbInterval"] = getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt();
_wsDoUpdate(true);
} }
void wsSend(JsonObject& root) { void wsSend(JsonObject& root) {
// TODO: avoid serializing twice?
size_t len = root.measureLength(); size_t len = root.measureLength();
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len);
@ -393,6 +567,7 @@ void wsSend(uint32_t client_id, JsonObject& root) {
AsyncWebSocketClient* client = _ws.client(client_id); AsyncWebSocketClient* client = _ws.client(client_id);
if (client == nullptr) return; if (client == nullptr) return;
// TODO: avoid serializing twice?
size_t len = root.measureLength(); size_t len = root.measureLength();
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len);
@ -402,29 +577,24 @@ void wsSend(uint32_t client_id, JsonObject& root) {
} }
} }
void _wsStart(uint32_t client_id) {
void _wsConnected(uint32_t client_id) {
#if USE_PASSWORD && WEB_FORCE_PASS_CHANGE
bool changePassword = getAdminPass().equals(ADMIN_PASS);
#else
bool changePassword = false;
#endif
const bool changePassword = (USE_PASSWORD && WEB_FORCE_PASS_CHANGE)
? getAdminPass().equals(ADMIN_PASS)
: false;
if (changePassword) { if (changePassword) {
DynamicJsonBuffer jsonBuffer;
StaticJsonBuffer<JSON_OBJECT_SIZE(1)> jsonBuffer;
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
root["webMode"] = WEB_MODE_PASSWORD; root["webMode"] = WEB_MODE_PASSWORD;
wsSend(client_id, root); wsSend(client_id, root);
return; return;
} }
for (auto& callback : _ws_on_send_callbacks) {
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
callback(root);
wsSend(client_id, root);
}
wsPostAll(client_id, _ws_callbacks.on_visible);
wsPostSequence(client_id, _ws_callbacks.on_connected);
wsPostSequence(client_id, _ws_callbacks.on_data);
} }
void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
@ -444,9 +614,10 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy
IPAddress ip = client->remoteIP(); IPAddress ip = client->remoteIP();
DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url());
_wsStart(client->id());
client->_tempObject = new WebSocketIncommingBuffer(&_wsParse, true);
_wsConnected(client->id());
_wsResetUpdateTimer();
wifiReconnectCheck(); wifiReconnectCheck();
client->_tempObject = new WebSocketIncommingBuffer(_wsParse, true);
} else if(type == WS_EVT_DISCONNECT) { } else if(type == WS_EVT_DISCONNECT) {
DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id()); DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id());
@ -463,6 +634,7 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy
} else if(type == WS_EVT_DATA) { } else if(type == WS_EVT_DATA) {
//DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : ""); //DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : "");
if (!client->_tempObject) return;
WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject; WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject;
AwsFrameInfo * info = (AwsFrameInfo*)arg; AwsFrameInfo * info = (AwsFrameInfo*)arg;
buffer->data_event(client, info, data, len); buffer->data_event(client, info, data, len);
@ -471,9 +643,62 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy
} }
// TODO: make this generic loop method to queue important ws messages?
// or, if something uses ticker / async ctx to send messages,
// it needs a retry mechanism built into the callback object
void _wsHandleClientData(const bool connected) {
if (!connected && !_ws_client_data.empty()) {
_ws_client_data.pop();
return;
}
if (_ws_client_data.empty()) return;
auto& data = _ws_client_data.front();
// client_id == 0 means we need to send the message to every client
if (data.client_id) {
AsyncWebSocketClient* ws_client = _ws.client(data.client_id);
if (!ws_client) {
_ws_client_data.pop();
return;
}
// wait until we can send the next batch of messages
// XXX: enforce that callbacks send only one message per iteration
if (ws_client->queueIsFull()) {
return;
}
}
// XXX: block allocation will try to create *2 next time,
// likely failing and causing wsSend to reference empty objects
// XXX: arduinojson6 will not do this, but we may need to use per-callback buffers
constexpr const size_t BUFFER_SIZE = 3192;
DynamicJsonBuffer jsonBuffer(BUFFER_SIZE);
JsonObject& root = jsonBuffer.createObject();
data.send(root);
if (data.client_id) {
wsSend(data.client_id, root);
} else {
wsSend(root);
}
yield();
if (data.done()) {
_ws_client_data.pop();
}
}
void _wsLoop() { void _wsLoop() {
if (!wsConnected()) return;
_wsDoUpdate();
const bool connected = wsConnected();
_wsDoUpdate(connected);
_wsHandleClientData(connected);
#if DEBUG_WEB_SUPPORT
_ws_debug.send(connected);
#endif
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -488,21 +713,13 @@ bool wsConnected(uint32_t client_id) {
return _ws.hasClient(client_id); return _ws.hasClient(client_id);
} }
void wsOnSendRegister(ws_on_send_callback_f callback) {
_ws_on_send_callbacks.push_back(callback);
}
void wsOnReceiveRegister(ws_on_receive_callback_f callback) {
_ws_on_receive_callbacks.push_back(callback);
}
void wsOnActionRegister(ws_on_action_callback_f callback) {
_ws_on_action_callbacks.push_back(callback);
ws_callbacks_t& wsRegister() {
return _ws_callbacks;
} }
void wsSend(ws_on_send_callback_f callback) { void wsSend(ws_on_send_callback_f callback) {
if (_ws.count() > 0) { if (_ws.count() > 0) {
DynamicJsonBuffer jsonBuffer;
DynamicJsonBuffer jsonBuffer(512);
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
callback(root); callback(root);
@ -528,17 +745,10 @@ void wsSend(uint32_t client_id, ws_on_send_callback_f callback) {
AsyncWebSocketClient* client = _ws.client(client_id); AsyncWebSocketClient* client = _ws.client(client_id);
if (client == nullptr) return; if (client == nullptr) return;
DynamicJsonBuffer jsonBuffer;
DynamicJsonBuffer jsonBuffer(512);
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
callback(root); callback(root);
size_t len = root.measureLength();
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len);
if (buffer) {
root.printTo(reinterpret_cast<char*>(buffer->get()), len + 1);
client->text(buffer);
}
wsSend(client_id, root);
} }
void wsSend(uint32_t client_id, const char * payload) { void wsSend(uint32_t client_id, const char * payload) {
@ -551,25 +761,52 @@ void wsSend_P(uint32_t client_id, PGM_P payload) {
_ws.text(client_id, buffer); _ws.text(client_id, buffer);
} }
void wsPost(const ws_on_send_callback_f& cb) {
_ws_client_data.emplace(cb);
}
void wsPost(uint32_t client_id, const ws_on_send_callback_f& cb) {
_ws_client_data.emplace(client_id, cb);
}
void wsPostAll(uint32_t client_id, const ws_on_send_callback_list_t& cbs) {
_ws_client_data.emplace(client_id, cbs, ws_data_t::ALL);
}
void wsPostAll(const ws_on_send_callback_list_t& cbs) {
_ws_client_data.emplace(0, cbs, ws_data_t::ALL);
}
void wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& cbs) {
_ws_client_data.emplace(client_id, cbs, ws_data_t::SEQUENCE);
}
void wsPostSequence(uint32_t client_id, ws_on_send_callback_list_t&& cbs) {
_ws_client_data.emplace(client_id, std::forward<ws_on_send_callback_list_t>(cbs), ws_data_t::SEQUENCE);
}
void wsPostSequence(const ws_on_send_callback_list_t& cbs) {
_ws_client_data.emplace(0, cbs, ws_data_t::SEQUENCE);
}
void wsSetup() { void wsSetup() {
_ws.onEvent(_wsEvent); _ws.onEvent(_wsEvent);
webServer()->addHandler(&_ws); webServer()->addHandler(&_ws);
// CORS // CORS
#ifdef WEB_REMOTE_DOMAIN
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", WEB_REMOTE_DOMAIN);
const String webDomain = getSetting("webDomain", WEB_REMOTE_DOMAIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", webDomain);
if (!webDomain.equals("*")) {
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
}
webServer()->on("/auth", HTTP_GET, _onAuth); webServer()->on("/auth", HTTP_GET, _onAuth);
#if MQTT_SUPPORT
mqttRegister(_wsMQTTCallback);
#endif
wsRegister()
.onConnected(_wsOnConnected)
.onKeyCheck(_wsOnKeyCheck);
wsOnSendRegister(_wsOnStart);
wsOnReceiveRegister(_wsOnReceive);
espurnaRegisterLoop(_wsLoop); espurnaRegisterLoop(_wsLoop);
} }


+ 90
- 0
code/extra_script_pre.py View File

@ -0,0 +1,90 @@
from __future__ import print_function
Import("env")
import os
import sys
TRAVIS = os.environ.get("TRAVIS")
PIO_PLATFORM = env.PioPlatform()
CONFIG = env.GetProjectConfig()
class ExtraScriptError(Exception):
pass
# Most portable way, without depending on platformio internals
def subprocess_libdeps(lib_deps, storage=None, silent=True):
import subprocess
args = [env.subst("$PYTHONEXE"), "-mplatformio", "lib"]
if not storage:
args.append("-g")
else:
args.extend(["-d", storage])
args.append("install")
if silent:
args.append("-s")
args.extend(lib_deps)
subprocess.check_call(args)
# Avoid spawning pio lib every time, hook into the LibraryManager API (sort-of internal)
def library_manager_libdeps(lib_deps, storage=None):
from platformio.managers.lib import LibraryManager
from platformio.project.helpers import get_project_global_lib_dir
if not storage:
manager = LibraryManager(get_project_global_lib_dir())
else:
manager = LibraryManager(storage)
for lib in lib_deps:
if manager.get_package_dir(*manager.parse_pkg_uri(lib)):
continue
print("installing: {}".format(lib), file=sys.stderr)
manager.install(lib)
def get_shared_libdeps_dir(section, name):
if not CONFIG.has_option(section, name):
raise ExtraScriptError("{}.{} is required to be set".format(section, name))
opt = CONFIG.get(section, name)
if not opt in env.GetProjectOption("lib_extra_dirs"):
raise ExtraScriptError(
"lib_extra_dirs must contain {}.{}".format(section, name)
)
return os.path.join(env["PROJECT_DIR"], opt)
def ensure_platform_updated():
try:
if PIO_PLATFORM.are_outdated_packages():
print("updating platform packages", file=sys.stderr)
PIO_PLATFORM.update_packages()
except Exception:
print("Warning: no connection, cannot check for outdated packages", file=sys.stderr)
# latest toolchain is still optional with PIO (TODO: recheck after 2.6.0!)
# also updates arduino core git to the latest master commit
if TRAVIS and (env.GetProjectOption("platform") == CONFIG.get("common", "arduino_core_git")):
ensure_platform_updated()
# to speed-up build process, install libraries in either global or local shared storage
if os.environ.get("ESPURNA_PIO_SHARED_LIBRARIES"):
if TRAVIS:
storage = None
print("using global library storage", file=sys.stderr)
else:
storage = get_shared_libdeps_dir("common", "shared_libdeps_dir")
print("using shared library storage: ", storage, file=sys.stderr)
subprocess_libdeps(env.GetProjectOption("lib_deps"), storage)

+ 32
- 6
code/extra_scripts.py View File

@ -8,6 +8,9 @@ import click
Import("env", "projenv") Import("env", "projenv")
PIO_PLATFORM = env.PioPlatform()
FRAMEWORK_DIR = PIO_PLATFORM.get_package_dir("framework-arduinoespressif8266")
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Utils # Utils
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -44,6 +47,31 @@ def print_filler(fill, color=Color.WHITE, err=False):
out = sys.stderr if err else sys.stdout out = sys.stderr if err else sys.stdout
print(clr(color, fill * width), file=out) print(clr(color, fill * width), file=out)
def ldscript_inject_libpath():
# espressif8266@1.5.0 did not append this directory into the LIBPATH
libpath_sdk = os.path.join(FRAMEWORK_DIR, "tools", "sdk", "ld")
env.Append(LIBPATH=[libpath_sdk])
libpath_base = os.path.join("$PROJECT_DIR", "..", "dist", "ld")
env.Append(LIBPATH=[
os.path.join(libpath_base, "pre_2.5.0")
])
# local.eagle.app.v6.common.ld exists only with Core >2.5.0
def check_local_ld(target ,source, env):
local_ld = env.subst(os.path.join("$BUILD_DIR", "ld", "local.eagle.app.v6.common.ld"))
if os.path.exists(local_ld):
env.Prepend(LIBPATH=[
os.path.join(libpath_base, "latest")
])
env.AddPreAction(
os.path.join("$BUILD_DIR", "firmware.elf"),
check_local_ld
)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Callbacks # Callbacks
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -97,19 +125,16 @@ def patch_lwip():
if "lwip_gcc" not in env["LIBS"]: if "lwip_gcc" not in env["LIBS"]:
return return
framework_dir = env["FRAMEWORK_ARDUINOESP8266_DIR"]
platform = env.PioPlatform()
toolchain_prefix = os.path.join(platform.get_package_dir("toolchain-xtensa"), "bin", "xtensa-lx106-elf-")
toolchain_prefix = os.path.join(PIO_PLATFORM.get_package_dir("toolchain-xtensa"), "bin", "xtensa-lx106-elf-")
patch_action = env.VerboseAction(" ".join([ patch_action = env.VerboseAction(" ".join([
"-patch", "-u", "-N", "-d", "-patch", "-u", "-N", "-d",
os.path.join(framework_dir, "tools", "sdk", "lwip"),
os.path.join(FRAMEWORK_DIR, "tools", "sdk", "lwip"),
os.path.join("src", "core", "tcp_out.c"), os.path.join("src", "core", "tcp_out.c"),
env.subst(os.path.join("$PROJECT_DIR", "..", "dist", "patches", "lwip_mtu_issue_1610.patch")) env.subst(os.path.join("$PROJECT_DIR", "..", "dist", "patches", "lwip_mtu_issue_1610.patch"))
]), "Patching lwip source") ]), "Patching lwip source")
build_action = env.VerboseAction(" ".join([ build_action = env.VerboseAction(" ".join([
"make", "-C", os.path.join(framework_dir, "tools", "sdk", "lwip", "src"),
"make", "-C", os.path.join(FRAMEWORK_DIR, "tools", "sdk", "lwip", "src"),
"install", "install",
"TOOLS_PATH={}".format(toolchain_prefix), "TOOLS_PATH={}".format(toolchain_prefix),
"LWIP_LIB=liblwip_gcc.a" "LWIP_LIB=liblwip_gcc.a"
@ -131,6 +156,7 @@ projenv.ProcessUnFlags("-w")
# 2.4.0 and up # 2.4.0 and up
remove_float_support() remove_float_support()
ldscript_inject_libpath()
# two-step update hint when using 1MB boards # two-step update hint when using 1MB boards
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size) env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size)


+ 176
- 99
code/html/custom.js View File

@ -12,6 +12,7 @@ var numChanged = 0;
var numReboot = 0; var numReboot = 0;
var numReconnect = 0; var numReconnect = 0;
var numReload = 0; var numReload = 0;
var conf_saved = false;
var useWhite = false; var useWhite = false;
var useCCT = false; var useCCT = false;
@ -24,6 +25,10 @@ var packets;
var filters = []; var filters = [];
<!-- endRemoveIf(!rfm69)--> <!-- endRemoveIf(!rfm69)-->
<!-- removeIf(!sensor)-->
var magnitudes = [];
<!-- endRemoveIf(!sensor)-->
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Messages // Messages
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -374,7 +379,7 @@ function checkTempRangeMin() {
$("#tempRangeMinInput").val(max - 1); $("#tempRangeMinInput").val(max - 1);
} }
} }
function checkTempRangeMax() { function checkTempRangeMax() {
var min = parseInt($("#tempRangeMinInput").val(), 10); var min = parseInt($("#tempRangeMinInput").val(), 10);
var max = parseInt($("#tempRangeMaxInput").val(), 10); var max = parseInt($("#tempRangeMaxInput").val(), 10);
@ -455,6 +460,7 @@ function setOriginalsFromValues(force) {
function resetOriginals() { function resetOriginals() {
setOriginalsFromValues(true); setOriginalsFromValues(true);
numReboot = numReconnect = numReload = 0; numReboot = numReconnect = numReload = 0;
conf_saved = false;
} }
function doReload(milliseconds) { function doReload(milliseconds) {
@ -514,47 +520,35 @@ function doUpgrade() {
var data = new FormData(); var data = new FormData();
data.append("upgrade", file, file.name); data.append("upgrade", file, file.name);
$.ajax({
var xhr = new XMLHttpRequest();
// Your server script to process the upload
url: urls.upgrade.href,
type: "POST",
// Form data
data: data,
// Tell jQuery not to process data or worry about content-type
// You *must* include these options!
cache: false,
contentType: false,
processData: false,
var network_error = function() {
alert("There was a network error trying to upload the new image, please try again.");
};
xhr.addEventListener("error", network_error, false);
xhr.addEventListener("abort", network_error, false);
xhr.addEventListener("load", function(e) {
$("#upgrade-progress").hide();
if ("OK" === xhr.responseText) {
alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds.");
doReload(5000);
} else {
alert("There was an error trying to upload the new image, please try again ("
+ "response: " + xhr.responseText + ", "
+ "status: " + xhr.statusText + ")");
}
}, false);
success: function(data, text) {
$("#upgrade-progress").hide();
if ("OK" === data) {
alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds.");
doReload(5000);
} else {
alert("There was an error trying to upload the new image, please try again (" + data + ").");
}
},
// Custom XMLHttpRequest
xhr: function() {
$("#upgrade-progress").show();
var myXhr = $.ajaxSettings.xhr();
if (myXhr.upload) {
// For handling the progress of the upload
myXhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
$("progress").attr({ value: e.loaded, max: e.total });
}
} , false);
}
return myXhr;
xhr.upload.addEventListener("progress", function(e) {
$("#upgrade-progress").show();
if (e.lengthComputable) {
$("progress").attr({ value: e.loaded, max: e.total });
} }
}, false);
});
xhr.open("POST", urls.upgrade.href);
xhr.send(data);
}); });
@ -616,6 +610,31 @@ function doReconnect(ask) {
} }
function doCheckOriginals() {
var response;
if (numReboot > 0) {
response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?");
if (response) { doReboot(false); }
} else if (numReconnect > 0) {
response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?");
if (response) { doReconnect(false); }
} else if (numReload > 0) {
response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?");
if (response) { doReload(0); }
}
resetOriginals();
}
function waitForSave(){
if (conf_saved == false) {
setTimeout(waitForSave, 1000);
} else {
doCheckOriginals();
}
}
function doUpdate() { function doUpdate() {
var forms = $(".form-settings"); var forms = $(".form-settings");
@ -632,24 +651,8 @@ function doUpdate() {
// Change handling // Change handling
numChanged = 0; numChanged = 0;
setTimeout(function() {
var response;
if (numReboot > 0) {
response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?");
if (response) { doReboot(false); }
} else if (numReconnect > 0) {
response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?");
if (response) { doReconnect(false); }
} else if (numReload > 0) {
response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?");
if (response) { doReload(0); }
}
resetOriginals();
}, 1000);
waitForSave();
} }
@ -846,7 +849,7 @@ function createMagnitudeList(data, container, template_name) {
for (var i=0; i<size; ++i) { for (var i=0; i<size; ++i) {
var line = $(template).clone(); var line = $(template).clone();
$("label", line).html(magnitudeType(data.type[i]) + " #" + parseInt(data.index[i], 10)); $("label", line).html(magnitudeType(data.type[i]) + " #" + parseInt(data.index[i], 10));
$("div.hint", line).html(data.name[i]);
$("div.hint", line).html(magnitudes[i].description);
$("input", line).attr("tabindex", 40 + i).val(data.idx[i]); $("input", line).attr("tabindex", 40 + i).val(data.idx[i]);
line.appendTo("#" + container); line.appendTo("#" + container);
} }
@ -1018,6 +1021,15 @@ function initRelays(data) {
} }
function updateRelays(data) {
var size = data.size;
for (var i=0; i<size; ++i) {
var elem = $("input[name='relay'][data='" + i + "']");
elem.prop("checked", data.status[i]);
elem.prop("disabled", data.lock[i] < 2); // RELAY_LOCK_DISABLED=2
}
}
function createCheckboxes() { function createCheckboxes() {
$("input[type='checkbox']").each(function() { $("input[type='checkbox']").each(function() {
@ -1098,9 +1110,16 @@ function initMagnitudes(data) {
var template = $("#magnitudeTemplate").children(); var template = $("#magnitudeTemplate").children();
for (var i=0; i<size; ++i) { for (var i=0; i<size; ++i) {
var magnitude = {
"name": magnitudeType(data.type[i]) + " #" + parseInt(data.index[i], 10),
"units": data.units[i],
"description": data.description[i]
};
magnitudes.push(magnitude);
var line = $(template).clone(); var line = $(template).clone();
$("label", line).html(magnitudeType(data.type[i]) + " #" + parseInt(data.index[i], 10));
$("div.hint", line).html(data.description[i]);
$("label", line).html(magnitude.name);
$("div.hint", line).html(magnitude.description);
$("input", line).attr("data", i); $("input", line).attr("data", i);
line.appendTo("#magnitudes"); line.appendTo("#magnitudes");
} }
@ -1114,7 +1133,47 @@ function initMagnitudes(data) {
<!-- removeIf(!light)--> <!-- removeIf(!light)-->
function initColor(rgb) {
// wheelColorPicker accepts:
// hsv(0...360,0...1,0...1)
// hsv(0...100%,0...100%,0...100%)
// While we use:
// hsv(0...360,0...100%,0...100%)
function _hsv_round(value) {
return Math.round(value * 100) / 100;
}
function getPickerRGB(picker) {
return $(picker).wheelColorPicker("getValue", "css");
}
function setPickerRGB(picker, color) {
$(picker).wheelColorPicker("setValue", value, true);
}
// TODO: use pct values instead of doing conversion?
function getPickerHSV(picker) {
var color = $(picker).wheelColorPicker("getColor");
return String(Math.ceil(_hsv_round(color.h) * 360))
+ "," + String(Math.ceil(_hsv_round(color.s) * 100))
+ "," + String(Math.ceil(_hsv_round(color.v) * 100));
}
function setPickerHSV(picker, value) {
if (value === getPickerHSV(picker)) return;
var chunks = value.split(",");
$(picker).wheelColorPicker("setColor", {
h: _hsv_round(chunks[0] / 360),
s: _hsv_round(chunks[1] / 100),
v: _hsv_round(chunks[2] / 100)
});
}
function initColor(cfg) {
var rgb = false;
if (typeof cfg === "object") {
rgb = cfg.rgb;
}
// check if already initialized // check if already initialized
var done = $("#colors > div").length; var done = $("#colors > div").length;
@ -1127,15 +1186,12 @@ function initColor(rgb) {
// init color wheel // init color wheel
$("input[name='color']").wheelColorPicker({ $("input[name='color']").wheelColorPicker({
sliders: (rgb ? "wrgbp" : "whsvp")
sliders: (rgb ? "wrgbp" : "whsp")
}).on("sliderup", function() { }).on("sliderup", function() {
if (rgb) { if (rgb) {
var value = $(this).wheelColorPicker("getValue", "css");
sendAction("color", {rgb: value});
sendAction("color", {rgb: getPickerRGB(this)});
} else { } else {
var color = $(this).wheelColorPicker("getColor");
var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10);
sendAction("color", {hsv: value});
sendAction("color", {hsv: getPickerHSV(this)});
} }
}); });
@ -1493,20 +1549,14 @@ function processData(data) {
<!-- removeIf(!light)--> <!-- removeIf(!light)-->
if ("rgb" === key) { if ("rgb" === key) {
initColor(true);
$("input[name='color']").wheelColorPicker("setValue", value, true);
initColor({rgb: true});
setPickerRGB($("input[name='color']"), value);
return; return;
} }
if ("hsv" === key) { if ("hsv" === key) {
initColor(false);
// wheelColorPicker expects HSV to be between 0 and 1 all of them
var chunks = value.split(",");
var obj = {};
obj.h = chunks[0] / 360;
obj.s = chunks[1] / 100;
obj.v = chunks[2] / 100;
$("input[name='color']").wheelColorPicker("setColor", obj);
initColor({hsv: true});
setPickerHSV($("input[name='color']"), value);
return; return;
} }
@ -1550,12 +1600,15 @@ function processData(data) {
<!-- removeIf(!sensor)--> <!-- removeIf(!sensor)-->
if ("magnitudes" === key) {
if ("magnitudesConfig" === key) {
initMagnitudes(value); initMagnitudes(value);
}
if ("magnitudes" === key) {
for (var i=0; i<value.size; ++i) { for (var i=0; i<value.size; ++i) {
var error = value.error[i] || 0; var error = value.error[i] || 0;
var text = (0 === error) ? var text = (0 === error) ?
value.value[i] + value.units[i] :
value.value[i] + magnitudes[i].units :
magnitudeError(error); magnitudeError(error);
var element = $("input[name='magnitude'][data='" + i + "']"); var element = $("input[name='magnitude'][data='" + i + "']");
element.val(text); element.val(text);
@ -1630,11 +1683,9 @@ function processData(data) {
// Relays // Relays
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
if ("relayStatus" === key) {
initRelays(value);
for (i in value) {
$("input[name='relay'][data='" + i + "']").prop("checked", value[i]);
}
if ("relayState" === key) {
initRelays(value.status);
updateRelays(value);
return; return;
} }
@ -1699,6 +1750,9 @@ function processData(data) {
// Messages // Messages
if ("message" === key) { if ("message" === key) {
if (value == 8 && (numReboot > 0 || numReload > 0 || numReconnect > 0)){
conf_saved = true;
}
window.alert(messages[value]); window.alert(messages[value]);
return; return;
} }
@ -1707,10 +1761,15 @@ function processData(data) {
if ("weblog" === key) { if ("weblog" === key) {
send("{}"); send("{}");
if (value.prefix) {
$("#weblog").append(new Text(value.prefix));
msg = value["msg"];
pre = value["pre"];
for (var i=0; i < msg.length; ++i) {
if (pre[i]) {
$("#weblog").append(new Text(pre[i]));
}
$("#weblog").append(new Text(msg[i]));
} }
$("#weblog").append(new Text(value.message));
$("#weblog").scrollTop($("#weblog")[0].scrollHeight - $("#weblog").height()); $("#weblog").scrollTop($("#weblog")[0].scrollHeight - $("#weblog").height());
return; return;
@ -1785,9 +1844,16 @@ function processData(data) {
// Look for SPANs // Look for SPANs
var span = $("span[name='" + key + "']"); var span = $("span[name='" + key + "']");
if (span.length > 0) { if (span.length > 0) {
pre = span.attr("pre") || "";
post = span.attr("post") || "";
span.html(pre + value + post);
if (Array.isArray(value)) {
value.forEach(function(elem) {
span.append(elem);
span.append('</br>');
});
} else {
pre = span.attr("pre") || "";
post = span.attr("post") || "";
span.html(pre + value + post);
}
} }
// Look for SELECTs // Look for SELECTs
@ -1869,12 +1935,17 @@ function connectToURL(url) {
initUrls(url); initUrls(url);
$.ajax({
fetch(urls.auth.href, {
'method': 'GET', 'method': 'GET',
'crossDomain': true,
'url': urls.auth.href,
'xhrFields': { 'withCredentials': true }
}).done(function(data) {
'cors': true,
'credentials': 'same-origin'
}).then(function(response) {
// Nothing to do, reload page and retry
if (response.status != 200) {
doReload(5000);
return;
}
// update websock object
if (websock) { websock.close(); } if (websock) { websock.close(); }
websock = new WebSocket(urls.ws.href); websock = new WebSocket(urls.ws.href);
websock.onmessage = function(evt) { websock.onmessage = function(evt) {
@ -1883,8 +1954,9 @@ function connectToURL(url) {
processData(data); processData(data);
} }
}; };
}).fail(function() {
// Nothing to do, reload page and retry
}).catch(function(error) {
console.log(error);
doReload(5000);
}); });
} }
@ -1900,9 +1972,11 @@ function connectToCurrentURL() {
connectToURL(new URL(window.location)); connectToURL(new URL(window.location));
} }
function getParameterByName(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
function enableWSLogging() {
var processDataOrig = window.processData;
window.processData = function(data) { console.log(data); processDataOrig(data); }
var sendActionOrig = window.sendAction;
window.sendAction = function(action, data) { console.log(action,data); sendActionOrig(action, data);}
} }
$(function() { $(function() {
@ -1987,7 +2061,10 @@ $(function() {
if (window.location.protocol === "file:") { return; } if (window.location.protocol === "file:") { return; }
// Check host param in query string // Check host param in query string
if (host = getParameterByName('host')) {
var search = new URLSearchParams(window.location.search),
host = search.get("host");
if (host !== null) {
connect(host); connect(host);
} else { } else {
connectToCurrentURL(); connectToCurrentURL();


+ 7
- 5
code/html/index.html View File

@ -12,7 +12,7 @@
<link rel="stylesheet" href="vendor/pure-grids-responsive-1.0.0.min.css" /> <link rel="stylesheet" href="vendor/pure-grids-responsive-1.0.0.min.css" />
<link rel="stylesheet" href="vendor/side-menu.css" /> <link rel="stylesheet" href="vendor/side-menu.css" />
<!-- removeIf(!light) --> <!-- removeIf(!light) -->
<link rel="stylesheet" href="vendor/jquery.wheelcolorpicker-3.0.3.css" />
<link rel="stylesheet" href="vendor/jquery.wheelcolorpicker-3.0.8.css" />
<!-- endRemoveIf(!light) --> <!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) --> <!-- removeIf(!rfm69) -->
<link rel="stylesheet" href="vendor/datatables-1.10.16.css" /> <link rel="stylesheet" href="vendor/datatables-1.10.16.css" />
@ -512,7 +512,7 @@
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useWhite" action="reload" tabindex="9" /></div> <div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useWhite" action="reload" tabindex="9" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div> <div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div> <div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Use forth dimmable channel as (cold) white light calculated out of the RGB values.<br />Will only work if the device has at least 4 dimmable channels.<br />Enabling this will render useless the "Channel 4" slider in the status page.<br />Reload the page to update the web interface.</div>
<div class="pure-u-1 pure-u-lg-3-4 hint">For 2 channels warm white and cold white lights or color lights to use forth dimmable channel as (cold) white light calculated out of the RGB values.<br />Will only work if the device has at least 4 dimmable channels.<br />Enabling this will render useless the "Channel 4" slider in the status page.<br />Reload the page to update the web interface.</div>
</div> </div>
<div class="pure-g"> <div class="pure-g">
@ -520,7 +520,7 @@
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useCCT" action="reload" tabindex="10" /></div> <div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useCCT" action="reload" tabindex="10" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div> <div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div> <div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Use fifth dimmable channel as warm white light and the forth dimmable channel as cold white.<br />Will only work if the device has at least 5 dimmable channels and "white channel" above is also ON.<br />Enabling this will render useless the "Channel 5" slider in the status page.<br />Reload the page to update the web interface.</div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Use a dimmable channel as warm white light and another dimmable channel as cold white light.<br />On devices with two dimmable channels the first use used for warm white light and the second for cold white light.<br />On color lights the fifth use used for warm white light and the fourth for cold white light.<br />Will only work if the device has exactly 2 dimmable channels or at least 5 dimmable channels and "white channel" above is also ON.<br />Enabling this will render useless the "Channel 5" slider in the status page.<br />Reload the page to update the web interface.</div>
</div> </div>
<div class="pure-g"> <div class="pure-g">
@ -1932,6 +1932,8 @@
<option value="1">Always ON</option> <option value="1">Always ON</option>
<option value="2">Same as before</option> <option value="2">Same as before</option>
<option value="3">Toggle before</option> <option value="3">Toggle before</option>
<option value="4">Locked OFF</option>
<option value="5">Locked ON</option>
</select> </select>
</div> </div>
<div class="pure-g"> <div class="pure-g">
@ -2080,10 +2082,10 @@
</body> </body>
<!-- build:js script.js --> <!-- build:js script.js -->
<script src="vendor/jquery-3.2.1.min.js"></script>
<script src="vendor/jquery-3.4.1.slim.min.js"></script>
<script src="custom.js"></script> <script src="custom.js"></script>
<!-- removeIf(!light) --> <!-- removeIf(!light) -->
<script src="vendor/jquery.wheelcolorpicker-3.0.3.min.js"></script>
<script src="vendor/jquery.wheelcolorpicker-3.0.8.min.js"></script>
<!-- endRemoveIf(!light) --> <!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) --> <!-- removeIf(!rfm69) -->
<script src="vendor/datatables-1.10.16.min.js"></script> <script src="vendor/datatables-1.10.16.min.js"></script>


+ 0
- 4
code/html/vendor/jquery-3.2.1.min.js
File diff suppressed because it is too large
View File


+ 2
- 0
code/html/vendor/jquery-3.4.1.slim.min.js
File diff suppressed because it is too large
View File


+ 0
- 160
code/html/vendor/jquery.wheelcolorpicker-3.0.3.css View File

@ -1,160 +0,0 @@
/**
* jQuery Wheel Color Picker
* Base Stylesheet
*
* http://www.jar2.net/projects/jquery-wheelcolorpicker
*
* Copyright © 2011-2016 Fajar Chandra. All rights reserved.
* Released under MIT License.
* http://www.opensource.org/licenses/mit-license.php
*
* Note: Width, height, left, and top properties are handled by the
* plugin. These values might change on the fly.
*/
.jQWCP-wWidget {
position: absolute;
width: 250px;
height: 180px;
background: #eee;
box-shadow: 1px 1px 4px rgba(0,0,0,.5);
border-radius: 4px;
border: solid 1px #aaa;
padding: 10px;
z-index: 1001;
}
.jQWCP-wWidget.jQWCP-block {
position: relative;
border-color: #aaa;
box-shadow: inset 1px 1px 1px #ccc;
}
.jQWCP-wWheel {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
position: relative;
float: left;
width: 180px;
height: 180px;
-webkit-border-radius: 90px;
-moz-border-radius: 50%;
border-radius: 50%;
border: solid 1px #aaa;
margin: -1px;
margin-right: 10px;
transition: border .15s;
cursor: crosshair;
}
.jQWCP-wWheel:hover {
border-color: #666;
}
.jQWCP-wWheelOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0;
-webkit-border-radius: 90px;
-moz-border-radius: 50%;
border-radius: 50%;
}
.jQWCP-wWheelCursor {
width: 8px;
height: 8px;
position: absolute;
top: 50%;
left: 50%;
margin: -6px -6px;
cursor: crosshair;
border: solid 2px #fff;
box-shadow: 1px 1px 2px #000;
border-radius: 50%;
}
.jQWCP-slider-wrapper,
.jQWCP-wPreview {
position: relative;
width: 20px;
height: 180px;
float: left;
margin-right: 10px;
}
.jQWCP-wWheel:last-child,
.jQWCP-slider-wrapper:last-child,
.jQWCP-wPreview:last-child {
margin-right: 0;
}
.jQWCP-slider,
.jQWCP-wPreviewBox {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
box-sizing: border-box;
border: solid 1px #aaa;
margin: -1px;
-moz-border-radius: 4px;
border-radius: 4px;
transition: border .15s;
}
.jQWCP-slider {
cursor: crosshair;
}
.jQWCP-slider-wrapper:hover .jQWCP-slider {
border-color: #666;
}
.jQWCP-scursor {
position: absolute;
left: 0;
top: 0;
right: 0;
height: 6px;
margin: -5px -1px -5px -3px;
cursor: crosshair;
border: solid 2px #fff;
box-shadow: 1px 1px 2px #000;
border-radius: 4px;
}
.jQWCP-wAlphaSlider,
.jQWCP-wPreviewBox {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEVAQEB/f39eaJUuAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYRBDgK9dKdMgAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVAjXY/jPwIAVYRf9DwB+vw/x6vMT1wAAAABJRU5ErkJggg==') center center;
}
.jQWCP-overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1000;
}
/*********************/
/* Mobile layout */
.jQWCP-mobile.jQWCP-wWidget {
position: fixed;
bottom: 0;
left: 0 !important;
top: auto !important;
width: 100%;
height: 75%;
max-height: 240px;
box-sizing: border-box;
border-radius: 0;
}

+ 0
- 13
code/html/vendor/jquery.wheelcolorpicker-3.0.3.min.js
File diff suppressed because it is too large
View File


+ 161
- 0
code/html/vendor/jquery.wheelcolorpicker-3.0.8.css View File

@ -0,0 +1,161 @@
/**
* Wheel Color Picker for jQuery
* Base Stylesheet
*
* https://raffer.one/projects/jquery-wheelcolorpicker
*
* Copyright © 2011-2019 Fajar Chandra. All rights reserved.
* Released under MIT License.
* http://www.opensource.org/licenses/mit-license.php
*
* Note: Width, height, left, and top properties are handled by the
* plugin. These values might change on the fly.
*/
.jQWCP-wWidget {
position: absolute;
width: 250px;
height: 180px;
background: #eee;
box-shadow: 1px 1px 4px rgba(0,0,0,.5);
border-radius: 4px;
border: solid 1px #aaa;
padding: 10px;
z-index: 1001;
touch-action: none;
}
.jQWCP-wWidget.jQWCP-block {
position: relative;
border-color: #aaa;
box-shadow: inset 1px 1px 1px #ccc;
}
.jQWCP-wWheel {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
position: relative;
float: left;
width: 180px;
height: 180px;
-webkit-border-radius: 90px;
-moz-border-radius: 50%;
border-radius: 50%;
border: solid 1px #aaa;
margin: -1px;
margin-right: 10px;
transition: border .15s;
cursor: crosshair;
}
.jQWCP-wWheel:hover {
border-color: #666;
}
.jQWCP-wWheelOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0;
-webkit-border-radius: 90px;
-moz-border-radius: 50%;
border-radius: 50%;
}
.jQWCP-wWheelCursor {
width: 8px;
height: 8px;
position: absolute;
top: 50%;
left: 50%;
margin: -6px -6px;
cursor: crosshair;
border: solid 2px #fff;
box-shadow: 1px 1px 2px #000;
border-radius: 50%;
}
.jQWCP-slider-wrapper,
.jQWCP-wPreview {
position: relative;
width: 20px;
height: 180px;
float: left;
margin-right: 10px;
}
.jQWCP-wWheel:last-child,
.jQWCP-slider-wrapper:last-child,
.jQWCP-wPreview:last-child {
margin-right: 0;
}
.jQWCP-slider,
.jQWCP-wPreviewBox {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
box-sizing: border-box;
border: solid 1px #aaa;
margin: -1px;
-moz-border-radius: 4px;
border-radius: 4px;
transition: border .15s;
}
.jQWCP-slider {
cursor: crosshair;
}
.jQWCP-slider-wrapper:hover .jQWCP-slider {
border-color: #666;
}
.jQWCP-scursor {
position: absolute;
left: 0;
top: 0;
right: 0;
height: 6px;
margin: -5px -1px -5px -3px;
cursor: crosshair;
border: solid 2px #fff;
box-shadow: 1px 1px 2px #000;
border-radius: 4px;
}
.jQWCP-wAlphaSlider,
.jQWCP-wPreviewBox {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEVAQEB/f39eaJUuAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYRBDgK9dKdMgAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVAjXY/jPwIAVYRf9DwB+vw/x6vMT1wAAAABJRU5ErkJggg==') center center;
}
.jQWCP-overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1000;
}
/*********************/
/* Mobile layout */
.jQWCP-mobile.jQWCP-wWidget {
position: fixed;
bottom: 0;
left: 0 !important;
top: auto !important;
width: 100%;
height: 75%;
max-height: 240px;
box-sizing: border-box;
border-radius: 0;
}

+ 6
- 0
code/html/vendor/jquery.wheelcolorpicker-3.0.8.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
code/libraries/README View File

@ -0,0 +1 @@
Shared lib_deps storage, see code/extra_script_libdeps.py

+ 11
- 7
code/memanalyzer.py View File

@ -103,7 +103,11 @@ def run(env_, modules_):
flags = "" flags = ""
for k, v in modules_.items(): for k, v in modules_.items():
flags += "-D{}_SUPPORT={:d} ".format(k, v) flags += "-D{}_SUPPORT={:d} ".format(k, v)
command = "ESPURNA_BOARD=\"WEMOS_D1_MINI_RELAYSHIELD\" ESPURNA_FLAGS=\"{}\" platformio run --silent --environment {} 2>/dev/null".format(flags, env_)
os_env = os.environ.copy()
os_env["ESPURNA_BOARD"] = "WEMOS_D1_MINI_RELAYSHIELD"
os_env["ESPURNA_FLAGS"] = flags
os_env["ESPURNA_PIO_SHARED_LIBRARIES"] = "y"
command = "platformio run --silent --environment {} 2>/dev/null".format(env_)
subprocess.check_call(command, shell=True) subprocess.check_call(command, shell=True)
@ -215,8 +219,8 @@ if __name__ == '__main__':
# Build the core without modules to get base memory usage # Build the core without modules to get base memory usage
run(env, modules) run(env, modules)
base = analyse_memory(".pioenvs/{}/firmware.elf".format(env))
base['size'] = file_size(".pioenvs/{}/firmware.bin".format(env))
base = analyse_memory(".pio/build/{}/firmware.elf".format(env))
base['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
calc_free(base) calc_free(base)
print(output_format.format( print(output_format.format(
"CORE" if args.core == 1 else "DEFAULT", "CORE" if args.core == 1 else "DEFAULT",
@ -235,8 +239,8 @@ if __name__ == '__main__':
modules[module] = 1 modules[module] = 1
run(env, modules) run(env, modules)
results[module] = analyse_memory(".pioenvs/{}/firmware.elf".format(env))
results[module]['size'] = file_size(".pioenvs/{}/firmware.bin".format(env))
results[module] = analyse_memory(".pio/build/{}/firmware.elf".format(env))
results[module]['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
calc_free(results[module]) calc_free(results[module])
modules[module] = 0 modules[module] = 0
@ -257,8 +261,8 @@ if __name__ == '__main__':
for module in test_modules: for module in test_modules:
modules[module] = 1 modules[module] = 1
run(env, modules) run(env, modules)
total = analyse_memory(".pioenvs/{}/firmware.elf".format(env))
total['size'] = file_size(".pioenvs/{}/firmware.bin".format(env))
total = analyse_memory(".pio/build/{}/firmware.elf".format(env))
total['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
calc_free(total) calc_free(total)
print(output_format.format( print(output_format.format(


+ 8
- 4
code/ota.py View File

@ -55,6 +55,7 @@ def on_service_state_change(zeroconf, service_type, name, state_change):
'mac': '', 'mac': '',
'app_name': '', 'app_name': '',
'app_version': '', 'app_version': '',
'build_date': '',
'target_board': '', 'target_board': '',
'mem_size': 0, 'mem_size': 0,
'sdk_size': 0, 'sdk_size': 0,
@ -76,7 +77,7 @@ def list_devices():
""" """
Shows the list of discovered devices Shows the list of discovered devices
""" """
output_format = "{:>3} {:<14} {:<15} {:<17} {:<12} {:<12} {:<25} {:<8} {:<8} {:<10}"
output_format = "{:>3} {:<14} {:<15} {:<17} {:<12} {:<12} {:<20} {:<25} {:<8} {:<8} {:<10}"
print(output_format.format( print(output_format.format(
"#", "#",
"HOSTNAME", "HOSTNAME",
@ -84,12 +85,13 @@ def list_devices():
"MAC", "MAC",
"APP", "APP",
"VERSION", "VERSION",
"BUILD_DATE",
"DEVICE", "DEVICE",
"MEM_SIZE", "MEM_SIZE",
"SDK_SIZE", "SDK_SIZE",
"FREE_SPACE" "FREE_SPACE"
)) ))
print("-" * 139)
print("-" * 164)
index = 0 index = 0
for device in devices: for device in devices:
@ -101,6 +103,7 @@ def list_devices():
device.get('mac', ''), device.get('mac', ''),
device.get('app_name', ''), device.get('app_name', ''),
device.get('app_version', ''), device.get('app_version', ''),
device.get('build_date', ''),
device.get('target_board', ''), device.get('target_board', ''),
device.get('mem_size', 0), device.get('mem_size', 0),
device.get('sdk_size', 0), device.get('sdk_size', 0),
@ -240,8 +243,8 @@ def boardname(board):
def store(device, env): def store(device, env):
source = ".pioenvs/{}/firmware.elf".format(env)
destination = ".pioenvs/elfs/{}.elf".format(boardname(device).lower())
source = ".pio/build/{}/firmware.elf".format(env)
destination = ".pio/build/elfs/{}.elf".format(boardname(device).lower())
dst_dir = os.path.dirname(destination) dst_dir = os.path.dirname(destination)
if not os.path.exists(dst_dir): if not os.path.exists(dst_dir):
@ -257,6 +260,7 @@ def run(device, env):
environ["ESPURNA_BOARD"] = device["board"] environ["ESPURNA_BOARD"] = device["board"]
environ["ESPURNA_AUTH"] = device["auth"] environ["ESPURNA_AUTH"] = device["auth"]
environ["ESPURNA_FLAGS"] = device["flags"] environ["ESPURNA_FLAGS"] = device["flags"]
environ["ESPURNA_PIO_SHARED_LIBRARIES"] = "y"
command = ("platformio", "run", "--silent", "--environment", env, "-t", "upload") command = ("platformio", "run", "--silent", "--environment", env, "-t", "upload")
subprocess.check_call(command, env=environ) subprocess.check_call(command, env=environ)


+ 425
- 2236
code/platformio.ini
File diff suppressed because it is too large
View File


+ 1
- 1
code/symbols.sh View File

@ -47,7 +47,7 @@ if [ $ENVIRONMENT == "" ]; then
help help
exit 1 exit 1
fi fi
ELF=.pioenvs/$ENVIRONMENT/firmware.elf
ELF=.pio/build/$ENVIRONMENT/firmware.elf
if [ ! -f $ELF ]; then if [ ! -f $ELF ]; then
echo "Could not find ELF file for the selected environment: $ELF" echo "Could not find ELF file for the selected environment: $ELF"
exit 2 exit 2


+ 39
- 0
dist/arduino_ide/2.3.0/boards.local.txt View File

@ -0,0 +1,39 @@
#version=2.3.0
menu.float_support=scanf and printf float support
generic.menu.FlashSize.1M1S=1M (1 EEPROM Sector, no SPIFFS)
generic.menu.FlashSize.1M1S.build.flash_size=1M
generic.menu.FlashSize.1M1S.build.flash_size_bytes=0x100000
generic.menu.FlashSize.1M1S.build.flash_ld=eagle.flash.1m0m1s.ld
generic.menu.FlashSize.1M1S.build.spiffs_pagesize=256
generic.menu.FlashSize.1M1S.upload.maximum_size=1023984
generic.menu.FlashSize.1M1S.build.rfcal_addr=0xFC000
generic.menu.FlashSize.2M4S=2M (4 EEPROM Sectors, 1M SPIFFS)
generic.menu.FlashSize.2M4S.build.flash_size=2M
generic.menu.FlashSize.2M4S.build.flash_size_bytes=0x200000
generic.menu.FlashSize.2M4S.build.flash_ld=eagle.flash.2m1m4s.ld
generic.menu.FlashSize.2M4S.build.spiffs_pagesize=256
generic.menu.FlashSize.2M4S.upload.maximum_size=1044464
generic.menu.FlashSize.2M4S.build.rfcal_addr=0x1FC000
generic.menu.FlashSize.4M1M4S=4M (4 EEPROM Sectors, 1M SPIFFS)
generic.menu.FlashSize.4M1M4S.build.flash_size=4M
generic.menu.FlashSize.4M1M4S.build.flash_size_bytes=0x400000
generic.menu.FlashSize.4M1M4S.build.flash_ld=eagle.flash.4m1m4s.ld
generic.menu.FlashSize.4M1M4S.build.spiffs_pagesize=256
generic.menu.FlashSize.4M1M4S.upload.maximum_size=1044464
generic.menu.FlashSize.4M1M4S.build.rfcal_addr=0x3FC000
generic.menu.FlashSize.4M3M4S=4M (4 EEPROM Sectors, 3M SPIFFS)
generic.menu.FlashSize.4M3M4S.build.flash_size=4M
generic.menu.FlashSize.4M3M4S.build.flash_size_bytes=0x400000
generic.menu.FlashSize.4M3M4S.build.flash_ld=eagle.flash.4m3m4s.ld
generic.menu.FlashSize.4M3M4S.build.spiffs_pagesize=256
generic.menu.FlashSize.4M3M4S.upload.maximum_size=1044464
generic.menu.FlashSize.4M3M4S.build.rfcal_addr=0x3FC000
generic.menu.float_support.disabled=Disabled (Recommended)
generic.menu.float_support.disabled.build.float=
generic.menu.float_support.enabled=Enabled
generic.menu.float_support.enabled.build.float=-u _printf_float -u _scanf_float
generic.compiler.cpp.extra_flags=-DNO_GLOBAL_EEPROM -DMQTT_MAX_PACKET_SIZE=1024

+ 24
- 0
dist/arduino_ide/README.md View File

@ -0,0 +1,24 @@
# boards.local.txt for ESPurna
Additional flash layouts to support multiple EEPROM sectors
### Installation
Place boards.local.txt into Arduino hardware directory, in the same directory as the boards.txt file. Depending on platform and ESP8266 installation method, it is one of:
- Linux (boards manager): `~/.arduino15/packages/esp8266/hardware/esp8266/<version>`
- Linux (git): `~/Arduino/hardware/esp8266com/esp8266/`
- Windows (boards manager): `%LOCALAPPDATA%\Arduino15\packages\esp8266\hardware\esp8266\<version>`
- Windows (git): `~\Documents\Arduino\hardware\esp8266com\esp8266\`
- macOS (boards manager): `~/Library/Arduino15/packages/esp2866/hardware/esp8266/<version>`
- macOS (git): `<application-directory>/Arduino.app/Contents/Java/hardware/esp8266com/esp8266`
Use `2.3.0/boards.local.txt` for Core version 2.3.0
Use `latest/boards.local.txt` for all the others
### Arduino documentation
https://arduino-esp8266.readthedocs.io/en/latest/installing.html
https://www.arduino.cc/en/Hacking/Preferences
https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5-3rd-party-Hardware-specification#boardslocaltxt

+ 39
- 0
dist/arduino_ide/latest/boards.local.txt View File

@ -0,0 +1,39 @@
#version=latest
menu.float_support=scanf and printf float support
generic.menu.eesz.1M1S=1M (1 EEPROM Sector, no SPIFFS)
generic.menu.eesz.1M1S.build.flash_size=1M
generic.menu.eesz.1M1S.build.flash_size_bytes=0x100000
generic.menu.eesz.1M1S.build.flash_ld=eagle.flash.1m0m1s.ld
generic.menu.eesz.1M1S.build.spiffs_pagesize=256
generic.menu.eesz.1M1S.upload.maximum_size=1023984
generic.menu.eesz.1M1S.build.rfcal_addr=0xFC000
generic.menu.eesz.2M4S=2M (4 EEPROM Sectors, 1M SPIFFS)
generic.menu.eesz.2M4S.build.flash_size=2M
generic.menu.eesz.2M4S.build.flash_size_bytes=0x200000
generic.menu.eesz.2M4S.build.flash_ld=eagle.flash.2m1m4s.ld
generic.menu.eesz.2M4S.build.spiffs_pagesize=256
generic.menu.eesz.2M4S.upload.maximum_size=1044464
generic.menu.eesz.2M4S.build.rfcal_addr=0x1FC000
generic.menu.eesz.4M1M4S=4M (4 EEPROM Sectors, 1M SPIFFS)
generic.menu.eesz.4M1M4S.build.flash_size=4M
generic.menu.eesz.4M1M4S.build.flash_size_bytes=0x400000
generic.menu.eesz.4M1M4S.build.flash_ld=eagle.flash.4m1m4s.ld
generic.menu.eesz.4M1M4S.build.spiffs_pagesize=256
generic.menu.eesz.4M1M4S.upload.maximum_size=1044464
generic.menu.eesz.4M1M4S.build.rfcal_addr=0x3FC000
generic.menu.eesz.4M3M4S=4M (4 EEPROM Sectors, 3M SPIFFS)
generic.menu.eesz.4M3M4S.build.flash_size=4M
generic.menu.eesz.4M3M4S.build.flash_size_bytes=0x400000
generic.menu.eesz.4M3M4S.build.flash_ld=eagle.flash.4m3m4s.ld
generic.menu.eesz.4M3M4S.build.spiffs_pagesize=256
generic.menu.eesz.4M3M4S.upload.maximum_size=1044464
generic.menu.eesz.4M3M4S.build.rfcal_addr=0x3FC000
generic.menu.float_support.disabled=Disabled (Recommended)
generic.menu.float_support.disabled.build.float=
generic.menu.float_support.enabled=Enabled
generic.menu.float_support.enabled.build.float=-u _printf_float -u _scanf_float
generic.compiler.cpp.extra_flags=-DNO_GLOBAL_EEPROM -DMQTT_MAX_PACKET_SIZE=1024

+ 187
- 0
dist/boards_local_txt.py View File

@ -0,0 +1,187 @@
#!/usr/bin/env python
# adapted boards.txt.py from esp8266/Arduino
# - single board definition, ldscripts
# - portable boards.local.txt for 2.3.0 and up
import os
import argparse
import sys
import collections
# TODO: drop after platform.io supports python 3
if sys.version < (3, 2):
import string
_format = string.Formatter().vformat
def format_map(tmpl, f_map):
return _format(tmpl, None, f_map)
else:
def format_map(tmpl, f_map):
return tmpl.format_map(f_map)
class VersionedSubstitution(collections.MutableMapping):
def __init__(self, substitutions, targets, *args, **kwargs):
self._targets = targets
self._version = None
self._store = substitutions.copy()
self.update(dict(*args, **kwargs))
def set_version(self, version):
self._version = version
def __getitem__(self, key):
if self._version in self._targets:
return self._store[key]
return key
def __setitem__(self, key, value):
self._store[key] = value
def __delitem__(self, key):
del self._store[key]
def __iter__(self):
return iter(self._store)
def __len__(self):
return len(self._store)
BOARDS_LOCAL = {
"global": collections.OrderedDict(
[("menu.float_support", "scanf and printf float support")]
),
"flash_size": collections.OrderedDict(
[
(".menu.{eesz}.1M1S", "1M (1 EEPROM Sector, no SPIFFS)"),
(".menu.{eesz}.1M1S.build.flash_size", "1M"),
(".menu.{eesz}.1M1S.build.flash_size_bytes", "0x100000"),
(".menu.{eesz}.1M1S.build.flash_ld", "eagle.flash.1m0m1s.ld"),
(".menu.{eesz}.1M1S.build.spiffs_pagesize", "256"),
(".menu.{eesz}.1M1S.upload.maximum_size", "1023984"),
(".menu.{eesz}.1M1S.build.rfcal_addr", "0xFC000"),
(".menu.{eesz}.2M4S", "2M (4 EEPROM Sectors, 1M SPIFFS)"),
(".menu.{eesz}.2M4S.build.flash_size", "2M"),
(".menu.{eesz}.2M4S.build.flash_size_bytes", "0x200000"),
(".menu.{eesz}.2M4S.build.flash_ld", "eagle.flash.2m1m4s.ld"),
(".menu.{eesz}.2M4S.build.spiffs_pagesize", "256"),
(".menu.{eesz}.2M4S.upload.maximum_size", "1044464"),
(".menu.{eesz}.2M4S.build.rfcal_addr", "0x1FC000"),
(".menu.{eesz}.4M1M4S", "4M (4 EEPROM Sectors, 1M SPIFFS)"),
(".menu.{eesz}.4M1M4S.build.flash_size", "4M"),
(".menu.{eesz}.4M1M4S.build.flash_size_bytes", "0x400000"),
(".menu.{eesz}.4M1M4S.build.flash_ld", "eagle.flash.4m1m4s.ld"),
(".menu.{eesz}.4M1M4S.build.spiffs_pagesize", "256"),
(".menu.{eesz}.4M1M4S.upload.maximum_size", "1044464"),
(".menu.{eesz}.4M1M4S.build.rfcal_addr", "0x3FC000"),
(".menu.{eesz}.4M3M4S", "4M (4 EEPROM Sectors, 3M SPIFFS)"),
(".menu.{eesz}.4M3M4S.build.flash_size", "4M"),
(".menu.{eesz}.4M3M4S.build.flash_size_bytes", "0x400000"),
(".menu.{eesz}.4M3M4S.build.flash_ld", "eagle.flash.4m3m4s.ld"),
(".menu.{eesz}.4M3M4S.build.spiffs_pagesize", "256"),
(".menu.{eesz}.4M3M4S.upload.maximum_size", "1044464"),
(".menu.{eesz}.4M3M4S.build.rfcal_addr", "0x3FC000"),
]
),
"float_support": collections.OrderedDict(
[
(".menu.float_support.disabled", "Disabled (Recommended)"),
(".menu.float_support.disabled.build.float", ""),
(".menu.float_support.enabled", "Enabled"),
(
".menu.float_support.enabled.build.float",
"-u _printf_float -u _scanf_float",
),
]
),
}
BOARD = "generic"
MENUS = ["flash_size", "float_support"]
CORE_VERSIONS = ["2.3.0", "latest"]
EXTRA_FLAGS = [
(".compiler.cpp.extra_flags", "-DNO_GLOBAL_EEPROM -DMQTT_MAX_PACKET_SIZE=1024")
]
SUBSTITUTIONS = VersionedSubstitution(
dict(eesz="FlashSize", wipe="FlashErase", baud="UploadSpeed", vt="VTable"),
["2.3.0"],
)
def generate_boards_txt(args, sub=SUBSTITUTIONS):
versions = args.versions
if args.version:
versions = [args.version]
for version in versions:
sub.set_version(version)
result = ["#version={}\n\n".format(version)]
result.extend("{}={}\n".format(k, v) for k, v in BOARDS_LOCAL["global"].items())
result.append("\n")
# print("{} unused:".format(version, set(BOARDS_LOCAL.keys()) - set(MENUS[version])))
# continue
for menu in MENUS:
section = []
for k, v in BOARDS_LOCAL[menu].items():
k = format_map(k, sub)
section.append(BOARD + "=".join([k, v]))
result.append("\n".join(section))
result.append("\n\n")
if EXTRA_FLAGS:
result.extend(("{}{}={}".format(BOARD, k, v)) for k, v in EXTRA_FLAGS)
f_path = os.path.join(args.directory, version, "boards.local.txt")
f_dir, _ = os.path.split(f_path)
if not os.path.exists(f_dir):
os.makedirs(f_dir)
with open(f_path, "w") as f:
for part in result:
f.write(part)
f.write("\n")
def print_versions(args):
for version in CORE_VERSIONS:
print("- {}".format(version))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-d",
"--directory",
default=os.path.join(
os.path.dirname(os.path.realpath(__file__)), "arduino_ide"
),
)
subparsers = parser.add_subparsers(title="commands")
parser_versions = subparsers.add_parser("versions", help="list supported versions")
parser_versions.set_defaults(command=print_versions)
parser_generate = subparsers.add_parser("generate", help="")
parser_generate.add_argument("version", nargs="?")
parser_generate.set_defaults(command=generate_boards_txt, versions=CORE_VERSIONS)
args = parser.parse_args()
args.command(args)

+ 8
- 0
dist/ld/README.md View File

@ -0,0 +1,8 @@
# ESP8266 linker scripts with additional EEPROM sectors
### Installation
Depending on ESP8266 version, use files from either `Pre 2.5.0` or `latest`
Reference [../arduino_ide/README.md](../arduino_ide/README.md) about ESP8266 package location
Copy all \*.ld files into the `<esp8266-package>/tools/sdk/ld` directory

+ 30
- 0
dist/ld/latest/eagle.flash.1m0m1s.ld View File

@ -0,0 +1,30 @@
/*
sketch: 999KB
fs: 0KB
eeprom: 4KB
*/
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xf9ff0
}
/*
Provide both _SPIFFS_ and _FS_ to be compatible with 2.3.0...2.6.0+ and
any library that is using old _SPIFFS_...
*/
PROVIDE ( _SPIFFS_start = 0x402fb000 );
PROVIDE ( _SPIFFS_end = 0x402fb000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
PROVIDE ( _FS_start = _SPIFFS_start );
PROVIDE ( _FS_end = _SPIFFS_end );
PROVIDE ( _FS_page = _SPIFFS_page );
PROVIDE ( _FS_block = _SPIFFS_block );
INCLUDE "local.eagle.app.v6.common.ld"

+ 30
- 0
dist/ld/latest/eagle.flash.1m0m2s.ld View File

@ -0,0 +1,30 @@
/*
sketch: 995KB
fs: 0KB
eeprom: 8KB
*/
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xf8ff0
}
/*
Provide both _SPIFFS_ and _FS_ to be compatible with 2.3.0...2.6.0+ and
any library that is using old _SPIFFS_...
*/
PROVIDE ( _SPIFFS_start = 0x402fa000 );
PROVIDE ( _SPIFFS_end = 0x402fa000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
PROVIDE ( _FS_start = _SPIFFS_start );
PROVIDE ( _FS_end = _SPIFFS_end );
PROVIDE ( _FS_page = _SPIFFS_page );
PROVIDE ( _FS_block = _SPIFFS_block );
INCLUDE "local.eagle.app.v6.common.ld"

+ 30
- 0
dist/ld/latest/eagle.flash.2m1m4s.ld View File

@ -0,0 +1,30 @@
/*
sketch: 1019KB
fs: 992KB
eeprom: 16KB
*/
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
/*
Provide both _SPIFFS_ and _FS_ to be compatible with 2.3.0...2.6.0+ and
any library that is using old _SPIFFS_...
*/
PROVIDE ( _SPIFFS_start = 0x40300000 );
PROVIDE ( _SPIFFS_end = 0x403f8000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
PROVIDE ( _FS_start = _SPIFFS_start );
PROVIDE ( _FS_end = _SPIFFS_end );
PROVIDE ( _FS_page = _SPIFFS_page );
PROVIDE ( _FS_block = _SPIFFS_block );
INCLUDE "local.eagle.app.v6.common.ld"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save