Feature: Make RGB LED (preset colors) accessible via API/etc #3039

Fixes #3063
This commit is contained in:
proddy
2026-05-15 10:07:17 +02:00
parent 208717a896
commit 33fda705c0
15 changed files with 514 additions and 280 deletions

View File

@@ -18,6 +18,7 @@ For more details go to [emsesp.org](https://emsesp.org/).
- updated version check [#3047](https://github.com/emsesp/EMS-ESP32/issues/3047) - updated version check [#3047](https://github.com/emsesp/EMS-ESP32/issues/3047)
- auto-logic to set ht3/ems+ tx-mode - auto-logic to set ht3/ems+ tx-mode
- polarity for digital_in sensors [#3070](https://github.com/emsesp/EMS-ESP32/discussions/3070) - polarity for digital_in sensors [#3070](https://github.com/emsesp/EMS-ESP32/discussions/3070)
- user-requested LED blink [#3063](https://github.com/emsesp/EMS-ESP32/issues/3063)
## Fixed ## Fixed

View File

@@ -38,7 +38,7 @@
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"react-icons": "^5.6.0", "react-icons": "^5.6.0",
"react-router": "^7.15.0", "react-router": "^7.15.1",
"react-toastify": "^11.1.0", "react-toastify": "^11.1.0",
"typesafe-i18n": "^5.27.1", "typesafe-i18n": "^5.27.1",
"typescript": "^6.0.3" "typescript": "^6.0.3"
@@ -47,7 +47,7 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@preact/preset-vite": "^2.10.5", "@preact/preset-vite": "^2.10.5",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.7.0", "@types/node": "^25.8.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View File

@@ -54,8 +54,8 @@ importers:
specifier: ^5.6.0 specifier: ^5.6.0
version: 5.6.0(react@19.2.6) version: 5.6.0(react@19.2.6)
react-router: react-router:
specifier: ^7.15.0 specifier: ^7.15.1
version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react-toastify: react-toastify:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 11.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -71,13 +71,13 @@ importers:
version: 10.0.1(eslint@10.3.0) version: 10.0.1(eslint@10.3.0)
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.10.5 specifier: ^2.10.5
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)) version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))
'@trivago/prettier-plugin-sort-imports': '@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2(prettier@3.8.3) version: 6.0.2(prettier@3.8.3)
'@types/node': '@types/node':
specifier: ^25.7.0 specifier: ^25.8.0
version: 25.7.0 version: 25.8.0
'@types/react': '@types/react':
specifier: ^19.2.14 specifier: ^19.2.14
version: 19.2.14 version: 19.2.14
@@ -107,10 +107,10 @@ importers:
version: 8.59.3(eslint@10.3.0)(typescript@6.0.3) version: 8.59.3(eslint@10.3.0)(typescript@6.0.3)
vite: vite:
specifier: ^8.0.13 specifier: ^8.0.13
version: 8.0.13(@types/node@25.7.0)(terser@5.47.1) version: 8.0.13(@types/node@25.8.0)(terser@5.47.1)
vite-plugin-imagemin: vite-plugin-imagemin:
specifier: ^0.6.1 specifier: ^0.6.1
version: 0.6.1(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)) version: 0.6.1(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))
packages: packages:
@@ -829,8 +829,8 @@ packages:
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
'@types/node@25.7.0': '@types/node@25.8.0':
resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==}
'@types/parse-json@4.0.2': '@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1330,8 +1330,8 @@ packages:
duplexer3@0.1.5: duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.355: electron-to-chromium@1.5.356:
resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} resolution: {integrity: sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==}
emoji-regex@10.6.0: emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2589,8 +2589,8 @@ packages:
react-is@19.2.6: react-is@19.2.6:
resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==}
react-router@7.15.0: react-router@7.15.1:
resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==} resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
peerDependencies: peerDependencies:
react: '>=18' react: '>=18'
@@ -3010,8 +3010,8 @@ packages:
unbzip2-stream@1.4.3: unbzip2-stream@1.4.3:
resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
undici-types@7.21.0: undici-types@7.24.6:
resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
universalify@2.0.1: universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
@@ -3604,19 +3604,19 @@ snapshots:
'@popperjs/core@2.11.8': {} '@popperjs/core@2.11.8': {}
'@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1))': '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0)
'@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)) '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))
'@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@rollup/pluginutils': 5.3.0(rollup@4.59.0)
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
debug: 4.4.3 debug: 4.4.3
magic-string: 0.30.21 magic-string: 0.30.21
picocolors: 1.1.1 picocolors: 1.1.1
vite: 8.0.13(@types/node@25.7.0)(terser@5.47.1) vite: 8.0.13(@types/node@25.8.0)(terser@5.47.1)
vite-prerender-plugin: 0.5.13(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)) vite-prerender-plugin: 0.5.13(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))
zimmerframe: 1.1.4 zimmerframe: 1.1.4
transitivePeerDependencies: transitivePeerDependencies:
- preact - preact
@@ -3631,7 +3631,7 @@ snapshots:
'@prefresh/utils@1.2.1': {} '@prefresh/utils@1.2.1': {}
'@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1))': '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@prefresh/babel-plugin': 0.5.3 '@prefresh/babel-plugin': 0.5.3
@@ -3639,7 +3639,7 @@ snapshots:
'@prefresh/utils': 1.2.1 '@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
preact: 10.29.1 preact: 10.29.1
vite: 8.0.13(@types/node@25.7.0)(terser@5.47.1) vite: 8.0.13(@types/node@25.8.0)(terser@5.47.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3822,7 +3822,7 @@ snapshots:
'@types/glob@7.2.0': '@types/glob@7.2.0':
dependencies: dependencies:
'@types/minimatch': 6.0.0 '@types/minimatch': 6.0.0
'@types/node': 25.7.0 '@types/node': 25.8.0
'@types/imagemin-gifsicle@7.0.4': '@types/imagemin-gifsicle@7.0.4':
dependencies: dependencies:
@@ -3851,21 +3851,21 @@ snapshots:
'@types/imagemin@7.0.1': '@types/imagemin@7.0.1':
dependencies: dependencies:
'@types/node': 25.7.0 '@types/node': 25.8.0
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/keyv@3.1.4': '@types/keyv@3.1.4':
dependencies: dependencies:
'@types/node': 25.7.0 '@types/node': 25.8.0
'@types/minimatch@6.0.0': '@types/minimatch@6.0.0':
dependencies: dependencies:
minimatch: 10.2.5 minimatch: 10.2.5
'@types/node@25.7.0': '@types/node@25.8.0':
dependencies: dependencies:
undici-types: 7.21.0 undici-types: 7.24.6
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
@@ -3885,11 +3885,11 @@ snapshots:
'@types/responselike@1.0.3': '@types/responselike@1.0.3':
dependencies: dependencies:
'@types/node': 25.7.0 '@types/node': 25.8.0
'@types/svgo@2.6.4': '@types/svgo@2.6.4':
dependencies: dependencies:
'@types/node': 25.7.0 '@types/node': 25.8.0
'@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)': '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.3.0)(typescript@6.0.3))(eslint@10.3.0)(typescript@6.0.3)':
dependencies: dependencies:
@@ -4109,7 +4109,7 @@ snapshots:
dependencies: dependencies:
baseline-browser-mapping: 2.10.29 baseline-browser-mapping: 2.10.29
caniuse-lite: 1.0.30001792 caniuse-lite: 1.0.30001792
electron-to-chromium: 1.5.355 electron-to-chromium: 1.5.356
node-releases: 2.0.44 node-releases: 2.0.44
update-browserslist-db: 1.2.3(browserslist@4.28.2) update-browserslist-db: 1.2.3(browserslist@4.28.2)
@@ -4468,7 +4468,7 @@ snapshots:
duplexer3@0.1.5: {} duplexer3@0.1.5: {}
electron-to-chromium@1.5.355: {} electron-to-chromium@1.5.356: {}
emoji-regex@10.6.0: {} emoji-regex@10.6.0: {}
@@ -5648,7 +5648,7 @@ snapshots:
react-is@19.2.6: {} react-is@19.2.6: {}
react-router@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies: dependencies:
cookie: 1.1.1 cookie: 1.1.1
react: 19.2.6 react: 19.2.6
@@ -6088,7 +6088,7 @@ snapshots:
buffer: 5.7.1 buffer: 5.7.1
through: 2.3.8 through: 2.3.8
undici-types@7.21.0: {} undici-types@7.24.6: {}
universalify@2.0.1: {} universalify@2.0.1: {}
@@ -6121,7 +6121,7 @@ snapshots:
spdx-correct: 3.2.0 spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
vite-plugin-imagemin@0.6.1(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)): vite-plugin-imagemin@0.6.1(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1)):
dependencies: dependencies:
'@types/imagemin': 7.0.1 '@types/imagemin': 7.0.1
'@types/imagemin-gifsicle': 7.0.4 '@types/imagemin-gifsicle': 7.0.4
@@ -6146,11 +6146,11 @@ snapshots:
imagemin-webp: 6.1.0 imagemin-webp: 6.1.0
jpegtran-bin: 6.0.1 jpegtran-bin: 6.0.1
pathe: 0.2.0 pathe: 0.2.0
vite: 8.0.13(@types/node@25.7.0)(terser@5.47.1) vite: 8.0.13(@types/node@25.8.0)(terser@5.47.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-prerender-plugin@0.5.13(vite@8.0.13(@types/node@25.7.0)(terser@5.47.1)): vite-prerender-plugin@0.5.13(vite@8.0.13(@types/node@25.8.0)(terser@5.47.1)):
dependencies: dependencies:
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.21 magic-string: 0.30.21
@@ -6158,9 +6158,9 @@ snapshots:
simple-code-frame: 1.3.0 simple-code-frame: 1.3.0
source-map: 0.7.6 source-map: 0.7.6
stack-trace: 1.0.0 stack-trace: 1.0.0
vite: 8.0.13(@types/node@25.7.0)(terser@5.47.1) vite: 8.0.13(@types/node@25.8.0)(terser@5.47.1)
vite@8.0.13(@types/node@25.7.0)(terser@5.47.1): vite@8.0.13(@types/node@25.8.0)(terser@5.47.1):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.4 picomatch: 4.0.4
@@ -6168,7 +6168,7 @@ snapshots:
rolldown: 1.0.1 rolldown: 1.0.1
tinyglobby: 0.2.16 tinyglobby: 0.2.16
optionalDependencies: optionalDependencies:
'@types/node': 25.7.0 '@types/node': 25.8.0
fsevents: 2.3.3 fsevents: 2.3.3
terser: 5.47.1 terser: 5.47.1

View File

@@ -90,6 +90,7 @@ Network EMSESP::network_; // network services
TemperatureSensor EMSESP::temperaturesensor_; // Temperature sensors TemperatureSensor EMSESP::temperaturesensor_; // Temperature sensors
AnalogSensor EMSESP::analogsensor_; // Analog sensors AnalogSensor EMSESP::analogsensor_; // Analog sensors
Shower EMSESP::shower_; // Shower logic Shower EMSESP::shower_; // Shower logic
LED EMSESP::led_; // LED handler
Preferences EMSESP::nvs_; // NV Storage Preferences EMSESP::nvs_; // NV Storage
// for a specific EMS device go and request data values // for a specific EMS device go and request data values
@@ -1532,7 +1533,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
Roomctrl::check(data[1], data, length); Roomctrl::check(data[1], data, length);
#ifdef EMSESP_UART_DEBUG #ifdef EMSESP_UART_DEBUG
// get_uptime is only updated once per loop, does not give the right time // get_uptime is only updated once per loop, does not give the right time
LOG_TRACE("[UART_DEBUG] Echo after %d ms: %s", ::millis() - rx_time_, Helpers::data_to_hex(data, length).c_str()); LOG_TRACE("[UART_DEBUG] Echo after %d ms: %s", uuid::get_uptime_ms() - rx_time_, Helpers::data_to_hex(data, length).c_str());
#endif #endif
// add to RxQueue for log/watch // add to RxQueue for log/watch
rxservice_.add(data, length); rxservice_.add(data, length);
@@ -1616,11 +1617,11 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
#ifdef EMSESP_UART_DEBUG #ifdef EMSESP_UART_DEBUG
char s[4]; char s[4];
if (first_value & 0x80) { if (first_value & 0x80) {
LOG_TRACE("[UART_DEBUG] next Poll %s after %d ms", Helpers::hextoa(s, first_value), ::millis() - rx_time_); LOG_TRACE("[UART_DEBUG] next Poll %s after %d ms", Helpers::hextoa(s, first_value), uuid::get_uptime_ms() - rx_time_);
// time measurement starts here, use millis because get_uptime is only updated once per loop // time measurement starts here, use millis because get_uptime is only updated once per loop
rx_time_ = ::millis(); rx_time_ = uuid::get_uptime_ms();
} else { } else {
LOG_TRACE("[UART_DEBUG] Poll ack %s after %d ms", Helpers::hextoa(s, first_value), ::millis() - rx_time_); LOG_TRACE("[UART_DEBUG] Poll ack %s after %d ms", Helpers::hextoa(s, first_value), uuid::get_uptime_ms() - rx_time_);
} }
#endif #endif
// check for poll to us, if so send top message from Tx queue immediately and quit // check for poll to us, if so send top message from Tx queue immediately and quit
@@ -1634,7 +1635,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
return; return;
} else { } else {
#ifdef EMSESP_UART_DEBUG #ifdef EMSESP_UART_DEBUG
LOG_TRACE("[UART_DEBUG] Reply after %d ms: %s", ::millis() - rx_time_, Helpers::data_to_hex(data, length).c_str()); LOG_TRACE("[UART_DEBUG] Reply after %d ms: %s", uuid::get_uptime_ms() - rx_time_, Helpers::data_to_hex(data, length).c_str());
#endif #endif
Roomctrl::check(data[1], data, length); // check if there is a message for the roomcontroller Roomctrl::check(data[1], data, length); // check if there is a message for the roomcontroller
@@ -1711,10 +1712,10 @@ void EMSESP::start() {
bool factory_settings = false; bool factory_settings = false;
#endif #endif
#if defined(EMSESP_DEBUG) // #if defined(EMSESP_DEBUG)
// LOG_DEBUG("Listing root directory before:"); // LOG_DEBUG("Listing root directory before:");
// system_.listDir("/", 3); // show the contents of the root directory // system_.listDir("/", 3); // show the contents of the root directory
#endif // #endif
// start NVS storage // start NVS storage
if (!nvs_.begin("ems-esp", false, "nvs1")) { // try bigger nvs partition on 16M flash first if (!nvs_.begin("ems-esp", false, "nvs1")) { // try bigger nvs partition on 16M flash first
@@ -1730,10 +1731,10 @@ void EMSESP::start() {
// loads core system services settings (mqtt, ap, ntp etc) // loads core system services settings (mqtt, ap, ntp etc)
esp32React.begin(); esp32React.begin();
#if defined(EMSESP_DEBUG) // #if defined(EMSESP_DEBUG)
// LOG_DEBUG("Listing root directory after:"); // LOG_DEBUG("Listing root directory after:");
// system_.listDir("/", 3); // show the contents of the root directory // system_.listDir("/", 3); // show the contents of the root directory
#endif // #endif
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if (factory_settings) { if (factory_settings) {
@@ -1866,7 +1867,7 @@ void EMSESP::loop() {
// handles LED and checks system health, and syslog service // handles LED and checks system health, and syslog service
if (system_.loop()) { if (system_.loop()) {
return; // LED flashing is active, skip the rest of the loop return; // LED flashing is active meaning its about to reboot, skip the rest of the loop
} }
esp32React.loop(); // core services: Network, AP, MQTT and NTP esp32React.loop(); // core services: Network, AP, MQTT and NTP

View File

@@ -233,6 +233,7 @@ class EMSESP {
static TemperatureSensor temperaturesensor_; static TemperatureSensor temperaturesensor_;
static AnalogSensor analogsensor_; static AnalogSensor analogsensor_;
static Shower shower_; static Shower shower_;
static LED led_;
static RxService rxservice_; static RxService rxservice_;
static TxService txservice_; static TxService txservice_;
static Preferences nvs_; static Preferences nvs_;

287
src/core/led.cpp Normal file
View File

@@ -0,0 +1,287 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2025 emsesp.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "led.h"
#include "emsesp.h"
namespace emsesp {
uuid::log::Logger LED::logger_{F_(led), uuid::log::Facility::KERN};
// initialise the LED, fetching the settings from the WebSettingsService
// set the LED to on or off when in normal operating mode
void LED::init() {
// copy the application settings
EMSESP::webSettingsService.read([&](WebSettings & settings) {
led_gpio_ = settings.led_gpio;
led_type_ = settings.led_type;
hide_led_ = settings.hide_led;
});
if (!led_gpio_) { // 0 means disabled
LOG_INFO("LED disabled");
return;
}
// for safety
if (!led_type_) {
pinMode(led_gpio_, OUTPUT);
}
reset_led(false); // start with LED off
}
// handle LED routine
// called from the System::loop()
// returns true if the LED flash is active, i.e its a lock down state
bool LED::loop(uint8_t healthcheck, bool button_busy) {
if (!led_gpio_) {
return false;
}
// if LED flashing is active it means its about to perform a factory reset, so don't do anything else and keep it flashing
if (led_fast_flash_timer_) {
led_fast_flash();
return true;
}
// show the LED status based on the healthcheck and button busy status
monitor(healthcheck, button_busy);
return false;
}
// turn the LED off (false) or to it's default state (true)
void LED::reset_led(bool default_state) {
if (!led_gpio_) {
return;
}
if (default_state) {
// set the LED to it's default state (for RGB its green)
set_led(hide_led_ ? Color::OFF : Color::GREEN); // Green
} else {
// force it off
set_led(Color::OFF);
}
}
// LED flash every few ms and then perform a factory reset
void LED::led_fast_flash() {
uint32_t current_time = uuid::get_uptime();
if (current_time - last_toggle_time_ >= LED_FLASH_INTERVAL_MS) {
led_flash_state_ = !led_flash_state_;
last_toggle_time_ = current_time;
set_led(led_flash_state_ ? Color::YELLOW : Color::OFF); // Yellow
}
// after duration, turn off the LED
if (current_time - led_flash_start_time_ >= led_flash_duration_) {
reset_led(false);
led_fast_flash_timer_ = false;
#ifndef EMSESP_DEBUG
System::command_format(nullptr, 0); // Execute format operation, unless in debug mode
#endif
}
}
// set LED on/off or RGB color
// ignores whether the LED is hidden or not (hide_led_ is set)
void LED::set_led(Color color) {
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
if (color == Color::RED) {
red = RGB_LED_BRIGHTNESS;
green = 0;
blue = 0;
} else if (color == Color::GREEN) {
red = 0;
green = RGB_LED_BRIGHTNESS;
blue = 0;
} else if (color == Color::BLUE) {
red = 0;
green = 0;
blue = RGB_LED_BRIGHTNESS;
} else if (color == Color::YELLOW) {
red = RGB_LED_BRIGHTNESS;
green = RGB_LED_BRIGHTNESS;
blue = 0;
} else if (color == Color::OFF) { // off
red = 0;
green = 0;
blue = 0;
} else if (color == Color::ON) { // white
red = RGB_LED_BRIGHTNESS;
green = RGB_LED_BRIGHTNESS;
blue = RGB_LED_BRIGHTNESS;
}
if (led_type_) {
rgbLedWrite(led_gpio_, red, green, blue);
} else {
digitalWrite(led_gpio_, (red == 0 && green == 0 && blue == 0) || color == Color::OFF ? !LED_ON : LED_ON);
}
}
// set LED custom routine
// color is red, green, blue, yellow, white
// pattern is blink1, blink2, blink3, rgb
// For example: /api/system/led?data=red:blink1
// For older non-RGB models, the colour would default to just being on.
void LED::set_led_routine(std::string color, std::string pattern) {
Color color_type = Color::OFF;
if (color == "red") {
color_type = Color::RED;
} else if (color == "green") {
color_type = Color::GREEN;
} else if (color == "blue") {
color_type = Color::BLUE;
} else if (color == "yellow") {
color_type = Color::YELLOW;
} else if (color == "white") {
color_type = Color::ON;
} else {
color_type = Color::OFF;
}
color_steps_[0] = Color::OFF;
color_steps_[1] = Color::OFF;
color_steps_[2] = Color::OFF;
if (pattern == "blink1") {
color_steps_[0] = color_type;
} else if (pattern == "blink2") {
color_steps_[0] = color_type;
color_steps_[1] = color_type;
} else if (pattern == "blink3") {
color_steps_[0] = color_type;
color_steps_[1] = color_type;
color_steps_[2] = color_type;
} else if (pattern == "rgb") {
color_steps_[0] = Color::RED;
color_steps_[1] = Color::GREEN;
color_steps_[2] = Color::BLUE;
}
}
// uses LED to show system health and user-requested LED blinks
// it works in a batch of 3 configured flashes, then a long pause
// the timing is different for user-requested LED blink and for system healthcheck
void LED::monitor(uint8_t healthcheck, bool button_busy) {
// see if we're doing as user-requested LED blink
bool is_user_led_blink = false;
if (color_steps_[0] != Color::OFF || color_steps_[1] != Color::OFF || color_steps_[2] != Color::OFF) {
is_user_led_blink = true;
}
// if button is pressed, show LED (yellow on RGB LED, on/off on standard LED) and exit
if (last_button_busy_ != button_busy) {
last_button_busy_ = button_busy;
set_led(button_busy ? Color::OFF : Color::YELLOW); // Yellow
}
// we only need to run the LED monitor if there are errors, or if a button has been pressed or a user-requested LED blink is active
if ((!healthcheck || button_busy) && !is_user_led_blink) {
return; // nothing to show
}
// first long pause before we start flashing
auto current_time = uuid::get_uptime();
if (led_long_timer_
&& (uint32_t)(current_time - led_long_timer_) >= (is_user_led_blink ? HEALTHCHECK_LED_LONG_FAST_DURATION : HEALTHCHECK_LED_LONG_DURATION)) {
led_short_timer_ = current_time; // start the short timer
led_long_timer_ = 0; // stop long timer
led_flash_step_ = 1; // enable the short flash timer
}
// the flash timer which starts after the long pause
if (led_flash_step_
&& (uint32_t)(current_time - led_short_timer_) >= (is_user_led_blink ? HEALTHCHECK_LED_FLASH_FAST_DURATION : HEALTHCHECK_LED_FLASH_DURATION)) {
led_long_timer_ = 0; // stop the long timer
led_short_timer_ = current_time;
if (++led_flash_step_ == 8) {
// finished first iteration, reset the whole sequence
led_long_timer_ = uuid::get_uptime();
led_flash_step_ = 0;
reset_led(true); // LED back to what is was before
} else if (led_flash_step_ % 2) {
// handle the three step events (on odd numbers 3,5,7 etc). see if we need to set a LED color
// For the system healthcheck:
// 1 flash (blue) is the EMS bus is not connected
// 2 flashes (red, red) if the network (wifi or ethernet) is not connected
// 3 flashes (red, red, blue) is both the bus and the network are not connected
bool no_network = (healthcheck & System::HEALTHCHECK_NO_NETWORK) == System::HEALTHCHECK_NO_NETWORK;
bool no_bus = (healthcheck & System::HEALTHCHECK_NO_BUS) == System::HEALTHCHECK_NO_BUS;
switch (led_flash_step_) {
case 3: // first flash
if (is_user_led_blink) {
set_led(color_steps_[0]);
color_steps_[0] = Color::OFF; // reset
} else {
if (no_network) {
set_led(Color::RED); // red, no network
} else if (no_bus) {
set_led(Color::BLUE); // blue, no bus
}
}
break;
case 5: // second flash
if (is_user_led_blink) {
set_led(color_steps_[1]);
color_steps_[1] = Color::OFF; // reset
} else if (no_network) {
set_led(Color::RED); // red, no network
}
break;
case 7: // third flash
if (is_user_led_blink) {
set_led(color_steps_[2]);
color_steps_[2] = Color::OFF; // reset
} else if (no_network && no_bus) {
set_led(Color::BLUE); // blue, no network and no bus
}
break;
default:
break;
}
} else {
// turn off LED after the LED flash, or on even number count
set_led(Color::OFF);
}
}
}
// Start the LED flash timer - duration in seconds
void LED::start_led_fast_flash(uint8_t duration) {
// Don't start if already running
if (led_fast_flash_timer_) {
return;
}
// Reset counter and state
led_flash_start_time_ = uuid::get_uptime(); // current time
led_flash_duration_ = (uint32_t)duration * 1000; // duration in milliseconds
led_fast_flash_timer_ = true; // it's active
}
} // namespace emsesp

81
src/core/led.h Normal file
View File

@@ -0,0 +1,81 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2025 emsesp.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_LED_H_
#define EMSESP_LED_H_
#include <Arduino.h>
#include <uuid/log.h>
#include "emsesp_common.h"
namespace emsesp {
class LED {
public:
enum Color { RED = 0, GREEN = 1, BLUE = 2, YELLOW = 3, OFF = 4, ON = 5 };
void init();
bool loop(uint8_t healthcheck, bool button_busy);
void reset_led(bool default_state = true); // turn the LED to default state or use false for off
void start_led_fast_flash(uint8_t duration); // duration in seconds
void set_led(Color color);
void set_led_routine(std::string color, std::string pattern);
private:
static uuid::log::Logger logger_;
void monitor(uint8_t led_routine, bool button_busy);
void led_fast_flash();
static constexpr uint32_t HEALTHCHECK_LED_LONG_DURATION = 1000; // 1 second between flash sequences
static constexpr uint32_t HEALTHCHECK_LED_LONG_FAST_DURATION = 500; // 1/2 second between flash sequences
static constexpr uint32_t HEALTHCHECK_LED_FLASH_DURATION = 150; // 150ms
static constexpr uint32_t HEALTHCHECK_LED_FLASH_FAST_DURATION = 150;
static constexpr uint32_t LED_FLASH_INTERVAL_MS = 100; // LED toggle period during factory-reset flash
static constexpr uint8_t RGB_LED_BRIGHTNESS = 20; // 255 is max brightness
static constexpr uint8_t LED_ON = HIGH; // LED on
// local copies of the application settings
uint8_t led_gpio_ = 0;
uint8_t led_type_ = 0;
bool hide_led_ = false;
bool led_fast_flash_timer_ = false;
uint32_t led_flash_start_time_ = 0;
uint32_t led_flash_duration_ = 0;
// led_flash() state
bool led_flash_state_ = false;
uint32_t last_toggle_time_ = 0;
// monitor() state
bool last_button_busy_ = false;
uint32_t led_long_timer_ = 1; // 1 will kick it off immediately
uint32_t led_short_timer_ = 0;
uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer
// set_led_routine() state
Color color_steps_[3] = {Color::OFF, Color::OFF, Color::OFF};
};
} // namespace emsesp
#endif

View File

@@ -43,6 +43,7 @@ MAKE_WORD(fetch)
MAKE_WORD(restart) MAKE_WORD(restart)
MAKE_WORD(format) MAKE_WORD(format)
MAKE_WORD(txpause) MAKE_WORD(txpause)
MAKE_WORD(led)
MAKE_WORD(raw) MAKE_WORD(raw)
MAKE_WORD(watch) MAKE_WORD(watch)
MAKE_WORD(syslog) MAKE_WORD(syslog)
@@ -171,7 +172,6 @@ MAKE_WORD_CUSTOM(password_prompt, "Password: ")
MAKE_WORD_CUSTOM(unset, "<unset>") MAKE_WORD_CUSTOM(unset, "<unset>")
MAKE_WORD_CUSTOM(enable_mandatory, "<enable | disable>") MAKE_WORD_CUSTOM(enable_mandatory, "<enable | disable>")
MAKE_WORD_CUSTOM(service_mandatory, "<ap | mqtt | ntp>") MAKE_WORD_CUSTOM(service_mandatory, "<ap | mqtt | ntp>")
MAKE_WORD_CUSTOM(txpause_cmd, "enable/disable TX")
// more common names that don't need translations // more common names that don't need translations
MAKE_NOTRANSLATION(1x3min, "1x3min") MAKE_NOTRANSLATION(1x3min, "1x3min")

View File

@@ -85,6 +85,7 @@ MAKE_WORD_TRANSLATION(system_cmd, "system setting", "System Einstellung", "syste
MAKE_WORD_TRANSLATION(showertimer_cmd, "enable shower timer", "aktiviere Duschzeitmessung", "activeer douche timer", "aktivera duschtimer", "aktywuj czasomierz prysznica", "aktiver dusjtimer", "activer minuteur de douche", "duş zamanlayıcısını etkinleştir", "abilita timer doccia", "povoliť časovač sprchovania", "povolit časovač sprchy") MAKE_WORD_TRANSLATION(showertimer_cmd, "enable shower timer", "aktiviere Duschzeitmessung", "activeer douche timer", "aktivera duschtimer", "aktywuj czasomierz prysznica", "aktiver dusjtimer", "activer minuteur de douche", "duş zamanlayıcısını etkinleştir", "abilita timer doccia", "povoliť časovač sprchovania", "povolit časovač sprchy")
MAKE_WORD_TRANSLATION(showeralert_cmd, "enable shower alert", "aktiviere Duschzeitwarnung", "activeer douche alarm", "aktivera duschvarning", "aktywuj alarm prysznica", "aktiver dusjvarsel", "activer alerte de douche", "duş uyarısını etkinleştir", "abilita allarme doccia", "povoliť upozornenie na sprchu", "povolit alarm sprchy") MAKE_WORD_TRANSLATION(showeralert_cmd, "enable shower alert", "aktiviere Duschzeitwarnung", "activeer douche alarm", "aktivera duschvarning", "aktywuj alarm prysznica", "aktiver dusjvarsel", "activer alerte de douche", "duş uyarısını etkinleştir", "abilita allarme doccia", "povoliť upozornenie na sprchu", "povolit alarm sprchy")
MAKE_WORD_TRANSLATION(txpause_cmd, "pause EMS Tx", "EMS Tx pausieren", "pauzeer EMS Tx", "pausa EMS Tx", "wstrzymaj EMS Tx", "pause EMS Tx", "pause EMS Tx", "EMS Tx'i duraklat", "pausa EMS Tx", "pozastaviť EMS Tx", "pauzovat EMS Tx") MAKE_WORD_TRANSLATION(txpause_cmd, "pause EMS Tx", "EMS Tx pausieren", "pauzeer EMS Tx", "pausa EMS Tx", "wstrzymaj EMS Tx", "pause EMS Tx", "pause EMS Tx", "EMS Tx'i duraklat", "pausa EMS Tx", "pozastaviť EMS Tx", "pauzovat EMS Tx")
MAKE_WORD_TRANSLATION(led_cmd, "flash the LED", "LED blinken", "LED knipperen", "LED blinka", "LED błyska", "LED blink", "LED clignote", "LED yanıp söner", "LED lampeggia", "LED bliká", "LED bliká")
// tags // tags
MAKE_WORD_TRANSLATION(tag_hc1, "hc1", "HK1", "hc1", "VK1", "OG1", "hc1", "hc1", "ID1", "hc1", "hc1", "hc1") MAKE_WORD_TRANSLATION(tag_hc1, "hc1", "HK1", "hc1", "VK1", "OG1", "hc1", "hc1", "ID1", "hc1", "hc1", "hc1")

View File

@@ -257,7 +257,7 @@ NetPhase Network::initialPhase() const {
void Network::loop() { void Network::loop() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// if we already have a Wifi or Ethernet connection then re-check every NETWORK_RECONNECTION_DELAY_LONG, otherwise NETWORK_RECONNECTION_DELAY_SHORT // if we already have a Wifi or Ethernet connection then re-check every NETWORK_RECONNECTION_DELAY_LONG, otherwise NETWORK_RECONNECTION_DELAY_SHORT
const unsigned long currentMillis = millis(); const unsigned long currentMillis = uuid::get_uptime_ms();
const uint32_t reconnectDelay = const uint32_t reconnectDelay =
(network_iface_ == NetIface::WIFI || network_iface_ == NetIface::ETHERNET) ? NETWORK_RECONNECTION_DELAY_LONG : NETWORK_RECONNECTION_DELAY_SHORT; (network_iface_ == NetIface::WIFI || network_iface_ == NetIface::ETHERNET) ? NETWORK_RECONNECTION_DELAY_LONG : NETWORK_RECONNECTION_DELAY_SHORT;
if (!lastConnectionAttempt_ || static_cast<uint32_t>(currentMillis - lastConnectionAttempt_) >= reconnectDelay) { if (!lastConnectionAttempt_ || static_cast<uint32_t>(currentMillis - lastConnectionAttempt_) >= reconnectDelay) {

View File

@@ -88,13 +88,6 @@ bool System::test_set_all_active_ = false;
uint32_t System::max_alloc_mem_; uint32_t System::max_alloc_mem_;
uint32_t System::heap_mem_; uint32_t System::heap_mem_;
// LED flash timer
uint8_t System::led_flash_gpio_ = 0;
uint8_t System::led_flash_type_ = 0;
uint32_t System::led_flash_start_time_ = 0;
uint32_t System::led_flash_duration_ = 0;
bool System::led_flash_timer_ = false;
// GPIOs // GPIOs
std::vector<uint8_t, AllocatorPSRAM<uint8_t>> System::valid_system_gpios_; std::vector<uint8_t, AllocatorPSRAM<uint8_t>> System::valid_system_gpios_;
std::vector<System::GpioUsage, AllocatorPSRAM<System::GpioUsage>> System::used_gpios_; std::vector<System::GpioUsage, AllocatorPSRAM<System::GpioUsage>> System::used_gpios_;
@@ -734,10 +727,8 @@ void System::start() {
}); });
commands_init(); // console & api commands commands_init(); // console & api commands
led_init(); // init LED EMSESP::led_.init(); // init LED
button_init(); // button button_init(); // button
last_system_check_ = 0; // force the LED to go from fast flash to pulse
uart_init(); // start UART uart_init(); // start UART
syslog_init(); // start syslog syslog_init(); // start syslog
modbus_init(); // start modbus modbus_init(); // start modbus
@@ -757,7 +748,7 @@ void System::button_OnClick(PButton & b) {
// reconnect to AP by removing the SSID from the network settings // reconnect to AP by removing the SSID from the network settings
// note: in v3.9 this is normal behaviour to fallback to AP if the Wifi or Ethernet connection fails // note: in v3.9 this is normal behaviour to fallback to AP if the Wifi or Ethernet connection fails
void System::button_OnDblClick(PButton & b) { void System::button_OnDblClick(PButton & b) {
LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); LOG_NOTICE("Button pressed - double click - reset network");
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// set AP mode to always so will join AP if wifi ssid fails to connect // set AP mode to always so will join AP if wifi ssid fails to connect
EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) {
@@ -773,55 +764,6 @@ void System::button_OnDblClick(PButton & b) {
#endif #endif
} }
// LED flash every 100ms
void System::led_flash() {
static bool led_flash_state_ = false;
static uint32_t last_toggle_time_ = 0;
uint32_t current_time = uuid::get_uptime();
if (current_time - last_toggle_time_ >= 100) { // every 100ms
led_flash_state_ = !led_flash_state_;
last_toggle_time_ = current_time;
if (led_flash_type_) {
uint8_t intensity = led_flash_state_ ? RGB_LED_BRIGHTNESS : 0;
EMSESP_RGB_WRITE(led_flash_gpio_, intensity, intensity, 0); // RGB LED - Yellow
} else {
digitalWrite(led_flash_gpio_, led_flash_state_ ? LED_ON : !LED_ON); // Standard LED
}
}
// after duration, turn off the LED
if (current_time - led_flash_start_time_ >= led_flash_duration_) {
if (led_flash_type_) {
EMSESP_RGB_WRITE(led_flash_gpio_, 0, 0, 0);
} else {
digitalWrite(led_flash_gpio_, !LED_ON);
}
led_flash_timer_ = false;
command_format(nullptr, 0); // Execute format operation
}
}
// Start the LED flash timer - duration in seconds
void System::start_led_flash(uint8_t duration) {
// Don't start if already running
if (led_flash_timer_) {
return;
}
// Get LED settings
EMSESP::webSettingsService.read([&](WebSettings & settings) {
led_flash_type_ = settings.led_type;
led_flash_gpio_ = settings.led_gpio;
});
// Reset counter and state
led_flash_start_time_ = uuid::get_uptime(); // current time
led_flash_duration_ = duration * 1000; // duration in milliseconds
led_flash_timer_ = true; // it's active
}
// button long press // button long press
void System::button_OnLongPress(PButton & b) { void System::button_OnLongPress(PButton & b) {
LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); LOG_NOTICE("Button pressed - long press - restart EMS-ESP");
@@ -831,7 +773,7 @@ void System::button_OnLongPress(PButton & b) {
// button indefinite press // button indefinite press
void System::button_OnVLongPress(PButton & b) { void System::button_OnVLongPress(PButton & b) {
LOG_NOTICE("Button pressed - very long press - perform factory reset"); LOG_NOTICE("Button pressed - very long press - perform factory reset");
start_led_flash(5); // Start LED flash timer for 5 seconds EMSESP::led_.start_led_fast_flash(5); // Start LED flash timer for 5 seconds
} }
// push button // push button
@@ -850,21 +792,7 @@ void System::button_init() {
#endif #endif
} }
// set the LED to on or off when in normal operating mode // init UART
void System::led_init() {
if (!led_gpio_) { // 0 means disabled
LOG_INFO("LED disabled");
return;
}
if (led_type_) {
EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0);
} else {
pinMode(led_gpio_, OUTPUT);
digitalWrite(led_gpio_, !LED_ON); // start with LED off
}
}
void System::uart_init() { void System::uart_init() {
EMSuart::stop(); EMSuart::stop();
EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked
@@ -879,18 +807,16 @@ bool System::loop() {
system_restart(); system_restart();
} }
// if LED flashing is active, run the LED flash myPButton_.check(); // check button press
if (led_flash_timer_) { system_check(); // System health check
led_flash();
return true; // is active // handle the LED
if (EMSESP::led_.loop(healthcheck_, myPButton_.button_busy())) {
return true; // restart is pending, skip the rest of the loop
} }
led_monitor(); // check status and report back using the LED
myPButton_.check(); // check button press
system_check(); // check system health
// syslog
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// syslog service
if (syslog_enabled_) { if (syslog_enabled_) {
syslog_.loop(); syslog_.loop();
} }
@@ -898,7 +824,7 @@ bool System::loop() {
send_info_mqtt(); send_info_mqtt();
return false; // LED flashing is not active return false;
} }
// send MQTT info topic appended with the version information as JSON, as a retained flag // send MQTT info topic appended with the version information as JSON, as a retained flag
@@ -1065,22 +991,9 @@ void System::system_check() {
static uint8_t last_healthcheck_ = 0; static uint8_t last_healthcheck_ = 0;
if (healthcheck_ != last_healthcheck_) { if (healthcheck_ != last_healthcheck_) {
last_healthcheck_ = healthcheck_; last_healthcheck_ = healthcheck_;
EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected
// see if we're better now
if (healthcheck_ == 0) { if (healthcheck_ == 0) {
// everything is healthy, show LED permanently on or off depending on setting EMSESP::led_.reset_led(true); // LED back to what is was before
// Green on RGB LED, on/off on standard LED
if (led_gpio_) {
led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0)
: digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // Green
}
} else {
// turn off LED so we're ready for the warning flashes
if (led_gpio_) {
led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON);
}
} }
} }
} }
@@ -1096,6 +1009,7 @@ void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(led), System::command_led, FL_(led_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd));
Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd));
#if defined(EMSESP_TEST) #if defined(EMSESP_TEST)
@@ -1109,98 +1023,6 @@ void System::commands_init() {
Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback
} }
// uses LED to show system health
void System::led_monitor() {
// if button is pressed, show LED (yellow on RGB LED, on/off on standard LED)
static bool button_busy_ = false;
if (button_busy_ != myPButton_.button_busy()) {
button_busy_ = myPButton_.button_busy();
if (led_type_) {
EMSESP_RGB_WRITE(led_gpio_, button_busy_ ? RGB_LED_BRIGHTNESS : 0, button_busy_ ? RGB_LED_BRIGHTNESS : 0, 0); // Yellow
} else {
digitalWrite(led_gpio_, button_busy_ ? LED_ON : !LED_ON);
}
}
// we only need to run the LED healthcheck if there are errors
// skip if we're in the led_flash_timer or if a button has been pressed
if (!healthcheck_ || !led_gpio_ || button_busy_ || led_flash_timer_) {
return; // all good
}
static uint32_t led_long_timer_ = 1; // 1 will kick it off immediately
static uint32_t led_short_timer_ = 0;
static uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer
auto current_time = uuid::get_uptime();
// first long pause before we start flashing
if (led_long_timer_ && (uint32_t)(current_time - led_long_timer_) >= HEALTHCHECK_LED_LONG_DUARATION) {
led_short_timer_ = current_time; // start the short timer
led_long_timer_ = 0; // stop long timer
led_flash_step_ = 1; // enable the short flash timer
}
// the flash timer which starts after the long pause
if (led_flash_step_ && (uint32_t)(current_time - led_short_timer_) >= HEALTHCHECK_LED_FLASH_DUARATION) {
led_long_timer_ = 0; // stop the long timer
led_short_timer_ = current_time;
static bool led_on_ = false;
if (++led_flash_step_ == 8) {
// reset the whole sequence
led_long_timer_ = uuid::get_uptime();
led_flash_step_ = 0;
led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); // LED off
} else if (led_flash_step_ % 2) {
// handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED
// 1 flash (blue) is the EMS bus is not connected
// 2 flashes (red, red) if the network (wifi or ethernet) is not connected
// 3 flashes (red, red, blue) is both the bus and the network are not connected
bool no_network = (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK;
bool no_bus = (healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS;
if (led_type_) {
if (led_flash_step_ == 3) {
if (no_network) {
EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
} else if (no_bus) {
EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
}
}
if (led_flash_step_ == 5 && no_network) {
EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
}
if ((led_flash_step_ == 7) && no_network && no_bus) {
EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
}
} else {
if ((led_flash_step_ == 3) && (no_network || no_bus)) {
led_on_ = true;
}
if ((led_flash_step_ == 5) && no_network) {
led_on_ = true;
}
if ((led_flash_step_ == 7) && no_network && no_bus) {
led_on_ = true;
}
if (led_on_) {
digitalWrite(led_gpio_, LED_ON); // LED on
}
}
} else {
// turn the led off after the flash, on even number count
if (led_on_) {
led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON);
led_on_ = false;
}
}
}
}
// Return the quality (Received Signal Strength Indicator) of the WiFi network as a % // Return the quality (Received Signal Strength Indicator) of the WiFi network as a %
// High quality: 90% ~= -55dBm // High quality: 90% ~= -55dBm
// Medium quality: 50% ~= -75dBm // Medium quality: 50% ~= -75dBm
@@ -2667,11 +2489,11 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
node["ethPhyAddr"] = settings.eth_phy_addr; node["ethPhyAddr"] = settings.eth_phy_addr;
node["ethClockMmode"] = settings.eth_clock_mode; node["ethClockMmode"] = settings.eth_clock_mode;
} }
node["rxGPIO"] = EMSESP::system_.rx_gpio_; node["rxGPIO"] = settings.rx_gpio;
node["txGPIO"] = EMSESP::system_.tx_gpio_; node["txGPIO"] = settings.tx_gpio;
node["dallasGPIO"] = EMSESP::system_.dallas_gpio_; node["dallasGPIO"] = settings.dallas_gpio;
node["pbuttonGPIO"] = EMSESP::system_.pbutton_gpio_; node["pbuttonGPIO"] = settings.pbutton_gpio;
node["ledGPIO"] = EMSESP::system_.led_gpio_; node["ledGPIO"] = settings.led_gpio;
node["ledType"] = settings.led_type; node["ledType"] = settings.led_type;
} }
node["hideLed"] = settings.hide_led; node["hideLed"] = settings.hide_led;
@@ -2846,6 +2668,43 @@ bool System::load_board_profile(std::vector<int8_t> & data, const std::string &
return true; return true;
} }
// led command
// https://github.com/emsesp/EMS-ESP32/issues/3063
// /api//system/led command that takes an argument in the form [color]:[pattern]
// color is red, green, blue, yellow, white
// pattern is
// blink1 for 1 time
// blink2 for 2 times
// blink3 for 3 times
// rgb for RGB
// For example: /api/system/led?data=red:blink1
// For older non-RGB models, the colour would default to just being on.
bool System::command_led(const char * value, const int8_t id) {
if (!value) {
return false; // no argument
}
std::string arg = value;
if (arg.find(':') == std::string::npos) {
LOG_ERROR("LED command must be in the form [color]:[pattern]");
return false; // not in the form [color]:[pattern]
}
std::string color = arg.substr(0, arg.find(':'));
std::string pattern = arg.substr(arg.find(':') + 1);
// the color must be red, green, blue, yellow, white
// the style must be blink1, blink2, blink3
if ((color != "red" && color != "green" && color != "blue" && color != "yellow" && color != "white")
|| (pattern != "blink1" && pattern != "blink2" && pattern != "blink3" && pattern != "rgb")) {
LOG_ERROR("Invalid format. Must be [red|green|blue|yellow|white]:[blink1|blink2|blink3|rgb]");
return false;
}
EMSESP::led_.set_led_routine(color, pattern);
return true;
}
// txpause command - temporarily pause the TX, by setting Txmode to 0 (disabled) // txpause command - temporarily pause the TX, by setting Txmode to 0 (disabled)
bool System::command_txpause(const char * value, const int8_t id) { bool System::command_txpause(const char * value, const int8_t id) {
bool arg; bool arg;

View File

@@ -26,6 +26,7 @@
#include "console.h" #include "console.h"
#include "mqtt.h" #include "mqtt.h"
#include "telegram.h" #include "telegram.h"
#include "led.h"
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
#include <esp_wifi.h> #include <esp_wifi.h>
@@ -37,8 +38,6 @@
#include <uuid/log.h> #include <uuid/log.h>
#include <PButton.h> #include <PButton.h>
#define EMSESP_RGB_WRITE rgbLedWrite
#if CONFIG_IDF_TARGET_ESP32 #if CONFIG_IDF_TARGET_ESP32
// there is no official API available on the original ESP32 // there is no official API available on the original ESP32
extern "C" { extern "C" {
@@ -54,8 +53,6 @@ using uuid::console::Shell;
#define EMSESP_CUSTOMSUPPORT_FILE "/config/customSupport.json" #define EMSESP_CUSTOMSUPPORT_FILE "/config/customSupport.json"
#define RGB_LED_BRIGHTNESS 20 // 255 is max brightness
namespace emsesp { namespace emsesp {
enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110, PHY_TYPE_RTL8201 }; enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110, PHY_TYPE_RTL8201 };
@@ -98,6 +95,7 @@ class System {
static bool command_service(const char * cmd, const char * value); static bool command_service(const char * cmd, const char * value);
static bool command_sendmail(const char * value, const int8_t id); static bool command_sendmail(const char * value, const int8_t id);
static bool command_txpause(const char * value, const int8_t id); static bool command_txpause(const char * value, const int8_t id);
static bool command_led(const char * value, const int8_t id);
static bool get_value_info(JsonObject root, const char * cmd); static bool get_value_info(JsonObject root, const char * cmd);
static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val); static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val);
@@ -144,7 +142,6 @@ class System {
static bool uploadFirmwareURL(const char * url = nullptr); static bool uploadFirmwareURL(const char * url = nullptr);
void led_init();
void button_init(); void button_init();
void commands_init(); void commands_init();
void uart_init(); void uart_init();
@@ -354,6 +351,10 @@ class System {
static bool set_partition(const char * partitionname); static bool set_partition(const char * partitionname);
// healthcheck flags - shared with LED for status visualization
static constexpr uint8_t HEALTHCHECK_NO_BUS = (1 << 0); // 1
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
private: private:
static uuid::log::Logger logger_; static uuid::log::Logger logger_;
@@ -374,15 +375,6 @@ class System {
static constexpr uint32_t BUTTON_Debounce = 40; // Debounce period to prevent flickering when pressing or releasing the button (in ms) static constexpr uint32_t BUTTON_Debounce = 40; // Debounce period to prevent flickering when pressing or releasing the button (in ms)
static constexpr uint32_t BUTTON_DblClickDelay = 250; // Max period between clicks for a double click event (in ms) static constexpr uint32_t BUTTON_DblClickDelay = 250; // Max period between clicks for a double click event (in ms)
// LED flash timer
static bool led_flash_timer_;
static uint8_t led_flash_gpio_;
static uint8_t led_flash_type_;
static uint32_t led_flash_start_time_;
static uint32_t led_flash_duration_;
static void start_led_flash(uint8_t duration);
static void led_flash();
// button press delays // button press delays
static constexpr uint32_t BUTTON_LongPressDelay = 3000; // Hold period for a long press event (in ms) - ~3 seconds static constexpr uint32_t BUTTON_LongPressDelay = 3000; // Hold period for a long press event (in ms) - ~3 seconds
static constexpr uint32_t BUTTON_VLongPressDelay = 9500; // Hold period for a very long press event (in ms) - !10 seconds static constexpr uint32_t BUTTON_VLongPressDelay = 9500; // Hold period for a very long press event (in ms) - !10 seconds
@@ -393,17 +385,11 @@ class System {
#else #else
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // do a system check every 5 seconds static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // do a system check every 5 seconds
#endif #endif
static constexpr uint32_t HEALTHCHECK_LED_LONG_DUARATION = 1500; // 1.5 seconds
static constexpr uint32_t HEALTHCHECK_LED_FLASH_DUARATION = 150; // 150ms
static constexpr uint8_t HEALTHCHECK_NO_BUS = (1 << 0); // 1
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
static constexpr uint8_t LED_ON = HIGH; // LED on
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
static uuid::syslog::SyslogService syslog_; static uuid::syslog::SyslogService syslog_;
#endif #endif
void led_monitor();
void system_check(); void system_check();
static std::vector<uint8_t, AllocatorPSRAM<uint8_t>> string_range_to_vector(const std::string & range, const std::string & exclude = ""); static std::vector<uint8_t, AllocatorPSRAM<uint8_t>> string_range_to_vector(const std::string & range, const std::string & exclude = "");

View File

@@ -450,7 +450,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
// THESE ONLY WORK WITH AN ESP32, not in standalone/native mode // THESE ONLY WORK WITH AN ESP32, not in standalone/native mode
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if (command == "ls") { if (command == "ls") {
listDir(LittleFS, "/", 3); EMSESP::system_.listDir("/", 3);
ok = true; ok = true;
} }
@@ -1091,6 +1091,22 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
shell.invoke_command("call boiler circpump/value"); shell.invoke_command("call boiler circpump/value");
} }
if (command == "led") {
shell.printfln("Testing LED...");
JsonDocument doc;
AsyncWebServerRequest request;
request.method(HTTP_POST);
char data1[] = "{\"data\":\"red:blink1\"}";
deserializeJson(doc, data1);
JsonVariant json = doc.as<JsonVariant>();
request.url("/api/system/led");
EMSESP::webAPIService.webAPIService(&request, json);
ok = true;
}
if (command == "shuntingyard") { if (command == "shuntingyard") {
shell.printfln("Testing shunting yard..."); shell.printfln("Testing shunting yard...");

View File

@@ -64,6 +64,7 @@ namespace emsesp {
// #define EMSESP_DEBUG_DEFAULT "hpmode" // #define EMSESP_DEBUG_DEFAULT "hpmode"
// #define EMSESP_DEBUG_DEFAULT "shuntingyard" // #define EMSESP_DEBUG_DEFAULT "shuntingyard"
// #define EMSESP_DEBUG_DEFAULT "src" // #define EMSESP_DEBUG_DEFAULT "src"
#define EMSESP_DEBUG_DEFAULT "led"
#ifndef EMSESP_DEBUG_DEFAULT #ifndef EMSESP_DEBUG_DEFAULT
#define EMSESP_DEBUG_DEFAULT "general" #define EMSESP_DEBUG_DEFAULT "general"

View File

@@ -387,7 +387,7 @@ void WebSettingsService::onUpdate() {
} }
if (WebSettings::has_flags(WebSettings::ChangeFlags::LED)) { if (WebSettings::has_flags(WebSettings::ChangeFlags::LED)) {
EMSESP::system_.led_init(); EMSESP::led_.init();
} }
if (WebSettings::has_flags(WebSettings::ChangeFlags::MQTT)) { if (WebSettings::has_flags(WebSettings::ChangeFlags::MQTT)) {