104 Commits

Author SHA1 Message Date
proddy
1e8013100c rename build-webUI with build_webUI 2026-04-20 15:46:54 +02:00
proddy
62c8f55568 package update (vite fix) 2026-04-20 15:46:39 +02:00
Proddy
c40d828749 Merge pull request #3031 from proddy/core3
build_webUI -> build-webUI
2026-04-19 19:15:28 +02:00
proddy
84ad08887a build_webUI -> build-webUI 2026-04-19 19:13:15 +02:00
Proddy
37107d8500 Merge pull request #3029 from proddy/core3
sync memory optimizations from dev-16 to core3
2026-04-19 15:15:14 +02:00
proddy
dde6a8c5db close dialog after download 2026-04-19 15:10:51 +02:00
proddy
e2750b8572 don't show system backup as its the same page! 2026-04-19 15:10:41 +02:00
proddy
acd23925b5 download text changes 2026-04-19 15:10:20 +02:00
proddy
b0db054e11 fix firmware install date (was using UTC as TZ not initialised) 2026-04-19 14:17:31 +02:00
proddy
51cea8e757 no need to call ntp on begin as its in the loop 2026-04-19 13:15:11 +02:00
proddy
bbb086ea41 add back NTP 2026-04-19 13:14:54 +02:00
proddy
539e6ed080 remove lazy loading 2026-04-19 10:17:10 +02:00
proddy
1d33a26318 fix dns name being set to "tasmota" 2026-04-18 19:42:45 +02:00
proddy
86a20fc97a sync with dev-16 2026-04-18 18:54:33 +02:00
proddy
d6e00c4534 UART_FIFO_LEN is deprecated 2026-04-18 18:50:08 +02:00
Proddy
a813d38108 Merge pull request #3025 from MichaelDvP/core3
Core3
2026-04-17 15:44:51 +02:00
MichaelDvP
685a49c212 Merge branch 'dev' into core3, formatting, add back sendmail settings 2026-04-17 12:28:41 +02:00
Proddy
2c8eb534af Merge pull request #3022 from proddy/core3
fix merge errors
2026-04-16 08:46:06 +02:00
proddy
5210fab4cb switch to C++20 for string find commands 2026-04-16 08:45:07 +02:00
proddy
49787d27f1 add missing code lost in merge 2026-04-16 08:44:55 +02:00
proddy
dfe7b46461 remove unused http 2026-04-16 08:44:44 +02:00
Proddy
f8257de0dd Merge pull request #3009 from MichaelDvP/core3
https client for scheduler/shuntingYard
2026-04-15 21:34:03 +02:00
Proddy
3b3ecc9f1d Merge branch 'core3' into core3 2026-04-15 21:33:27 +02:00
proddy
84105acf5d 3.9.0-dev.0 2026-04-15 20:48:11 +02:00
proddy
def5173692 fix merge issues 2026-04-15 20:37:33 +02:00
proddy
6b31fef1af build on mac osx 2026-04-15 20:37:27 +02:00
Proddy
c9c059ca65 Merge pull request #3020 from proddy/core3
sync with dev
2026-04-15 09:26:32 +02:00
proddy
4d3b31e5a1 sync with dev 2026-04-15 09:25:38 +02:00
MichaelDvP
51d90095aa https client for scheduler/shuntingYard 2026-04-04 11:39:43 +02:00
Proddy
16c0370443 Merge pull request #3005 from proddy/core3
Core3 updates
2026-03-30 23:25:22 +02:00
proddy
67bb38dcf4 updated test data 2026-03-30 23:24:35 +02:00
proddy
049231a36e minor text changes 2026-03-30 23:15:18 +02:00
proddy
349d6b7375 package update 2026-03-30 23:15:07 +02:00
proddy
b72b368d3c ArduinoJson @ 7.4.3 2026-03-30 23:15:00 +02:00
proddy
7f9fd44a02 C14 2026-03-30 21:27:26 +02:00
proddy
a400c5974c comment change 2026-03-30 21:09:59 +02:00
proddy
afca995fe5 remove poll check 2026-03-30 21:06:08 +02:00
proddy
81504fedc5 package update 2026-03-30 21:05:55 +02:00
proddy
3da3345683 fix poll_match_count 2026-03-29 16:30:38 +02:00
Proddy
c6c2889306 Merge branch 'dev' into core3 2026-03-29 15:58:45 +02:00
proddy
b60f0d260a updated tests 2026-03-29 15:55:02 +02:00
proddy
cd750e4777 text change 2026-03-29 15:44:39 +02:00
proddy
4e5d503b35 remove mbedtls, update ArduinoJson 2026-03-29 15:44:27 +02:00
proddy
bd09e17e49 3.8.2-dev.C13 2026-03-29 15:43:44 +02:00
proddy
835eb743bb backup/restore #3002 2026-03-29 15:43:35 +02:00
proddy
69a129d80e move NVS initisalisation higher, add check for poll_id == bus ID 2026-03-29 15:43:03 +02:00
proddy
434bf483fd added comment 2026-03-29 15:41:57 +02:00
proddy
2b8e170b40 text changes 2026-03-29 15:41:46 +02:00
proddy
dc9b95f3e7 updated to prevent warning on WiFI mode (shown in debug mode) 2026-03-29 15:41:23 +02:00
proddy
1616b0da0a package update 2026-03-29 15:41:03 +02:00
proddy
91c457b22b don't show EMS bus error on Dashboard, only devices 2026-03-29 15:40:40 +02:00
proddy
70c60647c7 refresh changed from 3 to 5 seconds 2026-03-29 15:40:19 +02:00
Proddy
c0bea66d27 Merge pull request #2996 from MichaelDvP/core3 2026-03-22 10:12:08 +01:00
MichaelDvP
ed7cc078ed fix testdata 2026-03-22 09:53:21 +01:00
Proddy
60b7d6d795 Merge pull request #2995 from MichaelDvP/core3
Core3 updates, adapt to c6 and c3 chips
2026-03-21 18:02:49 +01:00
MichaelDvP
947f29cca0 Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-03-21 16:47:05 +01:00
MichaelDvP
d2a13ec0da core3 adaptions for c3 and c6, compiles for all chips 2026-03-21 16:23:07 +01:00
proddy
cc39ba409e package update 2026-03-21 15:49:14 +01:00
MichaelDvP
ac9db6256e Merge branch 'dev' into core3 2026-03-21 11:19:05 +01:00
MichaelDvP
096f628d97 Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-03-21 11:02:41 +01:00
proddy
22312812bb system backup 2026-03-20 17:14:58 +01:00
MichaelDvP
3584975acb env: c6 2026-03-19 16:43:18 +01:00
MichaelDvP
30b9ca4e6c Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-03-19 16:41:21 +01:00
MichaelDvP
7c6ff01ebe reset reason, uart adapt for C6 2026-03-19 16:39:27 +01:00
proddy
a54edcaf5b formatting 2026-03-18 20:51:40 +01:00
proddy
e446954844 #2971 2026-03-18 20:45:36 +01:00
proddy
4a2d0d6787 package update 2026-03-18 20:35:55 +01:00
proddy
9725314135 Platform 2026.03.50 Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313 and ESP32Async/ESPAsyncWebServer @ 3.10.3 2026-03-18 20:18:59 +01:00
MichaelDvP
e610f0d57f Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-03-18 14:21:59 +01:00
MichaelDvP
8244af2940 Merge branch 'dev' into core3, fix #2988 2026-03-18 11:47:16 +01:00
proddy
40f371d23b remove check for downloadOnly 2026-03-17 22:01:38 +01:00
proddy
817b791e59 remove flto 2026-03-17 21:47:12 +01:00
proddy
25a7aac360 lint warning fix 2026-03-17 21:47:01 +01:00
proddy
37115a174d show size of firmware not partition 2026-03-17 21:46:51 +01:00
proddy
1397f81fd0 add C in version so we know its Core 2026-03-17 21:46:27 +01:00
proddy
56365cb403 formatting 2026-03-17 21:46:15 +01:00
proddy
dfd245ee7b rename common.h 2026-03-17 21:45:12 +01:00
proddy
9c81e4b34d optimize for vite v8 2026-03-17 21:44:38 +01:00
proddy
67676df131 package update 2026-03-17 21:44:29 +01:00
proddy
a73b129596 update vite v8 2026-03-17 21:44:17 +01:00
Proddy
4600d886b5 Merge pull request #2986 from MichaelDvP/core3
update Core3, change TLS library
2026-03-17 20:27:45 +01:00
MichaelDvP
0fe45a2405 use ESP_SSLClient for mqtt, add sendmail command (using readymail) 2026-03-17 18:53:37 +01:00
MichaelDvP
db87213242 Merge branch 'dev' into core3 2026-03-17 10:19:26 +01:00
MichaelDvP
5c3c010d5a Merge branch 'dev' into core3 2026-03-16 14:02:02 +01:00
MichaelDvP
c804cedd7a update changelog 2026-03-16 14:01:45 +01:00
MichaelDvP
aa30ca99bf update core 2026-03-15 21:41:18 +01:00
MichaelDvP
c0ca9d1069 update pkg 2026-03-15 16:13:50 +01:00
MichaelDvP
5e79e1d57f more messages on network connect 2026-03-15 16:13:21 +01:00
MichaelDvP
8c732f9f1e allow modbus start/stop without reboot 2026-03-15 16:12:50 +01:00
MichaelDvP
5e94c2f636 Merge branch 'dev' into core3 2026-03-15 16:11:36 +01:00
MichaelDvP
64e5d29996 fix typo in HA-climate creation 2026-02-17 09:23:20 +01:00
Proddy
b320d8ded2 Merge pull request #2963 from MichaelDvP/core3
Core3 network fixes
2026-02-16 18:12:33 +01:00
MichaelDvP
0be1b20996 statemachine for network connection replaces onEvent 2026-02-16 10:37:28 +01:00
MichaelDvP
6c55460622 Merge branch 'dev' into core3 2026-02-16 08:34:23 +01:00
MichaelDvP
d627404dc2 skip onEvent for AP, MQTT, NTP 2026-02-16 07:47:13 +01:00
proddy
f317123c26 fix standalone 2026-02-15 15:27:11 +01:00
proddy
e4df1887b0 remove 2026-02-15 14:16:55 +01:00
proddy
34142c3e85 use Tasmota everywhere 2026-02-15 14:16:50 +01:00
proddy
6e7f8bdf02 add -DNO_TLS_SUPPORT 2026-02-15 14:16:27 +01:00
proddy
3dd9fcfb58 update 2026-02-15 14:16:04 +01:00
proddy
35e2954b8b fix lint errors 2026-02-15 14:15:55 +01:00
proddy
59aa63db0f package update 2026-02-15 14:15:43 +01:00
proddy
7a41a190f8 support s3 2026-02-15 14:15:13 +01:00
proddy
6741232450 WIP: ESP-IDF Core 3 migration - mbedtls SSL, module library, board configs, MQTT and network updates 2026-02-15 13:53:13 +01:00
85 changed files with 1592 additions and 1832 deletions

View File

@@ -47,7 +47,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |

View File

@@ -39,7 +39,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |

View File

@@ -47,7 +47,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |

View File

@@ -12,7 +12,9 @@ For more details go to [emsesp.org](https://emsesp.org/).
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
- e-mail notification using ReadyMail Client
- 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
- full system backup and restore
## Fixed
@@ -29,5 +31,8 @@ For more details go to [emsesp.org](https://emsesp.org/).
- support `minflowtemp` and `baseflowtemp` [#2969](https://github.com/emsesp/EMS-ESP32/discussions/2969)
- update version if it is 00.00 in first read [#2981](https://github.com/emsesp/EMS-ESP32/issues/2981)
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
- use tasmota core 2026.03.30
- secure mqtt uses ESP_SSLClient
- fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017)
- move http client from stack to heap
- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021)

View File

@@ -47,18 +47,18 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2)))
#----------------------------------------------------------------------
TARGET := emsesp
BUILD := build
SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
SOURCES := src/core src/devices src/web src/test lib_standalone lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
LIBRARIES :=
CPPCHECK = cppcheck
CHECKFLAGS = -q --force --std=gnu++17
CHECKFLAGS = -q --force --std=gnu++20
#----------------------------------------------------------------------
# Languages Standard
#----------------------------------------------------------------------
C_STANDARD := -std=c17
CXX_STANDARD := -std=gnu++17
C_STANDARD := -std=c20
CXX_STANDARD := -std=gnu++20
#----------------------------------------------------------------------
# Defined Symbols

View File

@@ -5,7 +5,7 @@
},
"core": "esp32",
"extra_flags": [
"-DTASMOTA_SDK",
"-DNO_TLS_SUPPORT",
"-DARDUINO_LOLIN_C3_MINI",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1"

View File

@@ -6,7 +6,7 @@
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DTASMOTA_SDK",
"-DNO_TLS_SUPPORT",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0"
],

View File

@@ -21,7 +21,7 @@
"arduino",
"espidf"
],
"name": "Espressif ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"name": "Tasmota ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "32MB",
"maximum_ram_size": 327680,

View File

@@ -1,7 +1,7 @@
{
"build": {
"core": "esp32",
"extra_flags": "-DTASMOTA_SDK",
"extra_flags": "-DNO_TLS_SUPPORT",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
@@ -19,7 +19,7 @@
"arduino",
"espidf"
],
"name": "Espressif ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
"name": "Tasmota ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,

View File

@@ -19,7 +19,7 @@
"arduino",
"espidf"
],
"name": "Espressif ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
"name": "Tasmota ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,

View File

@@ -1,7 +1,7 @@
{
"build": {
"core": "esp32",
"extra_flags": "-DTASMOTA_SDK",
"extra_flags": "-DNO_TLS_SUPPORT",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",

View File

@@ -2,6 +2,7 @@
"build": {
"core": "esp32",
"extra_flags": [
"-DNO_TLS_SUPPORT",
"-DARDUINO_XIAO_ESP32C6",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1"

View File

@@ -38,6 +38,7 @@
"vite.config.ts",
"lib/esp32-psram/**",
"test/test_api/test_api.h",
"lib_standalone/**"
"lib_standalone/**",
"**/*.js"
]
}

View File

@@ -17,7 +17,7 @@
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"",
"typesafe-i18n": "typesafe-i18n --no-watch",
"build_webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
"build-webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --fix",
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
@@ -30,7 +30,7 @@
"@mui/material": "^9.0.0",
"@preact/compat": "^18.3.2",
"@table-library/react-table-library": "4.1.15",
"alova": "3.5.1",
"alova": "^3.5.1",
"async-validator": "^4.2.5",
"etag": "^1.8.1",
"formidable": "^3.5.4",
@@ -42,9 +42,9 @@
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"react-router": "^7.14.1",
"react-toastify": "^11.0.5",
"react-toastify": "^11.1.0",
"typesafe-i18n": "^5.27.1",
"typescript": "^6.0.2"
"typescript": "^6.0.3"
},
"devDependencies": {
"@babel/core": "^7.29.0",
@@ -57,15 +57,14 @@
"@types/react-dom": "^19.2.3",
"axe-core": "^4.11.3",
"concurrently": "^9.2.1",
"eslint": "^10.2.0",
"eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.3",
"rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.46.1",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.8",
"vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^6.1.1"
"vite": "^8.0.9",
"vite-plugin-imagemin": "^0.6.1"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}

416
interface/pnpm-lock.yaml generated
View File

@@ -30,7 +30,7 @@ importers:
specifier: 4.1.15
version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
alova:
specifier: 3.5.1
specifier: ^3.5.1
version: 3.5.1
async-validator:
specifier: ^4.2.5
@@ -66,24 +66,24 @@ importers:
specifier: ^7.14.1
version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-toastify:
specifier: ^11.0.5
version: 11.0.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
specifier: ^11.1.0
version: 11.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
typesafe-i18n:
specifier: ^5.27.1
version: 5.27.1(typescript@6.0.2)
version: 5.27.1(typescript@6.0.3)
typescript:
specifier: ^6.0.2
version: 6.0.2
specifier: ^6.0.3
version: 6.0.3
devDependencies:
'@babel/core':
specifier: ^7.29.0
version: 7.29.0
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.2.0)
version: 10.0.1(eslint@10.2.1)
'@preact/preset-vite':
specifier: ^2.10.5
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
version: 6.0.2(prettier@3.8.3)
@@ -103,32 +103,29 @@ importers:
specifier: ^9.2.1
version: 9.2.1
eslint:
specifier: ^10.2.0
version: 10.2.0
specifier: ^10.2.1
version: 10.2.1
eslint-config-prettier:
specifier: ^10.1.8
version: 10.1.8(eslint@10.2.0)
version: 10.1.8(eslint@10.2.1)
prettier:
specifier: ^3.8.3
version: 3.8.3
rollup-plugin-visualizer:
specifier: ^7.0.1
version: 7.0.1(rolldown@1.0.0-rc.15)(rollup@4.59.0)
version: 7.0.1(rolldown@1.0.0-rc.16)(rollup@4.59.0)
terser:
specifier: ^5.46.1
version: 5.46.1
typescript-eslint:
specifier: ^8.58.2
version: 8.58.2(eslint@10.2.0)(typescript@6.0.2)
version: 8.58.2(eslint@10.2.1)(typescript@6.0.3)
vite:
specifier: ^8.0.8
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
specifier: ^8.0.9
version: 8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite-plugin-imagemin:
specifier: ^0.6.1
version: 0.6.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
vite-tsconfig-paths:
specifier: ^6.1.1
version: 6.1.1(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
version: 0.6.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
packages:
@@ -501,12 +498,16 @@ packages:
resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
'@humanfs/node@0.16.7':
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
'@humanfs/node@0.16.8':
resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
engines: {node: '>=18.18.0'}
'@humanfs/types@0.15.0':
resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
engines: {node: '>=18.18.0'}
'@humanwhocodes/module-importer@1.0.1':
@@ -649,8 +650,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.124.0':
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
'@oxc-project/types@0.126.0':
resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
@@ -686,103 +687,103 @@ packages:
preact: ^10.4.0 || ^11.0.0-0
vite: '>=2.0.0'
'@rolldown/binding-android-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
'@rolldown/binding-android-arm64@1.0.0-rc.16':
resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
'@rolldown/binding-darwin-arm64@1.0.0-rc.16':
resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
'@rolldown/binding-darwin-x64@1.0.0-rc.16':
resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
'@rolldown/binding-freebsd-x64@1.0.0-rc.16':
resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16':
resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16':
resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.16':
resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16':
resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16':
resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.16':
resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
'@rolldown/binding-linux-x64-musl@1.0.0-rc.16':
resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
'@rolldown/binding-openharmony-arm64@1.0.0-rc.16':
resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
engines: {node: '>=14.0.0'}
'@rolldown/binding-wasm32-wasi@1.0.0-rc.16':
resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16':
resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.16':
resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-rc.15':
resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
'@rolldown/pluginutils@1.0.0-rc.16':
resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==}
'@rollup/pluginutils@4.2.1':
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
@@ -1184,8 +1185,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.19:
resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
baseline-browser-mapping@2.10.20:
resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1519,8 +1520,8 @@ packages:
duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.338:
resolution: {integrity: sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg==}
electron-to-chromium@1.5.340:
resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -1713,8 +1714,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.2.0:
resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==}
eslint@10.2.1:
resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -1982,9 +1983,6 @@ packages:
resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==}
engines: {node: '>=8'}
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -2025,8 +2023,8 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
hasown@2.0.3:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
he@1.2.0:
@@ -2800,8 +2798,8 @@ packages:
react-dom:
optional: true
react-toastify@11.0.5:
resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
react-toastify@11.1.0:
resolution: {integrity: sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==}
peerDependencies:
react: ^18 || ^19
react-dom: ^18 || ^19
@@ -2877,8 +2875,8 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rolldown@1.0.0-rc.15:
resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
rolldown@1.0.0-rc.16:
resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -3172,16 +3170,6 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
hasBin: true
peerDependencies:
typescript: ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -3213,8 +3201,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
typescript@6.0.2:
resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -3270,13 +3258,8 @@ packages:
peerDependencies:
vite: 5.x || 6.x || 7.x || 8.x
vite-tsconfig-paths@6.1.1:
resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==}
peerDependencies:
vite: '*'
vite@8.0.8:
resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
vite@8.0.9:
resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -3714,9 +3697,9 @@ snapshots:
'@esbuild/win32-x64@0.27.4':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)':
'@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)':
dependencies:
eslint: 10.2.0
eslint: 10.2.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -3737,9 +3720,9 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/js@10.0.1(eslint@10.2.0)':
'@eslint/js@10.0.1(eslint@10.2.1)':
optionalDependencies:
eslint: 10.2.0
eslint: 10.2.1
'@eslint/object-schema@3.0.5': {}
@@ -3748,13 +3731,18 @@ snapshots:
'@eslint/core': 1.2.1
levn: 0.4.1
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/core': 0.19.1
'@humanfs/types': 0.15.0
'@humanfs/node@0.16.8':
dependencies:
'@humanfs/core': 0.19.2
'@humanfs/types': 0.15.0
'@humanwhocodes/retry': 0.4.3
'@humanfs/types@0.15.0': {}
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.4.3': {}
@@ -3891,7 +3879,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@oxc-project/types@0.124.0': {}
'@oxc-project/types@0.126.0': {}
'@paralleldrive/cuid2@2.3.1':
dependencies:
@@ -3903,19 +3891,19 @@ snapshots:
dependencies:
preact: 10.29.1
'@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
'@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
dependencies:
'@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)
'@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
'@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
debug: 4.4.3
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite-prerender-plugin: 0.5.13(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite-prerender-plugin: 0.5.13(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
zimmerframe: 1.1.4
transitivePeerDependencies:
- preact
@@ -3930,7 +3918,7 @@ snapshots:
'@prefresh/utils@1.2.1': {}
'@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
'@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
dependencies:
'@babel/core': 7.29.0
'@prefresh/babel-plugin': 0.5.3
@@ -3938,60 +3926,60 @@ snapshots:
'@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1
preact: 10.29.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
'@rolldown/binding-android-arm64@1.0.0-rc.15':
'@rolldown/binding-android-arm64@1.0.0-rc.16':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
'@rolldown/binding-darwin-arm64@1.0.0-rc.16':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
'@rolldown/binding-darwin-x64@1.0.0-rc.16':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
'@rolldown/binding-freebsd-x64@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.16':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
'@rolldown/binding-linux-x64-musl@1.0.0-rc.16':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
'@rolldown/binding-openharmony-arm64@1.0.0-rc.16':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
'@rolldown/binding-wasm32-wasi@1.0.0-rc.16':
dependencies:
'@emnapi/core': 1.9.2
'@emnapi/runtime': 1.9.2
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.16':
optional: true
'@rolldown/pluginutils@1.0.0-rc.15': {}
'@rolldown/pluginutils@1.0.0-rc.16': {}
'@rollup/pluginutils@4.2.1':
dependencies:
@@ -4187,40 +4175,40 @@ snapshots:
dependencies:
'@types/node': 25.6.0
'@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)':
'@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.58.2
eslint: 10.2.0
eslint: 10.2.1
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
'@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
eslint: 10.2.0
typescript: 6.0.2
eslint: 10.2.1
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.58.2(typescript@6.0.2)':
'@typescript-eslint/project-service@8.58.2(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2)
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3)
'@typescript-eslint/types': 8.58.2
debug: 4.4.3
typescript: 6.0.2
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -4229,47 +4217,47 @@ snapshots:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
'@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)':
'@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.3)':
dependencies:
typescript: 6.0.2
typescript: 6.0.3
'@typescript-eslint/type-utils@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
'@typescript-eslint/type-utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
debug: 4.4.3
eslint: 10.2.0
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
eslint: 10.2.1
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.58.2': {}
'@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)':
'@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.3)':
dependencies:
'@typescript-eslint/project-service': 8.58.2(typescript@6.0.2)
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2)
'@typescript-eslint/project-service': 8.58.2(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3)
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.16
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
'@typescript-eslint/utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0)
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
eslint: 10.2.0
typescript: 6.0.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
eslint: 10.2.1
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -4346,7 +4334,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.19: {}
baseline-browser-mapping@2.10.20: {}
bin-build@3.0.0:
dependencies:
@@ -4407,9 +4395,9 @@ snapshots:
browserslist@4.28.2:
dependencies:
baseline-browser-mapping: 2.10.19
baseline-browser-mapping: 2.10.20
caniuse-lite: 1.0.30001788
electron-to-chromium: 1.5.338
electron-to-chromium: 1.5.340
node-releases: 2.0.37
update-browserslist-db: 1.2.3(browserslist@4.28.2)
@@ -4773,7 +4761,7 @@ snapshots:
duplexer3@0.1.5: {}
electron-to-chromium@1.5.338: {}
electron-to-chromium@1.5.340: {}
emoji-regex@10.6.0: {}
@@ -4919,9 +4907,9 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.2.0):
eslint-config-prettier@10.1.8(eslint@10.2.1):
dependencies:
eslint: 10.2.0
eslint: 10.2.1
eslint-scope@9.1.2:
dependencies:
@@ -4934,15 +4922,15 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@10.2.0:
eslint@10.2.1:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0)
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.5.5
'@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.7.1
'@humanfs/node': 0.16.7
'@humanfs/node': 0.16.8
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
@@ -5190,7 +5178,7 @@ snapshots:
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
hasown: 2.0.3
math-intrinsics: 1.1.0
get-proto@1.0.1:
@@ -5256,8 +5244,6 @@ snapshots:
merge2: 1.4.1
slash: 3.0.0
globrex@0.1.2: {}
gopd@1.2.0: {}
got@7.1.0:
@@ -5325,7 +5311,7 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
hasown@2.0.3:
dependencies:
function-bind: 1.1.2
@@ -5435,7 +5421,7 @@ snapshots:
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
hasown: 2.0.3
is-cwebp-readable@3.0.0:
dependencies:
@@ -5999,7 +5985,7 @@ snapshots:
optionalDependencies:
react-dom: 19.2.5(react@19.2.5)
react-toastify@11.0.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
react-toastify@11.1.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
clsx: 2.1.1
react: 19.2.5
@@ -6081,35 +6067,35 @@ snapshots:
dependencies:
glob: 7.2.3
rolldown@1.0.0-rc.15:
rolldown@1.0.0-rc.16:
dependencies:
'@oxc-project/types': 0.124.0
'@rolldown/pluginutils': 1.0.0-rc.15
'@oxc-project/types': 0.126.0
'@rolldown/pluginutils': 1.0.0-rc.16
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.15
'@rolldown/binding-darwin-arm64': 1.0.0-rc.15
'@rolldown/binding-darwin-x64': 1.0.0-rc.15
'@rolldown/binding-freebsd-x64': 1.0.0-rc.15
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
'@rolldown/binding-android-arm64': 1.0.0-rc.16
'@rolldown/binding-darwin-arm64': 1.0.0-rc.16
'@rolldown/binding-darwin-x64': 1.0.0-rc.16
'@rolldown/binding-freebsd-x64': 1.0.0-rc.16
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.16
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.16
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.16
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16
rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.15)(rollup@4.59.0):
rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.16)(rollup@4.59.0):
dependencies:
open: 11.0.0
picomatch: 4.0.4
source-map: 0.7.6
yargs: 18.0.0
optionalDependencies:
rolldown: 1.0.0-rc.15
rolldown: 1.0.0-rc.16
rollup: 4.59.0
rollup@4.59.0:
@@ -6387,13 +6373,9 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
ts-api-utils@2.5.0(typescript@6.0.2):
ts-api-utils@2.5.0(typescript@6.0.3):
dependencies:
typescript: 6.0.2
tsconfck@3.1.6(typescript@6.0.2):
optionalDependencies:
typescript: 6.0.2
typescript: 6.0.3
tslib@2.8.1: {}
@@ -6413,22 +6395,22 @@ snapshots:
es-errors: 1.3.0
is-typed-array: 1.1.15
typesafe-i18n@5.27.1(typescript@6.0.2):
typesafe-i18n@5.27.1(typescript@6.0.3):
dependencies:
typescript: 6.0.2
typescript: 6.0.3
typescript-eslint@8.58.2(eslint@10.2.0)(typescript@6.0.2):
typescript-eslint@8.58.2(eslint@10.2.1)(typescript@6.0.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
eslint: 10.2.0
typescript: 6.0.2
'@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)
'@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
'@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3)
eslint: 10.2.1
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
typescript@6.0.2: {}
typescript@6.0.3: {}
unbzip2-stream@1.4.3:
dependencies:
@@ -6468,7 +6450,7 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite-plugin-imagemin@0.6.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
vite-plugin-imagemin@0.6.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
dependencies:
'@types/imagemin': 7.0.1
'@types/imagemin-gifsicle': 7.0.4
@@ -6493,11 +6475,11 @@ snapshots:
imagemin-webp: 6.1.0
jpegtran-bin: 6.0.1
pathe: 0.2.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
vite-prerender-plugin@0.5.13(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
vite-prerender-plugin@0.5.13(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
dependencies:
kolorist: 1.8.0
magic-string: 0.30.21
@@ -6505,24 +6487,14 @@ snapshots:
simple-code-frame: 1.3.0
source-map: 0.7.6
stack-trace: 1.0.0-pre2
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite-tsconfig-paths@6.1.1(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@6.0.2)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
- typescript
vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1):
vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.10
rolldown: 1.0.0-rc.15
rolldown: 1.0.0-rc.16
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.6.0

View File

@@ -24,20 +24,26 @@ let bundleStats = {
other: { count: 0, uncompressed: 0, compressed: 0 }
};
const generateWWWClass =
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
// Bundle Statistics:
// AsyncWebHandler that performs the lookup.
const generateWWWClass = () => `// Bundle Statistics:
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
// - Generated on: ${new Date().toISOString()}
class WWWData {
${INDENT}public:
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')}
${INDENT.repeat(2)}}
struct WWWAsset {
${INDENT}const char * uri;
${INDENT}const char * contentType;
${INDENT}const uint8_t * content;
${INDENT}size_t len;
${INDENT}const char * etag; // already includes enclosing double quotes
};
static const WWWAsset WWW_ASSETS[] = {
${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "\\"${f.rawHash}\\""},`).join('\n')}
};
static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]);
`;
const getFilesSync = (dir, files = []) => {
@@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => {
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
// const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
const hash = etag(zipBuffer); // use smaller md5 instead of sha256
const rawHash = hash.replace(/^"|"$/g, '');
zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) {
@@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => {
mimeType,
variable,
size,
hash
hash,
rawHash
});
totalSize += size;

View File

@@ -1,19 +1,13 @@
import { type FC, Suspense, lazy, memo, useContext, useEffect, useRef } from 'react';
import { type FC, memo, useContext, useEffect, useRef } from 'react';
import { Navigate, Route, Routes } from 'react-router';
import { toast } from 'react-toastify';
import {
LoadingSpinner,
RequireAuthenticated,
RequireUnauthenticated
} from 'components';
import AuthenticatedRouting from 'AuthenticatedRouting';
import SignIn from 'SignIn';
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
import { Authentication, AuthenticationContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
// Lazy load route components for better code splitting
const SignIn = lazy(() => import('SignIn'));
const AuthenticatedRouting = lazy(() => import('AuthenticatedRouting'));
interface SecurityRedirectProps {
readonly message: string;
readonly signOut?: boolean;
@@ -45,7 +39,6 @@ const AppRouting: FC = memo(() => {
return (
<Authentication>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route
path="/unauthorized"
@@ -72,7 +65,6 @@ const AppRouting: FC = memo(() => {
}
/>
</Routes>
</Suspense>
</Authentication>
);
});

View File

@@ -1,44 +1,39 @@
import { Suspense, lazy, memo, useContext } from 'react';
import { memo, useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router';
import { Layout, LoadingSpinner } from 'components';
import CustomEntities from 'app/main/CustomEntities';
import Customizations from 'app/main/Customizations';
import Dashboard from 'app/main/Dashboard';
import Devices from 'app/main/Devices';
import Help from 'app/main/Help';
import Modules from 'app/main/Modules';
import Scheduler from 'app/main/Scheduler';
import Sensors from 'app/main/Sensors';
import UserProfile from 'app/main/UserProfile';
import APSettings from 'app/settings/APSettings';
import ApplicationSettings from 'app/settings/ApplicationSettings';
import DownloadUpload from 'app/settings/DownloadUpload';
import MqttSettings from 'app/settings/MqttSettings';
import NTPSettings from 'app/settings/NTPSettings';
import Settings from 'app/settings/Settings';
import Network from 'app/settings/network/Network';
import Security from 'app/settings/security/Security';
import APStatus from 'app/status/APStatus';
import Activity from 'app/status/Activity';
import HardwareStatus from 'app/status/HardwareStatus';
import MqttStatus from 'app/status/MqttStatus';
import NTPStatus from 'app/status/NTPStatus';
import NetworkStatus from 'app/status/NetworkStatus';
import Status from 'app/status/Status';
import SystemLog from 'app/status/SystemLog';
import Version from 'app/status/Version';
import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
// Lazy load all route components for better code splitting
const Dashboard = lazy(() => import('app/main/Dashboard'));
const Devices = lazy(() => import('app/main/Devices'));
const Sensors = lazy(() => import('app/main/Sensors'));
const Help = lazy(() => import('app/main/Help'));
const Customizations = lazy(() => import('app/main/Customizations'));
const Scheduler = lazy(() => import('app/main/Scheduler'));
const CustomEntities = lazy(() => import('app/main/CustomEntities'));
const Modules = lazy(() => import('app/main/Modules'));
const UserProfile = lazy(() => import('app/main/UserProfile'));
const Status = lazy(() => import('app/status/Status'));
const HardwareStatus = lazy(() => import('app/status/HardwareStatus'));
const Activity = lazy(() => import('app/status/Activity'));
const SystemLog = lazy(() => import('app/status/SystemLog'));
const MqttStatus = lazy(() => import('app/status/MqttStatus'));
const NTPStatus = lazy(() => import('app/status/NTPStatus'));
const APStatus = lazy(() => import('app/status/APStatus'));
const NetworkStatus = lazy(() => import('app/status/NetworkStatus'));
const Version = lazy(() => import('app/status/Version'));
const Settings = lazy(() => import('app/settings/Settings'));
const ApplicationSettings = lazy(() => import('app/settings/ApplicationSettings'));
const MqttSettings = lazy(() => import('app/settings/MqttSettings'));
const NTPSettings = lazy(() => import('app/settings/NTPSettings'));
const APSettings = lazy(() => import('app/settings/APSettings'));
const DownloadUpload = lazy(() => import('app/settings/DownloadUpload'));
const Network = lazy(() => import('app/settings/network/Network'));
const Security = lazy(() => import('app/settings/security/Security'));
const AuthenticatedRouting = memo(() => {
const { me } = useContext(AuthenticatedContext);
return (
<Layout>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} />
@@ -59,10 +54,7 @@ const AuthenticatedRouting = memo(() => {
{me.admin && (
<>
<Route path="/settings" element={<Settings />} />
<Route
path="/settings/application"
element={<ApplicationSettings />}
/>
<Route path="/settings/application" element={<ApplicationSettings />} />
<Route path="/settings/mqtt" element={<MqttSettings />} />
<Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/ap" element={<APSettings />} />
@@ -80,7 +72,6 @@ const AuthenticatedRouting = memo(() => {
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
</Layout>
);
});

View File

@@ -43,6 +43,16 @@ export interface Settings {
modbus_port: number;
modbus_max_clients: number;
modbus_timeout: number;
email_enabled: boolean;
email_ssl?: boolean;
email_starttls?: boolean;
email_server: string;
email_port: number;
email_login: string;
email_pass: string;
email_sender: string;
email_recp: string;
email_subject: string;
developer_mode: boolean;
}

View File

@@ -28,6 +28,7 @@ import {
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField,
ValidatedTextField,
useLayoutTitle
} from 'components';
@@ -351,6 +352,156 @@ const ApplicationSettings = () => {
</Grid>
</Grid>
)}
<Typography color="secondary">eMail</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.email_enabled}
onChange={updateFormValue}
name="email_enabled"
disabled={!hardwareData.psram}
/>
}
label={
<Typography color={!hardwareData.psram ? 'grey' : 'default'}>
Enable eMail notification
{!hardwareData.psram && (
<Typography variant="caption">
&nbsp; &#40;{LL.IS_REQUIRED('PSRAM')}&#41;
</Typography>
)}
</Typography>
}
/>
{data.email_enabled && (
<>
<Grid
container
spacing={2}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_server"
label="SMTP Server"
variant="outlined"
value={data.email_server}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
sx={{ width: '12ch' }}
name="email_port"
variant="outlined"
label="Port"
value={numberValue(data.email_port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid size={4} mt={!data.email_ssl && !data.email_starttls ? 0 : 3}>
{!data.email_starttls && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_ssl}
onChange={updateFormValue}
name="email_ssl"
disabled={
data.email_starttls || data.email_ssl === undefined
}
/>
}
label="SSL/TLS"
/>
)}
{!data.email_ssl && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_starttls}
onChange={updateFormValue}
name="email_starttls"
disabled={
data.email_ssl || data.email_starttls === undefined
}
/>
}
label="STARTTLS"
/>
)}
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_login"
label="Login"
variant="outlined"
value={data.email_login}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedPasswordField
fieldErrors={fieldErrors || {}}
name="email_pass"
label="Password"
variant="outlined"
value={data.email_pass}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_sender"
label="From"
variant="outlined"
value={data.email_sender}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_recp"
label="To"
variant="outlined"
value={data.email_recp}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_subject"
label="Subject"
variant="outlined"
value={data.email_subject}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
</>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SENSORS()}
</Typography>

View File

@@ -112,6 +112,7 @@ const DownloadUpload = () => {
const handleDownload = useCallback(
(type: string) => () => {
void sendExportData(type);
setConfirmBackup(false);
},
[sendExportData]
);

View File

@@ -315,22 +315,13 @@ const InstallDialog = memo(
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
)}
</Typography>
{upgradeImportantMessageType === 1 && LL.UPGRADE_IMPORTANT_MESSAGES_1()}
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
{upgradeImportantMessageType === 1 && (
<>
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
<Typography sx={{ mt: 2 }}>
<Link to="/settings/downloadUpload" style={{ color: 'lightblue' }}>
{LL.DOWNLOAD_SYSTEM_BACKUP()}
</Link>
</Typography>
</>
)}
<Typography sx={{ mt: 2 }}>
<Link
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
target="_blank"
rel="noreferrer"
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}

View File

@@ -213,19 +213,8 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
<DialogContent dividers>
{upgradeImportantMessageType === 2 &&
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
{upgradeImportantMessageType === 1 && (
<>
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
<Typography sx={{ mt: 2 }}>
<Link
to="/settings/downloadUpload"
style={{ color: 'lightblue' }}
>
{LL.DOWNLOAD_SYSTEM_BACKUP()}
</Link>
</Typography>
</>
)}{' '}
{upgradeImportantMessageType === 1 &&
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
<Typography sx={{ mt: 2 }}>
<Link
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"

View File

@@ -362,7 +362,7 @@ const cz: Translation = {
STORED_VERSIONS: 'Uložené verze',
ONLINE_HELP: 'online nápověda',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že jste vytvořili zálohu své konfigurace a nastavení před pokračováním a nahrajte ji po instalaci nové verze.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?'

View File

@@ -362,7 +362,7 @@ const de: Translation = {
STORED_VERSIONS: 'Gespeicherte Versionen',
ONLINE_HELP: 'Online-Hilfe',
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie eine Sicherung Ihrer Konfiguration und Einstellungen vor dem Fortfahren erstellt haben und diese nach der Installation der neuen Version hochladen.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und -einstellungen erstellen. Alle Passwörter werden im Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'

View File

@@ -362,7 +362,7 @@ const en: Translation = {
STORED_VERSIONS: 'Stored Versions',
ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you have made a backup of your configuration and settings before continuing, and upload this after the new version is installed.',
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.',
UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?'

View File

@@ -362,7 +362,7 @@ const fr: Translation = {
STORED_VERSIONS: 'Versions stockées',
ONLINE_HELP: 'aide en ligne',
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous d\'avoir créé une sauvegarde de vos configurations et paramètres avant de continuer et de la charger après l\'installation de la nouvelle version.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?'

View File

@@ -362,7 +362,7 @@ const it: Translation = {
STORED_VERSIONS: 'Versioni memorizzate',
ONLINE_HELP: 'aiuto online',
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di aver creato un backup delle tue configurazioni e impostazioni prima di continuare e di caricarlo dopo l\'installazione della nuova versione.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?'

View File

@@ -362,7 +362,7 @@ const nl: Translation = {
STORED_VERSIONS: 'Opgeslagen versies',
ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u een back-up van uw configuratie en instellingen hebt gemaakt voordat u doorgaat en upload deze na de installatie van de nieuwe versie.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?'

View File

@@ -362,7 +362,7 @@ const no: Translation = {
STORED_VERSIONS: 'Lagret versjoner',
ONLINE_HELP: 'online hjelp',
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du har laget en sikkerhetskopi av din konfigurasjon og innstillinger før du fortsetter, og last denne opp etter at den nye versjonen er installert.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?'

View File

@@ -362,7 +362,7 @@ const pl: BaseTranslation = {
STORED_VERSIONS: 'Zapisane wersje',
ONLINE_HELP: 'pomoc online',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że masz utworzoną kopię swoich ustawień i konfiguracji przed kontynuowaniem i przesuń po zainstalowaniu nowej wersji.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?'

View File

@@ -362,7 +362,7 @@ const sk: Translation = {
STORED_VERSIONS: 'Uložené verzie',
ONLINE_HELP: 'online pomoc',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že ste vytvorili zálohu svojich konfigurácií a nastavení pred pokračovaním a nahrajte ju po instalácii novej verzie.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?'

View File

@@ -362,7 +362,7 @@ const sv: Translation = {
STORED_VERSIONS: 'Lagrad versioner',
ONLINE_HELP: 'online hjälp',
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du har gjort en säkerhetskopia av din konfiguration och inställningar innan du fortsätter och ladda upp denna efter att den nya versionen är installerad.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?'

View File

@@ -15,7 +15,7 @@ const REPEAT_CHAR = '=';
const REPEAT_COUNT = 50;
const DEFAULT_OUT_DIR = 'dist';
const ES_TARGET = 'es2020';
const CHUNK_SIZE_WARNING_LIMIT = 512;
const CHUNK_SIZE_WARNING_LIMIT = 1024;
const ASSETS_INLINE_LIMIT = 4096;
// Common resolve aliases
@@ -130,41 +130,10 @@ const createBasePlugins = (
return plugins;
};
// Manual chunk splitting strategy
const createManualChunks = (detailed = false) => {
return (id: string): string | undefined => {
if (id.includes('node_modules')) {
if (id.includes('preact')) return '@preact';
if (detailed) {
if (id.includes('react-router')) return '@react-router';
if (id.includes('@mui/material')) return '@mui-material';
if (id.includes('@mui/icons-material')) return '@mui-icons';
if (id.includes('alova')) return '@alova';
if (id.includes('typesafe-i18n')) return '@i18n';
if (id.includes('react-toastify')) return '@toastify';
if (id.includes('@table-library')) return '@table-library';
if (id.includes('uuid')) return '@uuid';
if (id.includes('axios') || id.includes('fetch')) return '@http';
if (id.includes('lodash') || id.includes('ramda')) return '@utils';
}
return 'vendor';
}
if (detailed) {
// Group circularly dependent modules together to avoid circular chunk warnings
// components, app, and utils are tightly coupled, so combine them
if (
id.includes('components/') ||
id.includes('app/') ||
id.includes('utils/')
) {
return 'app';
}
// Keep api separate as it's typically more independent
if (id.includes('api/')) return 'api';
}
const manualChunks = (id: string): string | undefined => {
if (id.includes('node_modules')) return 'vendor';
return undefined;
};
};
// Common build base configuration
const createBaseBuildConfig = () => ({
@@ -290,7 +259,7 @@ export default defineConfig(
moduleSideEffects: false
},
output: {
manualChunks: createManualChunks(false)
manualChunks
}
}
}
@@ -330,7 +299,7 @@ export default defineConfig(
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks: createManualChunks(true),
manualChunks,
sourcemap: false
}
}

View File

@@ -49,6 +49,10 @@ the LICENSE file.
#define EMC_CLIENTID_LENGTH 23 + 1
#endif
#ifdef EMSESP_MQTT_STACKSIZE
#define EMC_TASK_STACK_SIZE EMSESP_MQTT_STACKSIZE
#endif
#ifndef EMC_TASK_STACK_SIZE
#define EMC_TASK_STACK_SIZE 5120
#endif
@@ -73,7 +77,3 @@ the LICENSE file.
#define EMC_SIZE_POOL_ELEMENTS 128
#endif
#endif
#ifndef TASMOTA_SDK
#define EMC_CLIENT_SECURE
#endif

View File

@@ -62,14 +62,19 @@ MqttClient::MqttClient(espMqttClientTypes::UseInternalTask useInternalTask, uint
_xSemaphore = xSemaphoreCreateMutex();
EMC_SEMAPHORE_GIVE(); // release before first use
if (_useInternalTask == espMqttClientTypes::UseInternalTask::YES) {
if (core > 1) {
xTaskCreate((TaskFunction_t)_loop, "mqttclient", EMC_TASK_STACK_SIZE, this, priority, &_taskHandle);
} else {
xTaskCreatePinnedToCore((TaskFunction_t)_loop, "mqttclient", EMC_TASK_STACK_SIZE, this, priority, &_taskHandle, core);
}
}
#else
(void) useInternalTask;
(void) priority;
(void) core;
#endif
_clientId = _generatedClientId;
_core = core;
}
MqttClient::~MqttClient() {

View File

@@ -69,6 +69,16 @@ class MqttClient {
const char* getClientId() const;
size_t queueSize(); // No const because of mutex
void loop();
uint32_t stack() {
#ifndef EMSESP_STANDALONE
return uxTaskGetStackHighWaterMark(_taskHandle);
#else
return 0;
#endif
}
uint8_t core() {
return _core;
}
protected:
explicit MqttClient(espMqttClientTypes::UseInternalTask useInternalTask, uint8_t priority = 1, uint8_t core = 1);
@@ -98,6 +108,7 @@ class MqttClient {
uint8_t _willQos;
bool _willRetain;
uint32_t _timeout;
uint8_t _core;
// state is protected to allow state changes by the transport system, defined in child classes
// eg. to allow AsyncTCP

View File

@@ -8,7 +8,7 @@ the LICENSE file.
#include "ClientPosix.h"
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
namespace espMqttClientInternals {

View File

@@ -8,7 +8,7 @@ the LICENSE file.
#pragma once
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
#include <stdio.h>
#include <stdlib.h>

View File

@@ -6,28 +6,31 @@ For a copy, see <https://opensource.org/licenses/MIT> or
the LICENSE file.
*/
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#ifndef NO_TLS_SUPPORT
#include "ClientSecureSync.h"
#include <lwip/sockets.h> // socket options
#include <lwip/sockets.h>
#include "../Config.h"
namespace espMqttClientInternals {
ClientSecureSync::ClientSecureSync()
: client() {
// empty
client.setClient(&basic_client, true);
client.setBufferSizes(EMC_RX_BUFFER_SIZE, EMC_TX_BUFFER_SIZE);
client.setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
}
ClientSecureSync::~ClientSecureSync() {
stop();
}
bool ClientSecureSync::connect(IPAddress ip, uint16_t port) {
bool ret = client.connect(ip, port); // implicit conversion of return code int --> bool
if (ret) {
#if defined(ARDUINO_ARCH_ESP8266)
client.setNoDelay(true);
#elif defined(ARDUINO_ARCH_ESP32)
// Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure
int val = true;
client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
#endif
basic_client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
return ret;
}
@@ -35,13 +38,9 @@ bool ClientSecureSync::connect(IPAddress ip, uint16_t port) {
bool ClientSecureSync::connect(const char * host, uint16_t port) {
bool ret = client.connect(host, port); // implicit conversion of return code int --> bool
if (ret) {
#if defined(ARDUINO_ARCH_ESP8266)
client.setNoDelay(true);
#elif defined(ARDUINO_ARCH_ESP32)
// Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure
int val = true;
client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
#endif
basic_client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
return ret;
}

View File

@@ -8,15 +8,11 @@ the LICENSE file.
#pragma once
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#ifndef NO_TLS_SUPPORT
// Added for EMS-ESP
#include "../Config.h"
#if defined(EMC_CLIENT_SECURE)
#include <WiFiClientSecure.h> // includes IPAddress
#else
// #include "esp_tls.h"
#include <WiFiClient.h>
#endif
#include <ESP_SSLClient.h>
#include "Transport.h"
namespace espMqttClientInternals {
@@ -24,6 +20,7 @@ namespace espMqttClientInternals {
class ClientSecureSync : public Transport {
public:
ClientSecureSync();
~ClientSecureSync();
bool connect(IPAddress ip, uint16_t port) override;
bool connect(const char * host, uint16_t port) override;
size_t write(const uint8_t * buf, size_t size) override;
@@ -31,12 +28,9 @@ class ClientSecureSync : public Transport {
void stop() override;
bool connected() override;
bool disconnected() override;
// added for EMS-ESP
#if defined(EMC_CLIENT_SECURE)
WiFiClientSecure client;
#else
WiFiClient client;
#endif
WiFiClient basic_client;
ESP_SSLClient client;
};
} // namespace espMqttClientInternals

View File

@@ -8,50 +8,6 @@ the LICENSE file.
#include "espMqttClient.h"
#if defined(ARDUINO_ARCH_ESP8266)
espMqttClient::espMqttClient()
: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO)
, _client() {
_transport = &_client;
}
espMqttClientSecure::espMqttClientSecure()
: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO)
, _client() {
_transport = &_client;
}
espMqttClientSecure & espMqttClientSecure::setInsecure() {
_client.client.setInsecure();
return *this;
}
espMqttClientSecure & espMqttClientSecure::setFingerprint(const uint8_t fingerprint[20]) {
_client.client.setFingerprint(fingerprint);
return *this;
}
espMqttClientSecure & espMqttClientSecure::setTrustAnchors(const X509List * ta) {
_client.client.setTrustAnchors(ta);
return *this;
}
espMqttClientSecure & espMqttClientSecure::setClientRSACert(const X509List * cert, const PrivateKey * sk) {
_client.client.setClientRSACert(cert, sk);
return *this;
}
espMqttClientSecure & espMqttClientSecure::setClientECCert(const X509List * cert, const PrivateKey * sk, unsigned allowed_usages, unsigned cert_issuer_key_type) {
_client.client.setClientECCert(cert, sk, allowed_usages, cert_issuer_key_type);
return *this;
}
espMqttClientSecure & espMqttClientSecure::setCertStore(CertStoreBase * certStore) {
_client.client.setCertStore(certStore);
return *this;
}
#endif
#if defined(ARDUINO_ARCH_ESP32)
espMqttClient::espMqttClient(espMqttClientTypes::UseInternalTask useInternalTask)
: MqttClientSetup(useInternalTask)
@@ -78,51 +34,45 @@ espMqttClientSecure::espMqttClientSecure(uint8_t priority, uint8_t core)
}
espMqttClientSecure & espMqttClientSecure::setInsecure() {
#if defined(EMC_CLIENT_SECURE)
#ifndef NO_TLS_SUPPORT
_client.client.setInsecure();
#endif
return *this;
}
espMqttClientSecure & espMqttClientSecure::setCACert(const char * rootCA) {
#if defined(EMC_CLIENT_SECURE)
#ifndef NO_TLS_SUPPORT
_client.client.setCACert(rootCA);
#endif
return *this;
}
espMqttClientSecure & espMqttClientSecure::setCertificate(const char * clientCa) {
#if defined(EMC_CLIENT_SECURE)
#ifndef NO_TLS_SUPPORT
_client.client.setCertificate(clientCa);
#endif
return *this;
}
espMqttClientSecure & espMqttClientSecure::setPrivateKey(const char * privateKey) {
#if defined(EMC_CLIENT_SECURE)
#ifndef NO_TLS_SUPPORT
_client.client.setPrivateKey(privateKey);
#endif
return *this;
}
espMqttClientSecure & espMqttClientSecure::setPreSharedKey(const char * pskIdent, const char * psKey) {
#if defined(EMC_CLIENT_SECURE)
_client.client.setPreSharedKey(pskIdent, psKey);
#ifndef NO_TLS_SUPPORT
#endif
return *this;
}
#endif
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
espMqttClient::espMqttClient()
: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO)
, _client() {
_transport = &_client;
}
#elif defined(_WIN32) || defined(__APPLE__)
// Windows
espMqttClient::espMqttClient()
: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO) {
}
#endif

View File

@@ -14,7 +14,7 @@ the LICENSE file.
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#include "Transport/ClientSync.h"
#include "Transport/ClientSecureSync.h"
#elif defined(__linux__)
#elif defined(__linux__) || defined(__APPLE__)
#include "Transport/ClientPosix.h"
#endif
@@ -65,10 +65,16 @@ class espMqttClientSecure : public MqttClientSetup<espMqttClientSecure> {
espMqttClientSecure & setPreSharedKey(const char * pskIdent, const char * psKey);
protected:
#ifndef NO_TLS_SUPPORT
espMqttClientInternals::ClientSecureSync _client;
#else
espMqttClientInternals::ClientSync _client;
#endif
};
#elif defined(__linux__)
#endif
#if defined(__linux__) || defined(__APPLE__)
class espMqttClient : public MqttClientSetup<espMqttClient> {
public:
espMqttClient();
@@ -76,10 +82,4 @@ class espMqttClient : public MqttClientSetup<espMqttClient> {
protected:
espMqttClientInternals::ClientPosix _client;
};
#elif defined(_WIN32) || defined(__APPLE__)
class espMqttClient : public MqttClientSetup<espMqttClient> {
public:
espMqttClient();
};
#endif

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Marko Živanović
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.

View File

@@ -1,7 +0,0 @@
# About
This project is MIT-licensed, C++14 implementation of [semantic versioning](http://semver.org) parser and comparator with support for modifying parsed version strings. Semantic versioning 2.0.0 specification is supported out-of-the-box and the code should be flexible-enough to support future revisions or other similar versioning schemes.
Copyright (c) 2015 Marko Zivanovic
Based on https://github.com/zmarko/semver

View File

@@ -1,121 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
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.
*/
#include <algorithm>
#include <functional>
#include <map>
#include "semver200.h"
using namespace std;
namespace version {
namespace {
// Compare normal version identifiers.
int compare_normal(const Version_data & l, const Version_data & r) {
if (l.major > r.major)
return 1;
if (l.major < r.major)
return -1;
if (l.minor > r.minor)
return 1;
if (l.minor < r.minor)
return -1;
if (l.patch > r.patch)
return 1;
if (l.patch < r.patch)
return -1;
return 0;
}
// Compare alphanumeric prerelease identifiers.
inline int cmp_alnum_prerel_ids(const string & l, const string & r) {
auto cmp = l.compare(r);
if (cmp == 0) {
return cmp;
} else {
return cmp > 0 ? 1 : -1;
}
}
// Compare numeric prerelease identifiers.
inline int cmp_num_prerel_ids(const string & l, const string & r) {
long long li = stoll(l);
long long ri = stoll(r);
if (li == ri)
return 0;
return li > ri ? 1 : -1;
}
using Prerel_type_pair = pair<Id_type, Id_type>;
using Prerel_id_comparator = function<int(const string &, const string &)>;
const map<Prerel_type_pair, Prerel_id_comparator> comparators = {{{Id_type::alnum, Id_type::alnum}, cmp_alnum_prerel_ids},
{{Id_type::alnum, Id_type::num}, [](const string &, const string &) { return 1; }},
{{Id_type::num, Id_type::alnum}, [](const string &, const string &) { return -1; }},
{{Id_type::num, Id_type::num}, cmp_num_prerel_ids}};
// Compare prerelease identifiers based on their types.
inline int compare_prerel_identifiers(const Prerelease_identifier & l, const Prerelease_identifier & r) {
auto cmp = comparators.at({l.second, r.second});
return cmp(l.first, r.first);
}
inline int cmp_rel_prerel(const Prerelease_identifiers & l, const Prerelease_identifiers & r) {
if (l.empty() && !r.empty())
return 1;
if (r.empty() && !l.empty())
return -1;
return 0;
}
} // namespace
int Semver200_comparator::compare(const Version_data & l, const Version_data & r) const {
// Compare normal version components.
int cmp = compare_normal(l, r);
if (cmp != 0)
return cmp;
// Compare if one version is release and the other prerelease - release is always higher.
cmp = cmp_rel_prerel(l.prerelease_ids, r.prerelease_ids);
if (cmp != 0)
return cmp;
// Compare prerelease by looking at each identifier: numeric ones are compared as numbers,
// alphanum as ASCII strings.
auto shorter = min(l.prerelease_ids.size(), r.prerelease_ids.size());
for (size_t i = 0; i < shorter; i++) {
cmp = compare_prerel_identifiers(l.prerelease_ids[i], r.prerelease_ids[i]);
if (cmp != 0)
return cmp;
}
// Prerelease identifiers are the same, to the length of the shorter version string;
// if they are the same length, then versions are equal, otherwise, longer one wins.
if (l.prerelease_ids.size() == r.prerelease_ids.size())
return 0;
return l.prerelease_ids.size() > r.prerelease_ids.size() ? 1 : -1;
}
} // namespace version

View File

@@ -1,187 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
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.
*/
#include <functional>
#include <map>
#include "semver200.h"
using namespace std;
namespace version {
namespace {
enum class Parser_state { major, minor, patch, prerelease, build };
using Validator = function<void(const string &, const char)>;
using State_transition_hook = function<void(string &)>;
/// State transition is described by a character that triggers it, a state to transition to and
/// optional hook to be invoked on transition.
using Transition = tuple<const char, Parser_state, State_transition_hook>;
using Transitions = vector<Transition>;
using State = tuple<Transitions, string &, Validator>;
using State_machine = std::map<Parser_state, State>;
// Ranges of characters allowed in prerelease and build identifiers.
const vector<pair<char, char>> allowed_prerel_id_chars = {{'0', '9'}, {'A', 'Z'}, {'a', 'z'}, {'-', '-'}};
inline Transition mkx(const char c, Parser_state p, State_transition_hook pth) {
return make_tuple(c, p, pth);
}
inline void Parse_error(const std::string & s) {
// EMSESP::logger().err("parse error: %s", s.c_str());
}
/// Advance parser state machine by a single step.
/**
Perform single step of parser state machine: if character matches one from transition tables -
trigger transition to next state; otherwise, validate if current token is in legal state
(throw Parse_error if not) and then add character to current token; State transition includes
preparing various vars for next state and invoking state transition hook (if specified) which is
where whole tokens are validated.
*/
inline void process_char(const char c, Parser_state & cstate, Parser_state & pstate, const Transitions & transitions, string & target, Validator validate) {
for (const auto & transition : transitions) {
if (c == get<0>(transition)) {
if (get<2>(transition))
get<2>(transition)(target);
pstate = cstate;
cstate = get<1>(transition);
return;
}
}
validate(target, c);
target.push_back(c);
}
/// Validate normal (major, minor, patch) version components.
inline void normal_version_validator(const string & tgt, const char c) {
if (c < '0' || c > '9')
Parse_error("invalid character encountered: " + string(1, c));
if (tgt.compare(0, 1, "0") == 0)
Parse_error("leading 0 not allowed");
}
/// Validate that prerelease and build version identifiers are comprised of allowed chars only.
inline void prerelease_version_validator(const string &, const char c) {
bool res = false;
for (const auto & r : allowed_prerel_id_chars) {
res |= (c >= r.first && c <= r.second);
}
if (!res)
Parse_error("invalid character encountered: " + string(1, c));
}
inline bool is_identifier_numeric(const string & id) {
return id.find_first_not_of("0123456789") == string::npos;
}
inline bool check_for_leading_0(const string & str) {
return str.length() > 1 && str[0] == '0';
}
/// Validate every individual prerelease identifier, determine it's type and add it to collection.
void prerelease_hook_impl(string & id, Prerelease_identifiers & prerelease) {
if (id.empty())
Parse_error("version identifier cannot be empty");
Id_type t = Id_type::alnum;
if (is_identifier_numeric(id)) {
t = Id_type::num;
if (check_for_leading_0(id)) {
Parse_error("numeric identifiers cannot have leading 0");
}
}
prerelease.push_back(Prerelease_identifier(id, t));
id.clear();
}
/// Validate every individual build identifier and add it to collection.
void build_hook_impl(string & id, Parser_state & pstate, Build_identifiers & build, std::string & prerelease_id, Prerelease_identifiers & prerelease) {
// process last token left from parsing prerelease data
if (pstate == Parser_state::prerelease)
prerelease_hook_impl(prerelease_id, prerelease);
if (id.empty())
Parse_error("version identifier cannot be empty");
build.push_back(id);
id.clear();
}
} // namespace
/// Parse semver 2.0.0-compatible string to Version_data structure.
/**
Version text parser is implemented as a state machine. In each step one successive character from version
string is consumed and is either added to current token or triggers state transition. Hooks can be
injected into state transitions for validation/customization purposes.
*/
Version_data Semver200_parser::parse(const string & s) const {
string major;
string minor;
string patch;
string prerelease_id;
string build_id;
Prerelease_identifiers prerelease;
Build_identifiers build;
Parser_state cstate{Parser_state::major};
Parser_state pstate;
auto prerelease_hook = [&](string & id) { prerelease_hook_impl(id, prerelease); };
auto build_hook = [&](string & id) { build_hook_impl(id, pstate, build, prerelease_id, prerelease); };
// State transition tables
auto major_trans = {mkx('.', Parser_state::minor, {})};
auto minor_trans = {mkx('.', Parser_state::patch, {})};
auto patch_trans = {mkx('-', Parser_state::prerelease, {}), mkx('+', Parser_state::build, {})};
auto prerelease_trans = {// When identifier separator (.) is found, stay in the same state but invoke hook
// in order to process each individual identifier separately.
mkx('.', Parser_state::prerelease, prerelease_hook),
mkx('+', Parser_state::build, {})};
auto build_trans = {// Same stay-in-the-same-state-but-invoke-hook trick from above.
mkx('.', Parser_state::build, build_hook)};
State_machine state_machine = {{Parser_state::major, State{major_trans, major, normal_version_validator}},
{Parser_state::minor, State{minor_trans, minor, normal_version_validator}},
{Parser_state::patch, State{patch_trans, patch, normal_version_validator}},
{Parser_state::prerelease, State{prerelease_trans, prerelease_id, prerelease_version_validator}},
{Parser_state::build, State{build_trans, build_id, prerelease_version_validator}}};
// Main loop.
for (const auto & c : s) {
auto state = state_machine.at(cstate);
process_char(c, cstate, pstate, get<0>(state), get<1>(state), get<2>(state));
}
// Trigger appropriate hooks in order to process last token, because no state transition was
// triggered for it.
if (cstate == Parser_state::prerelease) {
prerelease_hook(prerelease_id);
} else if (cstate == Parser_state::build) {
build_hook(build_id);
}
return Version_data{stoi(major), stoi(minor), stoi(patch), prerelease, build};
}
} // namespace version

View File

@@ -1,53 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
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.
*/
#pragma once
#include "version.h"
namespace version {
/// Parse string into Version_data structure according to semantic versioning 2.0.0 rules.
struct Semver200_parser {
Version_data parse(const std::string &) const;
};
/// Compare Version_data to another using semantic versioning 2.0.0 rules.
struct Semver200_comparator {
int compare(const Version_data &, const Version_data &) const;
};
/// Concrete version class that binds all semver 2.0.0 functionality together.
class Semver200_version : public Basic_version<Semver200_parser, Semver200_comparator> {
public:
Semver200_version()
: Basic_version{Semver200_parser(), Semver200_comparator()} {
}
Semver200_version(const std::string & v)
: Basic_version{v, Semver200_parser(), Semver200_comparator()} {
}
};
} // namespace version

View File

@@ -1,149 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
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.
*/
#pragma once
#include <string>
#include <vector>
namespace version {
/// Type of prerelease identifier: alphanumeric or numeric.
/**
Type of identifier affects comparison: alphanumeric identifiers are compared as ASCII strings, while
numeric identifiers are compared as numbers.
*/
enum class Id_type {
alnum, ///< Identifier is alphanumerical
num ///< Identifier is numeric
};
/// Container for prerelease identifier value and it's type.
/**
Prerelease version string consist of an optional series of dot-separated identifiers.
These identifiers can be either numerical or alphanumerical.
This structure describes one such identifier.
*/
using Prerelease_identifier = std::pair<std::string, Id_type>;
/// Container for all prerelease identifiers for a given version string.
using Prerelease_identifiers = std::vector<Prerelease_identifier>;
/// Build identifier is arbitrary string with no special meaning with regards to version precedence.
using Build_identifier = std::string;
/// Container for all build identifiers of a given version string.
using Build_identifiers = std::vector<Build_identifier>;
/// Description of version broken into parts, as per semantic versioning specification.
struct Version_data {
Version_data(const int M, const int m, const int p, const Prerelease_identifiers & pr, const Build_identifiers & b)
: major{M}
, minor{m}
, patch{p}
, prerelease_ids{pr}
, build_ids{b} {
}
int major; ///< Major version, change only on incompatible API modifications.
int minor; ///< Minor version, change on backwards-compatible API modifications.
int patch; ///< Patch version, change only on bugfixes.
/// Optional series of prerelease identifiers.
Prerelease_identifiers prerelease_ids;
/// Optional series of build identifiers.
Build_identifiers build_ids;
};
// Forward declaration required for operators' template declarations.
template <typename Parser, typename Comparator>
class Basic_version;
/// Test if left-hand version operand is of lower precedence than the right-hand version.
template <typename Parser, typename Comparator>
bool operator<(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Test if left-hand version operand if of equal precedence as the right-hand version.
template <typename Parser, typename Comparator>
bool operator==(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Test if left-hand version and right-hand version are of different precedence.
template <typename Parser, typename Comparator>
bool operator!=(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Test if left-hand version operand is of higher precedence than the right-hand version.
template <typename Parser, typename Comparator>
bool operator>(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Test if left-hand version operand is of higher or equal precedence as the right-hand version.
template <typename Parser, typename Comparator>
bool operator>=(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Test if left-hand version operand is of lower or equal precedence as the right-hand version.
template <typename Parser, typename Comparator>
bool operator<=(const Basic_version<Parser, Comparator> &, const Basic_version<Parser, Comparator> &);
/// Base class for various version parsing, precedence ordering and data manipulation schemes.
/**
Basic_version class describes general version object without prescribing parsing,
validation, comparison and modification rules. These rules are implemented by supplied Parser, Comparator
and Modifier objects.
*/
template <typename Parser, typename Comparator>
class Basic_version {
public:
/// Construct Basic_version object using Parser object to parse default ("0.0.0") version string, Comparator for comparison and Modifier for modification.
Basic_version(Parser, Comparator);
/// Construct Basic_version object using Parser to parse supplied version string, Comparator for comparison and Modifier for modification.
Basic_version(const std::string &, Parser, Comparator);
/// Construct Basic_version object using supplied Version_data, Parser, Comparator and Modifier objects.
Basic_version(const Version_data &, Parser, Comparator);
/// Construct Basic_version by copying data from another one.
Basic_version(const Basic_version &);
/// Copy version data from another Basic_version to this one.
Basic_version & operator=(const Basic_version &);
int major() const; ///< Get major version.
int minor() const; ///< Get minor version.
int patch() const; ///< Get patch version.
const std::string prerelease() const; ///< Get prerelease version string.
const std::string build() const; ///< Get build version string.
friend bool operator< <>(const Basic_version &, const Basic_version &);
friend bool operator==<>(const Basic_version &, const Basic_version &);
private:
Parser parser_;
Comparator comparator_;
Version_data ver_;
};
} // namespace version
#include "version.inl"

View File

@@ -1,132 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
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.
*/
#pragma once
#include "version.h"
namespace version {
namespace {
/// Utility function to splice all vector elements to output stream, using designated separator
/// between elements and function object for getting values from vector elements.
template <typename T, typename F>
std::string & splice(std::string & ss, const std::vector<T> & vec, const std::string & sep, F read) {
if (!vec.empty()) {
for (auto it = vec.cbegin(); it < vec.cend() - 1; ++it) {
ss += read(*it) + sep;
}
ss += read(*vec.crbegin());
}
return ss;
}
} // namespace
template <typename Parser, typename Comparator>
Basic_version<Parser, Comparator>::Basic_version(Parser p, Comparator c)
: parser_(p)
, comparator_(c)
, ver_(parser_.parse("0.0.0")) {
}
template <typename Parser, typename Comparator>
Basic_version<Parser, Comparator>::Basic_version(const std::string & v, Parser p, Comparator c)
: parser_(p)
, comparator_(c)
, ver_(parser_.parse(v)) {
}
template <typename Parser, typename Comparator>
Basic_version<Parser, Comparator>::Basic_version(const Version_data & v, Parser p, Comparator c)
: parser_(p)
, comparator_(c)
, ver_(v) {
}
template <typename Parser, typename Comparator>
Basic_version<Parser, Comparator>::Basic_version(const Basic_version<Parser, Comparator> &) = default;
template <typename Parser, typename Comparator>
Basic_version<Parser, Comparator> & Basic_version<Parser, Comparator>::operator=(const Basic_version<Parser, Comparator> &) = default;
template <typename Parser, typename Comparator>
int Basic_version<Parser, Comparator>::major() const {
return ver_.major;
}
template <typename Parser, typename Comparator>
int Basic_version<Parser, Comparator>::minor() const {
return ver_.minor;
}
template <typename Parser, typename Comparator>
int Basic_version<Parser, Comparator>::patch() const {
return ver_.patch;
}
template <typename Parser, typename Comparator>
const std::string Basic_version<Parser, Comparator>::prerelease() const {
std::string ss;
return splice(ss, ver_.prerelease_ids, ".", [](const Prerelease_identifier & id) { return id.first; });
}
template <typename Parser, typename Comparator>
const std::string Basic_version<Parser, Comparator>::build() const {
std::string ss;
return splice(ss, ver_.build_ids, ".", [](const std::string & id) { return id; });
}
template <typename Parser, typename Comparator>
bool operator<(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return l.comparator_.compare(l.ver_, r.ver_) == -1;
}
template <typename Parser, typename Comparator>
bool operator==(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return l.comparator_.compare(l.ver_, r.ver_) == 0;
}
template <typename Parser, typename Comparator>
inline bool operator!=(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return !(l == r);
}
template <typename Parser, typename Comparator>
inline bool operator>(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return r < l;
}
template <typename Parser, typename Comparator>
inline bool operator>=(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return !(l < r);
}
template <typename Parser, typename Comparator>
inline bool operator<=(const Basic_version<Parser, Comparator> & l, const Basic_version<Parser, Comparator> & r) {
return !(l > r);
}
} // namespace version

View File

@@ -20,7 +20,7 @@ my_build_flags =
[platformio]
default_envs = s_16M_P ; BBQKees E32V2
; default_envs = build_webUI ; build the web interface only
; default_envs = build-webUI ; build the web interface only
; default_envs = s3_16M_P ; BBQKees S3
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram

View File

@@ -21,8 +21,8 @@ extra_configs =
pio_local.ini
[common]
core_build_flags = -std=c++17 -std=gnu++17 -O3 -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers
core_unbuild_flags = -std=gnu++11
core_build_flags = -std=gnu++20 -O3 -flto=auto -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers
core_unbuild_flags = -std=gnu++11 -std=gnu++14 -std=gnu++17 -fno-lto
my_build_flags =
@@ -53,13 +53,21 @@ build_flags =
unbuild_flags =
${common.core_unbuild_flags}
; 4MB Flash variants
[espressif32_base_4M]
framework = arduino
board_build.partitions = partitions/esp32_partition_4M.csv
board_upload.flash_size = 4MB
board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 16MB Flash variants
[espressif32_base_16M]
framework = arduino
board_build.partitions = partitions/esp32_partition_16M.csv
board_upload.flash_size = 16MB
board_build.app_partition_name = app0
platform = espressif32@6.13.0 ; Arduino Core 2.0.17 / IDF 4.4.7
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 32MB Flash variants
[espressif32_base_32M]
@@ -67,29 +75,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_32M.csv
board_upload.flash_size = 32MB
board_build.app_partition_name = app0
platform = espressif32@6.13.0 ; Arduino Core 2.0.17 / IDF 4.4.7
; use Tasmota's library for 4MB Flash variants.
; Removes libs (like mbedtsl, so no WiFi_secure.h) to increase available heap
[espressif32_base_T_4M]
framework = arduino
board_build.partitions = partitions/esp32_partition_4M.csv
board_upload.flash_size = 4MB
board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ; Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
; Tasmota Arduino Core 3.1.3.250302 based on IDF 5.3.2.250228
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2025.03.30/platform-espressif32.zip
; use Tasmota's library for 16MB Flash variants.
; Removes libs (like mbedtsl, so no WiFi_secure.h) to increase available heap
[espressif32_base_T_16M]
framework = arduino
board_build.partitions = partitions/esp32_partition_16M.csv
board_upload.flash_size = 16MB
board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ; Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8
; Tasmota Arduino Core 3.1.3.250302 based on IDF 5.3.2.250228
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2025.03.30/platform-espressif32.zip
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
[env]
build_flags =
@@ -106,10 +92,12 @@ lib_deps =
bblanchon/ArduinoJson @ 7.4.3
ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESPAsyncWebServer @ 3.10.3
https://github.com/mobizt/ReadyMail.git @ 0.3.8
https://github.com/mobizt/ESP_SSLClient.git @ 3.1.3
; https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
; builds the web interface only, not the firmware
[env:build_webUI]
[env:build-webUI]
platform = native
targets = build
extra_scripts = pre:scripts/build_interface.py
@@ -118,18 +106,17 @@ build_src_filter = -<*>
;
; Builds for different board types
; We use Tasmota for boards without PSRAM as this framework has mbedtls removed to save memory.
; If you're building for a single target environment, we recommend creating a pio_local.ini (see example file)
;
[env:s_4M]
; 4MB ESP32 - no SSL, no PSRAM - like a BBQKees older S32 and E32 models - uses Tasmota
extends = espressif32_base_T_4M
; 4MB ESP32 - no SSL, no PSRAM - like a BBQKees older S32 and E32 models
extends = espressif32_base_4M
board = s_4M
[env:s_16M]
; 16MB ESP32 - no PSRAM - like a BBQKees later S32 V2 models - uses Tasmota
extends = espressif32_base_T_16M
; 16MB ESP32 - no PSRAM - like a BBQKees later S32 V2 models
extends = espressif32_base_16M
board = s_16M
[env:s_16M_P]
@@ -148,24 +135,30 @@ extends = espressif32_base_32M
board = s3_32M_P
[env:s2_4M_P]
; based on lolin_s2_mini 4MB with 2MB PSRAM - uses Tasmota
extends = espressif32_base_T_4M
; based on lolin_s2_mini 4MB with 2MB PSRAM
extends = espressif32_base_4M
board = s2_4M_P
[env:c3_mini_4M]
; based on lolin_c3_mini 4MB, no PSRAM - uses Tasmota
extends = espressif32_base_T_4M
; based on lolin_c3_mini 4MB, no PSRAM
extends = espressif32_base_4M
board = c3_mini_4M
; lolin C3 mini v1 needs special wifi initialization
; https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
[env:c3_miniv1_4M]
extends = espressif32_base_T_4M
extends = espressif32_base_4M
board = c3_mini_4M
build_flags =
${common.build_flags}
-DBOARD_C3_MINI_V1
; XIAO ESP32C - 512KB SRAM & 4MB Flash - https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/
[env:c6_xiao_4M]
extends = espressif32_base_4M
board_build.app_partition_name = app0
board = seeed_xiao_esp32c6
; foundation for building and testing natively, standalone without an ESP32
; use the `standalone` environment instead of `native` for testing
[env:native]
@@ -175,7 +168,8 @@ build_flags =
build_src_flags =
-DEMSESP_STANDALONE -DEMSESP_TEST
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-std=gnu++17 -Og -ggdb
-DNO_TLS_SUPPORT
-std=gnu++20 -Og -ggdb
-Wall -Wextra
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
-Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare
@@ -214,8 +208,9 @@ build_src_flags =
-DEMSESP_STANDALONE -DEMSESP_TEST
-DEMSESP_UNITY
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-DNO_TLS_SUPPORT
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
-std=gnu++17 -Og -ggdb
-std=gnu++20 -Og -ggdb
-Wall -Wextra
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
-Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare

View File

@@ -74,7 +74,7 @@ def buildWeb():
# Run pnpm commands in the interface directory
commands = [
f"{pnpm_exe} install",
f"{pnpm_exe} build_webUI"
f"{pnpm_exe} build-webUI"
]
for command in commands:

View File

@@ -20,7 +20,7 @@ pnpm format
cd ..
cd interface
pnpm build_webUI
pnpm build-webUI
cd ..
npx cspell "**"

View File

@@ -10,7 +10,6 @@ APSettingsService::APSettingsService(AsyncWebServer * server, FS * fs, SecurityM
, _reconfigureAp(false)
, _connected(0) {
addUpdateHandler([this] { reconfigureAP(); }, false);
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
}
void APSettingsService::begin() {
@@ -19,39 +18,27 @@ void APSettingsService::begin() {
// reconfigureAP();
}
// wait 10 sec on STA disconnect before starting AP
void APSettingsService::WiFiEvent(WiFiEvent_t event) {
const uint8_t was_connected = _connected;
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
_connected &= ~1U;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
_connected &= ~2U;
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
_connected |= 1U;
break;
case ARDUINO_EVENT_ETH_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP6:
_connected |= 2U;
break;
default:
return;
}
// wait 10 sec before starting AP
if (was_connected && !_connected) {
_lastManaged = uuid::get_uptime();
}
}
void APSettingsService::reconfigureAP() {
_lastManaged = uuid::get_uptime() - MANAGE_NETWORK_DELAY;
_reconfigureAp = true;
}
void APSettingsService::loop() {
const uint8_t was_connected = _connected;
if (WiFi.isConnected()) {
_connected |= 1U;
} else {
_connected &= ~1U;
}
if (ETH.connected()) {
_connected |= 2U;
} else {
_connected &= ~2U;
}
// wait 10 sec before starting AP
if (was_connected && !_connected) {
_lastManaged = uuid::get_uptime();
}
const unsigned long currentMillis = uuid::get_uptime();
if ((currentMillis - _lastManaged) >= MANAGE_NETWORK_DELAY) {
_lastManaged = currentMillis;
@@ -76,11 +63,7 @@ void APSettingsService::manageAP() {
}
void APSettingsService::startAP() {
#if ESP_IDF_VERSION_MAJOR < 5
WiFi.softAPenableIpV6(); // force IPV6, same as for WiFi - fixes https://github.com/emsesp/EMS-ESP32/issues/1922
#else
WiFi.softAPenableIPv6(); // force IPV6, same as for WiFi - fixes https://github.com/emsesp/EMS-ESP32/issues/1922
#endif
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_AP), WIFI_BW_HT20);
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str(), _state.channel, _state.ssidHidden, _state.maxClients);

View File

@@ -104,7 +104,6 @@ class APSettingsService : public StatefulService<APSettings> {
void startAP();
void stopAP();
void handleDNS();
void WiFiEvent(WiFiEvent_t event);
};
#endif

View File

@@ -70,12 +70,16 @@ void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) {
*/
String ArduinoJsonJWT::sign(String & payload) {
std::array<unsigned char, 32> hmacResult{};
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
reinterpret_cast<const unsigned char *>(_secret.c_str()),
_secret.length(),
reinterpret_cast<const unsigned char *>(payload.c_str()),
payload.length(),
hmacResult.data());
{
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
mbedtls_md_hmac_starts(&ctx, reinterpret_cast<const unsigned char *>(_secret.c_str()), _secret.length());
mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char *>(payload.c_str()), payload.length());
mbedtls_md_hmac_finish(&ctx, hmacResult.data());
mbedtls_md_free(&ctx);
}
return encode(reinterpret_cast<const char *>(hmacResult.data()), hmacResult.size());
}

View File

@@ -2,10 +2,73 @@
#include "WWWData.h" // include auto-generated static web resources
#include <string.h>
static constexpr const char CACHE_CONTROL[] = "public,max-age=60";
// Single static-content handler serving all assets embedded in WWWData.h
class StaticContentHandler : public AsyncWebHandler {
public:
bool canHandle(AsyncWebServerRequest * request) const override {
const auto method = request->method();
return method == HTTP_GET || method == HTTP_HEAD || method == HTTP_OPTIONS;
}
void handleRequest(AsyncWebServerRequest * request) override {
// OPTIONS is handled generically - the server-level CORS headers are attached via DefaultHeaders in ESP32React::begin().
if (request->method() == HTTP_OPTIONS) {
request->send(200);
return;
}
const char * url = request->url().c_str();
const WWWAsset * found = lookup(url);
const WWWAsset * asset = found ? found : index_asset();
if (asset == nullptr) {
request->send(404);
return;
}
// If the client already has this exact ETag, respond 304 Not Modified without sending the body.
const String & inm = request->header(asyncsrv::T_INM);
if (inm.length() != 0 && strcmp(inm.c_str(), asset->etag) == 0) {
request->send(304);
return;
}
AsyncWebServerResponse * response = request->beginResponse(200, asset->contentType, asset->content, asset->len);
response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false);
response->addHeader(asyncsrv::T_ETag, asset->etag, false);
response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false);
request->send(response);
}
private:
// Exact-match lookup in the asset table
static const WWWAsset * lookup(const char * url) {
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) {
if (strcmp(WWW_ASSETS[i].uri, url) == 0) {
return &WWW_ASSETS[i];
}
}
return nullptr;
}
// Returns the /index.html asset, used as the SPA fallback for any GET
// that didn't match an embedded asset (React Router handles routing on the client side).
static const WWWAsset * index_asset() {
static const WWWAsset * cached = nullptr;
if (cached == nullptr) {
cached = lookup("/index.html");
}
return cached;
}
};
ESP32React::ESP32React(AsyncWebServer * server, FS * fs)
: _securitySettingsService(server, fs)
: _server(server)
, _securitySettingsService(server, fs)
, _networkSettingsService(server, fs, &_securitySettingsService)
, _wifiScanner(server, &_securitySettingsService)
, _networkStatus(server, &_securitySettingsService)
@@ -17,50 +80,6 @@ ESP32React::ESP32React(AsyncWebServer * server, FS * fs)
, _mqttSettingsService(server, fs, &_securitySettingsService)
, _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService)
, _authenticationService(server, &_securitySettingsService) {
//
// Serve static web resources
//
ArRequestHandlerFunction indexHtmlHandler = nullptr;
WWWData::registerRoutes([server, &indexHtmlHandler](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) {
String etag = "\"" + hash + "\""; // RFC9110: ETag must be enclosed in double quotes
ArRequestHandlerFunction requestHandler = [contentType, content, len, etag](AsyncWebServerRequest * request) {
if (request->header(asyncsrv::T_INM) == etag) {
request->send(304);
return;
}
AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len);
response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false);
response->addHeader(asyncsrv::T_ETag, etag, false);
response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false);
request->send(response);
};
server->on(uri, HTTP_GET, requestHandler);
// Capture index.html handler to set onNotFound once after all routes are registered
if (strcmp(uri, "/index.html") == 0) {
indexHtmlHandler = requestHandler;
}
});
// Set onNotFound handler once after all routes are registered
// Serving non matching get requests with "/index.html"
// OPTIONS get a straight up 200 response
if (indexHtmlHandler != nullptr) {
server->onNotFound([indexHtmlHandler](AsyncWebServerRequest * request) {
if (request->method() == HTTP_GET) {
indexHtmlHandler(request);
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404); // not found
}
});
}
}
void ESP32React::begin() {
@@ -78,10 +97,12 @@ void ESP32React::begin() {
_ntpSettingsService.begin();
_mqttSettingsService.begin();
_securitySettingsService.begin();
_server->addHandler(new StaticContentHandler());
}
void ESP32React::loop() {
_networkSettingsService.loop();
_apSettingsService.loop();
_mqttSettingsService.loop();
_ntpSettingsService.loop();
}

View File

@@ -68,6 +68,7 @@ class ESP32React {
}
private:
AsyncWebServer * _server;
SecuritySettingsService _securitySettingsService;
NetworkSettingsService _networkSettingsService;
WiFiScanner _wifiScanner;

View File

@@ -9,7 +9,6 @@ MqttSettingsService::MqttSettingsService(AsyncWebServer * server, FS * fs, Secur
, _disconnectedAt(0)
, _disconnectReason(espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED)
, _mqttClient(nullptr) {
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { onConfigUpdated(); }, false);
}
@@ -29,6 +28,7 @@ MqttSettingsService::~MqttSettingsService() {
void MqttSettingsService::begin() {
_fsPersistence.readFromFS();
startClient();
_reconfigureMqtt = true;
}
void MqttSettingsService::startClient() {
@@ -41,7 +41,7 @@ void MqttSettingsService::startClient() {
delete _mqttClient;
_mqttClient = nullptr;
}
#ifndef TASMOTA_SDK
#ifndef NO_TLS_SUPPORT
if (_state.enableTLS) {
isSecure = true;
if (emsesp::EMSESP::system_.PSram() == 0) {
@@ -79,6 +79,10 @@ void MqttSettingsService::startClient() {
}
void MqttSettingsService::loop() {
if (_state.enabled && _mqttClient && _mqttClient->connected() && !emsesp::EMSESP::system_.network_connected()) {
// emsesp::EMSESP::logger().info("Network connection dropped, stopping MQTT client");
_mqttClient->disconnect(true);
}
if (_reconfigureMqtt || (_disconnectedAt && static_cast<uint32_t>(uuid::get_uptime() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) {
// reconfigure MQTT client
_disconnectedAt = configureMqtt() ? 0 : uuid::get_uptime();
@@ -142,28 +146,6 @@ void MqttSettingsService::onConfigUpdated() {
emsesp::EMSESP::mqtt_.start(); // reload EMS-ESP MQTT settings
}
void MqttSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP6:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
if (_state.enabled && !_mqttClient->connected()) {
onConfigUpdated();
}
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
case ARDUINO_EVENT_ETH_DISCONNECTED:
if (_state.enabled) {
_mqttClient->disconnect(true);
}
break;
default:
break;
}
}
bool MqttSettingsService::configureMqtt() {
// disconnect if already connected
if (_mqttClient->connected()) {
@@ -182,7 +164,7 @@ bool MqttSettingsService::configureMqtt() {
}
_reconfigureMqtt = false;
#ifndef TASMOTA_SDK
#ifndef NO_TLS_SUPPORT
if (_state.enableTLS) {
if (_state.rootCA == "insecure") {
emsesp::EMSESP::logger().debug("Start insecure MQTT");
@@ -219,7 +201,7 @@ bool MqttSettingsService::configureMqtt() {
}
void MqttSettings::read(MqttSettings & settings, JsonObject root) {
#ifndef TASMOTA_SDK
#ifndef NO_TLS_SUPPORT
root["enableTLS"] = settings.enableTLS;
root["rootCA"] = settings.rootCA;
#endif
@@ -258,7 +240,7 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
MqttSettings newSettings;
bool changed = false;
#ifndef TASMOTA_SDK
#ifndef NO_TLS_SUPPORT
newSettings.enableTLS = root["enableTLS"];
newSettings.rootCA = root["rootCA"] | "";
#else
@@ -389,7 +371,7 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
}
#ifndef TASMOTA_SDK
#ifndef NO_TLS_SUPPORT
// strip down to certificate only
newSettings.rootCA.replace("\r", "");
newSettings.rootCA.replace("\n", "");

View File

@@ -134,7 +134,6 @@ class MqttSettingsService : public StatefulService<MqttSettings> {
// the MQTT client instance
MqttClient * _mqttClient;
void WiFiEvent(WiFiEvent_t event);
void onMqttConnect(bool sessionPresent);
void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason);
void

View File

@@ -11,37 +11,18 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer * server, FS * fs, Securit
configureTime(request, json);
});
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { configureNTP(); }, false);
}
void NTPSettingsService::begin() {
_fsPersistence.readFromFS();
configureNTP();
}
// handles both WiFI and Ethernet
void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
case ARDUINO_EVENT_ETH_DISCONNECTED:
if (_connected && emsesp::EMSESP::system_.ntp_connected()) {
emsesp::EMSESP::logger().info("WiFi connection dropped, stopping NTP");
_connected = false;
void NTPSettingsService::loop() {
if (_connected != emsesp::EMSESP::system_.network_connected()) {
_connected = emsesp::EMSESP::system_.network_connected();
configureNTP();
}
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
// emsesp::EMSESP::logger().info("Got IP address, starting NTP synchronization");
_connected = true;
configureNTP();
break;
default:
break;
}
}
// https://werner.rothschopf.net/microcontroller/202103_arduino_esp32_ntp_en.htm
@@ -55,9 +36,11 @@ void NTPSettingsService::configureNTP() {
} else {
setenv("TZ", _state.tzFormat.c_str(), 1);
tzset();
if (esp_sntp_enabled()) {
esp_sntp_stop();
}
}
}
void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVariant json) {
if (json.is<JsonObject>()) {
@@ -68,7 +51,12 @@ void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVari
tm.tm_isdst = -1; // not set by strptime, tells mktime to determine daylightsaving
time_t time = mktime(&tm);
struct timeval now = {.tv_sec = time, .tv_usec = {}};
#if CONFIG_IDF_TARGET_ESP32C3
// settimeofday and adjtime() does not work, unknown how to set time
emsesp::EMSESP::logger().warning("manual clock setting not possible");
#else
settimeofday(&now, nullptr);
#endif
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
return;

View File

@@ -44,6 +44,7 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
NTPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void loop();
static void ntp_received(struct timeval * tv);
private:
@@ -51,7 +52,6 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
FSPersistence<NTPSettings> _fsPersistence;
volatile bool _connected;
void WiFiEvent(WiFiEvent_t event);
void configureNTP();
void configureTime(AsyncWebServerRequest * request, JsonVariant json);
};

View File

@@ -9,7 +9,7 @@ NetworkSettingsService::NetworkSettingsService(AsyncWebServer * server, FS * fs,
, _stopping(false) {
addUpdateHandler([this] { reconfigureWiFiConnection(); }, false);
// Eth is also bound to the WifiGeneric event handler
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event, info); });
// Network.onEvent([this](arduino_event_id_t event, arduino_event_info_t info) { WiFiEvent(event, info); });
}
static bool formatBssid(const String & bssid, uint8_t (&mac)[6]) {
@@ -34,8 +34,8 @@ void NetworkSettingsService::begin() {
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
WiFi.mode(WIFI_STA);
WiFi.mode(WIFI_OFF);
// scan settings give connect issues since arduino 2.0.14 and arduino 3.x.x with some wifi systems
// WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN
@@ -62,28 +62,124 @@ void NetworkSettingsService::loop() {
_lastConnectionAttempt = currentMillis;
manageSTA();
}
static uint8_t connect = 0;
enum uint8_t {
CONNECT_IDLE = 0,
CONNECT_WAIT_ETH,
CONNECT_WAIT_IP4,
CONNECT_WAIT_ETH_IP4,
CONNECT_WAIT_IP6,
CONNECT_WAIT_ETH_IP6,
CONNECT_ETH_ACTIVE,
CONNECT_WIFI_ACTIVE
};
switch (connect) {
default:
connect = CONNECT_IDLE;
break;
case CONNECT_IDLE:
if (ETH.started() && _state.ssid.length() == 0) {
emsesp::EMSESP::logger().info("ETH started");
ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str());
ETH.enableIPv6(true);
if (_state.staticIPConfig) {
ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2);
}
connect = CONNECT_WAIT_ETH;
}
if (WiFi.isConnected()) {
emsesp::EMSESP::logger().info("Wifi connected");
if (_state.tx_power == 0) {
setWiFiPowerOnRSSI();
}
mDNS_start();
emsesp::EMSESP::system_.has_ipv6(true);
connect = CONNECT_WAIT_IP4;
}
break;
case CONNECT_WAIT_ETH:
if (ETH.connected()) {
emsesp::EMSESP::logger().info("ETH connected");
emsesp::EMSESP::system_.ethernet_connected(true);
mDNS_start();
emsesp::EMSESP::system_.has_ipv6(true);
connect = CONNECT_WAIT_ETH_IP4;
}
break;
case CONNECT_WAIT_ETH_IP4:
if (ETH.hasIP()) {
emsesp::EMSESP::logger().info("ETH IPv4: %s", ETH.localIP().toString().c_str());
connect = CONNECT_WAIT_ETH_IP6;
}
if (!ETH.connected()) {
connect = CONNECT_ETH_ACTIVE;
}
break;
case CONNECT_WAIT_ETH_IP6:
if (ETH.hasLinkLocalIPv6() && ETH.hasGlobalIPv6()) {
emsesp::EMSESP::system_.has_ipv6(true);
connect = CONNECT_ETH_ACTIVE;
}
if (!ETH.connected()) {
connect = CONNECT_ETH_ACTIVE;
}
break;
case CONNECT_ETH_ACTIVE:
if (!ETH.connected()) {
emsesp::EMSESP::logger().info("ETH disconnected");
emsesp::EMSESP::system_.ethernet_connected(false);
emsesp::EMSESP::system_.has_ipv6(false);
connect = CONNECT_IDLE;
}
break;
case CONNECT_WAIT_IP4:
if (!WiFi.localIP().toString().isEmpty()) {
emsesp::EMSESP::logger().info("Wifi IPv4: %s", WiFi.localIP().toString().c_str());
connect = CONNECT_WAIT_IP6;
}
if (!WiFi.isConnected()) {
connect = CONNECT_ETH_ACTIVE;
}
break;
case CONNECT_WAIT_IP6:
if (WiFi.linkLocalIPv6().toString() != "::" && WiFi.globalIPv6().toString() != "::") {
emsesp::EMSESP::logger().info("Wifi IPv6: %s, %s", WiFi.linkLocalIPv6().toString().c_str(), WiFi.globalIPv6().toString().c_str());
emsesp::EMSESP::system_.has_ipv6(true);
connect = CONNECT_WIFI_ACTIVE;
}
if (!WiFi.isConnected()) {
connect = CONNECT_WIFI_ACTIVE;
}
break;
case CONNECT_WIFI_ACTIVE:
if (!WiFi.isConnected()) {
emsesp::EMSESP::logger().info("WiFi disconnected");
if (_stopping) {
_lastConnectionAttempt = 0;
_stopping = false;
}
emsesp::EMSESP::system_.has_ipv6(false);
connect = CONNECT_IDLE;
}
break;
}
}
void NetworkSettingsService::manageSTA() {
// Abort if already connected, or if we have no SSID
if (WiFi.isConnected() || _state.ssid.length() == 0) {
#if ESP_IDF_VERSION_MAJOR >= 5
if (_state.ssid.length() == 0) {
ETH.enableIPv6(true);
}
#endif
return;
}
// Connect or reconnect as required
if ((WiFi.getMode() & WIFI_STA) == 0) {
#if ESP_IDF_VERSION_MAJOR >= 5
WiFi.setHostname(_state.hostname.c_str()); // updates shared default_hostname buffer
WiFi.enableSTA(true); // creates the STA netif
WiFi.STA.setHostname(_state.hostname.c_str()); // pushes to esp_netif_set_hostname
WiFi.enableIPv6(true);
#endif
if (_state.staticIPConfig) {
WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); // configure for static IP
}
WiFi.setHostname(_state.hostname.c_str()); // set hostname
// www.esp32.com/viewtopic.php?t=12055
if (_state.bandwidth20) {
@@ -293,7 +389,7 @@ const char * NetworkSettingsService::disconnectReason(uint8_t code) {
}
// handles both WiFI and Ethernet
void NetworkSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
void NetworkSettingsService::WiFiEvent(arduino_event_id_t event, arduino_event_info_t info) {
#ifndef EMSESP_STANDALONE
switch (event) {
@@ -305,7 +401,7 @@ void NetworkSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
connectcount_++; // count the number of WiFi reconnects
connectcount_ = connectcount_ + 1; // count the number of WiFi reconnects
emsesp::EMSESP::logger().warning("WiFi disconnected (#%d). Reason: %s (%d)",
connectcount_,
disconnectReason(info.wifi_sta_disconnected.reason),
@@ -325,11 +421,11 @@ void NetworkSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
break;
case ARDUINO_EVENT_ETH_START:
// configure for static IP
// apply hostname FIRST so DHCP DISCOVER carries the correct name
ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str());
if (_state.staticIPConfig) {
ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2);
}
ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str());
break;
case ARDUINO_EVENT_ETH_GOT_IP:
@@ -360,25 +456,15 @@ void NetworkSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
if (_state.tx_power == 0) {
setWiFiPowerOnRSSI();
}
#if ESP_IDF_VERSION_MAJOR < 5
WiFi.enableIpV6(); // force ipv6
#endif
break;
case ARDUINO_EVENT_ETH_CONNECTED:
#if ESP_IDF_VERSION_MAJOR < 5
ETH.enableIpV6(); // force ipv6
#endif
break;
// IPv6 specific - WiFi/Eth
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
case ARDUINO_EVENT_ETH_GOT_IP6: {
#if !TASMOTA_SDK && ESP_IDF_VERSION_MAJOR < 5
auto ip6 = IPv6Address((uint8_t *)info.got_ip6.ip6_info.ip.addr).toString();
#else
auto ip6 = IPAddress(IPv6, (uint8_t *)info.got_ip6.ip6_info.ip.addr, 0).toString();
#endif
const char * link = event == ARDUINO_EVENT_ETH_GOT_IP6 ? "Eth" : "WiFi";
if (ip6.startsWith("fe80")) {
emsesp::EMSESP::logger().info("IPv6 (%s) local: %s", link, ip6.c_str());

View File

@@ -107,7 +107,7 @@ class NetworkSettingsService : public StatefulService<NetworkSettings> {
volatile uint16_t connectcount_ = 0; // number of wifi reconnects
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void WiFiEvent(arduino_event_id_t event, arduino_event_info_t info);
void mDNS_start() const;
const char * disconnectReason(uint8_t code);
void reconfigureWiFiConnection();

View File

@@ -2,7 +2,7 @@
#include <emsesp.h>
#ifdef TASMOTA_SDK
#ifdef NO_TLS_SUPPORT
#include "lwip/dns.h"
#endif
@@ -32,21 +32,12 @@ void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
if (ethernet_connected) {
// Ethernet
root["local_ip"] = ETH.localIP().toString();
#if ESP_IDF_VERSION_MAJOR < 5
root["local_ipv6"] = ETH.localIPv6().toString();
#else
root["local_ipv6"] = ETH.linkLocalIPv6().toString();
#endif
root["mac_address"] = ETH.macAddress();
root["subnet_mask"] = ETH.subnetMask().toString();
root["gateway_ip"] = ETH.gatewayIP().toString();
#ifdef TASMOTA_SDK
IPAddress dnsIP1 = IPAddress(dns_getserver(0));
IPAddress dnsIP2 = IPAddress(dns_getserver(1));
#else
IPAddress dnsIP1 = ETH.dnsIP(0);
IPAddress dnsIP2 = ETH.dnsIP(1);
#endif
if (IPUtils::isSet(dnsIP1)) {
root["dns_ip_1"] = dnsIP1.toString();
}
@@ -55,11 +46,7 @@ void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
}
} else if (wifi_status == WL_CONNECTED) {
root["local_ip"] = WiFi.localIP().toString();
#if ESP_IDF_VERSION_MAJOR < 5
root["local_ipv6"] = WiFi.localIPv6().toString();
#else
root["local_ipv6"] = WiFi.linkLocalIPv6().toString();
#endif
root["mac_address"] = WiFi.macAddress();
root["rssi"] = WiFi.RSSI();
root["ssid"] = WiFi.SSID();
@@ -71,14 +58,8 @@ void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
if (WiFi.gatewayIP() != INADDR_NONE) {
root["gateway_ip"] = WiFi.gatewayIP().toString();
}
#ifdef TASMOTA_SDK
IPAddress dnsIP1 = IPAddress(dns_getserver(0));
IPAddress dnsIP2 = IPAddress(dns_getserver(1));
#else
IPAddress dnsIP1 = WiFi.dnsIP(0);
IPAddress dnsIP2 = WiFi.dnsIP(1);
#endif
if (dnsIP1 != INADDR_NONE) {
root["dns_ip_1"] = dnsIP1.toString();
}

124
src/core/EMSESP_Version.h Normal file
View File

@@ -0,0 +1,124 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2026 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_Version_H
#define EMSESP_Version_H
#include <cstdio>
#include <cstring>
#include <string>
// Drop-in lightweight replacement for the subset of the semver library actually used by EMS-ESP.
// The previous semver library (lib/semver) builds a std::map + std::function-based state machine on
// every parse, which fragments the internal heap on the ESP32. This replacement does no heap
// allocation beyond the std::string member for the prerelease tag, and matches the API surface
// we consume: construction from string, major()/minor()/patch()/prerelease(), and operator>/</==.
//
// Only strict numeric precedence (major.minor.patch) is used for comparison in EMS-ESP, so we
// intentionally ignore prerelease tags during comparison rather than implement the full semver
// ordering rules. This is consistent with how the old code was used (callers only check major/
// minor/patch numerically; prerelease() is only read for logging).
namespace version {
class EMSESP_Version {
public:
EMSESP_Version() = default;
// Construct from a version string like "3.9.0-dev.14" or "3.9.0".
// Anything past a '-' or '+' is kept as the prerelease string and not interpreted.
explicit EMSESP_Version(const std::string & s) {
parse(s.c_str());
}
explicit EMSESP_Version(const char * s) {
parse(s ? s : "");
}
int major() const {
return major_;
}
int minor() const {
return minor_;
}
int patch() const {
return patch_;
}
const std::string & prerelease() const {
return prerelease_;
}
// Numeric-only comparison (major.minor.patch). Prerelease tags are ignored on purpose.
friend bool operator<(const EMSESP_Version & a, const EMSESP_Version & b) {
if (a.major_ != b.major_)
return a.major_ < b.major_;
if (a.minor_ != b.minor_)
return a.minor_ < b.minor_;
return a.patch_ < b.patch_;
}
friend bool operator>(const EMSESP_Version & a, const EMSESP_Version & b) {
return b < a;
}
friend bool operator==(const EMSESP_Version & a, const EMSESP_Version & b) {
return a.major_ == b.major_ && a.minor_ == b.minor_ && a.patch_ == b.patch_;
}
friend bool operator!=(const EMSESP_Version & a, const EMSESP_Version & b) {
return !(a == b);
}
friend bool operator>=(const EMSESP_Version & a, const EMSESP_Version & b) {
return !(a < b);
}
friend bool operator<=(const EMSESP_Version & a, const EMSESP_Version & b) {
return !(b < a);
}
private:
int major_ = 0;
int minor_ = 0;
int patch_ = 0;
std::string prerelease_;
void parse(const char * s) {
major_ = minor_ = patch_ = 0;
prerelease_.clear();
if (s == nullptr || *s == '\0') {
return;
}
// parse numeric major.minor.patch; accept partial ("3", "3.9", "3.9.0")
sscanf(s, "%d.%d.%d", &major_, &minor_, &patch_);
// capture prerelease tag after '-' if present (stop at '+' which is build metadata)
const char * dash = strchr(s, '-');
if (dash != nullptr) {
const char * plus = strchr(dash, '+');
if (plus != nullptr) {
prerelease_.assign(dash + 1, plus - dash - 1);
} else {
prerelease_.assign(dash + 1);
}
}
}
};
} // namespace version
#endif

View File

@@ -347,23 +347,13 @@ void AnalogSensor::reload(bool get_nvs) {
sensor.polltime_ = sensor.value() != 0 ? uuid::get_uptime() + (sensor.factor() * 1000) : 0;
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
LOG_DEBUG("PWM output on GPIO %02d", sensor.gpio());
#if ESP_IDF_VERSION_MAJOR >= 5
ledcAttach(sensor.gpio(), sensor.factor(), 13);
#else
uint8_t channel = sensor.type() - AnalogType::PWM_0;
ledcSetup(channel, sensor.factor(), 13);
ledcAttachPin(sensor.gpio(), channel);
#endif
if (sensor.offset() > 100) {
sensor.set_offset(100);
} else if (sensor.offset() < 0) {
sensor.set_offset(0);
}
#if ESP_IDF_VERSION_MAJOR >= 5
ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100));
#else
ledcWrite(channel, (uint32_t)(sensor.offset() * 8191 / 100));
#endif
sensor.set_value(sensor.offset());
sensor.set_uom(DeviceValueUOM::PERCENT);
publish_sensor(sensor);
@@ -1041,12 +1031,7 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) {
}
sensor.set_offset(val);
sensor.set_value(val);
#if ESP_IDF_VERSION_MAJOR >= 5
ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100));
#else
uint8_t channel = sensor.type() - AnalogType::PWM_0;
ledcWrite(channel, (uint32_t)(val * 8191 / 100));
#endif
} else {
return false;
}

View File

@@ -1,91 +0,0 @@
/*
* 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_COMMON_H
#define EMSESP_COMMON_H
#include <uuid/log.h>
using uuid::log::Level;
#if defined(EMSESP_DEBUG)
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
#else
#define LOG_DEBUG(...)
#endif
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
// flash strings
using uuid::string_vector;
using string_vector = std::vector<const char *>;
#ifdef FPSTR
#undef FPSTR
#endif
// clang-format off
#define FPSTR(pstr_pointer) pstr_pointer
#define MAKE_WORD_CUSTOM(string_name, string_literal) static const char __pstr__##string_name[] = string_literal;
#define MAKE_WORD(string_name) MAKE_WORD_CUSTOM(string_name, #string_name)
#define F_(string_name) (__pstr__##string_name)
#define FL_(list_name) (__pstr__L_##list_name)
// Translation counter - capture baseline before any MAKE_TRANSLATION calls
enum { EMSESP_TRANSLATION_COUNT_START = __COUNTER__ };
// The language settings below must match system.cpp
#if defined(EMSESP_EN_ONLY)
// EN only
#define MAKE_WORD_TRANSLATION(list_name, en, ...) static const char * const __pstr__L_##list_name[] = {en, nullptr};
#define MAKE_TRANSLATION(list_name, shortname, en, ...) static constexpr int __translation_counter_##list_name = __COUNTER__; static const char * const __pstr__L_##list_name[] = {shortname, en, nullptr};
#elif defined(EMSESP_TEST) || defined(EMSESP_DE_ONLY)
// EN + DE (Test mode uses two languages to save flash memory)
#define MAKE_WORD_TRANSLATION(list_name, en, de, ...) static const char * const __pstr__L_##list_name[] = {en, de, nullptr};
#define MAKE_TRANSLATION(list_name, shortname, en, de, ...) static constexpr int __translation_counter_##list_name = __COUNTER__; static const char * const __pstr__L_##list_name[] = {shortname, en, de, nullptr};
#else
// All languages
#define MAKE_WORD_TRANSLATION(list_name, ...) static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
#define MAKE_TRANSLATION(list_name, ...) static constexpr int __translation_counter_##list_name = __COUNTER__; static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
#endif
#define MAKE_NOTRANSLATION(list_name, ...) static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
// fixed strings, no translations
#define MAKE_ENUM_FIXED(enum_name, ...) static const char * const __pstr__L_##enum_name[] = {__VA_ARGS__, nullptr};
// with translations
#define MAKE_ENUM(enum_name, ...) static const char * const * __pstr__L_##enum_name[] = {__VA_ARGS__, nullptr};
// clang-format on
// load translations
#include "locale_translations.h"
#include "locale_common.h"
// Translation count - dynamically calculated at compile-time
enum { EMSESP_TRANSLATION_COUNT_END = __COUNTER__ };
static constexpr uint16_t EMSESP_TRANSLATION_COUNT = EMSESP_TRANSLATION_COUNT_END - EMSESP_TRANSLATION_COUNT_START - 1;
#endif

View File

@@ -285,6 +285,8 @@ enum {
#define EMSESP_PLATFORM "ESP32S3"
#elif CONFIG_IDF_TARGET_ESP32 || EMSESP_STANDALONE
#define EMSESP_PLATFORM "ESP32"
#elif CONFIG_IDF_TARGET_ESP32C6
#define EMSESP_PLATFORM "ESP32C6"
#else
#error Target CONFIG_IDF_TARGET is not supported
#endif
@@ -293,12 +295,9 @@ enum {
#ifndef STRINGIZE
#define STRINGIZE(s) #s
#endif
#ifdef TASMOTA_SDK
#define ARDUINO_VERSION_STR(major, minor, patch) "Tasmota Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#else
#define ARDUINO_VERSION_STR(major, minor, patch) "ESP32 Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#endif
#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
#endif
#endif
#endif

View File

@@ -19,6 +19,7 @@
#ifndef EMSESP_EMSFACTORY_H_
#define EMSESP_EMSFACTORY_H_
#include <cstdint>
#include <memory> // for unique_ptr
#include <map>

View File

@@ -380,12 +380,16 @@ std::string commands(std::string & expr, bool quotes) {
if (return_code != CommandRet::OK && return_code != CommandRet::NO_VALUE) {
return expr = "";
}
std::string data = output["api_data"] | "";
std::string data;
if (output["api_data"].is<std::string>()) {
data = output["api_data"].as<std::string>();
if (!isnum(data) && quotes) {
data.insert(data.begin(), '"');
data.insert(data.end(), '"');
}
} else {
serializeJson(output, data);
}
expr.replace(f, l, data);
e = f + data.length();
expr_new = Helpers::toLower(expr);
@@ -700,7 +704,6 @@ std::string compute(const std::string & expr) {
std::string cmd = expr_new.substr(f, e - f).c_str();
JsonDocument doc;
if (DeserializationError::Ok == deserializeJson(doc, cmd)) {
HTTPClient * http = new HTTPClient;
std::string url, header_s, value_s, method_s, key_s, keys_s;
// search keys lower case
for (JsonPair p : doc.as<JsonObject>()) {
@@ -720,33 +723,126 @@ std::string compute(const std::string & expr) {
keys_s = p.key().c_str();
}
}
if (http->begin(url.c_str())) {
bool content_set = false;
std::string value = doc[value_s] | "";
std::string method = doc[method_s] | "GET";
if (value.length()) {
method = "POST";
}
std::string result;
int httpResult = 0;
#ifndef NO_TLS_SUPPORT
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
ssl_client->setBufferSizes(1024, 1024);
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
url.replace(0, 8, "");
std::string host = url;
auto index = url.find_first_of('/');
if (index != std::string::npos) {
host = url.substr(0, index);
url.replace(0, index, "");
}
/*
index = host.find_first_of('@');
std::string auth;
if (index != std::string::npos) {
auth = base64::encode(host.substr(0, index));
host.replace(0, index, "");
}
*/
ssl_client->setClient(basic_client);
if (ssl_client->connect(host.c_str(), 443)) {
if (value.length() || Helpers::toLower(method) == "post") {
ssl_client->print("POST ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc[header_s].as<JsonObject>()) {
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
if (!content_set) {
ssl_client->print("Content-Type: ");
if (value.starts_with('{')) {
ssl_client->println(asyncsrv::T_application_json);
} else {
ssl_client->println(asyncsrv::T_text_plain);
}
}
ssl_client->print("Content-Length: ");
ssl_client->println(value.length());
ssl_client->println("Connection: close");
ssl_client->print("\r\n");
ssl_client->print(value.c_str());
} else {
ssl_client->print("GET ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc[header_s].as<JsonObject>()) {
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
ssl_client->println("Connection: close");
}
auto ms = millis();
while (!ssl_client->available() && millis() - ms < 3000) {
delay(0);
}
while (ssl_client->available()) {
result += (char)ssl_client->read();
}
ssl_client->stop();
index = result.find_first_of(' ');
if (index != std::string::npos) {
httpResult = stoi(result.substr(index + 1, 3));
}
index = result.find("\r\n\r\n");
if (index != std::string::npos) {
result.replace(0, index + 4, "");
}
}
delete ssl_client;
delete basic_client;
} else
#endif
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
HTTPClient * http = new HTTPClient;
if (http->begin(url.c_str())) {
for (JsonPair p : doc[header_s].as<JsonObject>()) {
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
}
std::string value = doc[value_s] | "";
std::string method = doc[method_s] | "get";
// if there is data, force a POST
if (value.length() || Helpers::toLower(method) == "post") {
if (value.find_first_of('{') != std::string::npos) {
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON
if (!content_set) {
http->addHeader("Content-Type", value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain);
}
httpResult = http->POST(value.c_str());
} else {
httpResult = http->GET(); // normal GET
}
if (httpResult > 0) {
std::string result = http->getString().c_str();
result = http->getString().c_str();
}
}
http->end();
delete http;
}
if (httpResult == 200) {
std::string key = doc[key_s] | "";
JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body
if (doc[keys_s].is<JsonArray>()) {
keys_doc.set(doc[keys_s].as<JsonArray>());
}
JsonArray keys = keys_doc.as<JsonArray>();
if (key.length() || !keys.isNull()) {
doc.clear();
if (DeserializationError::Ok == deserializeJson(doc, result)) {
@@ -767,10 +863,9 @@ std::string compute(const std::string & expr) {
}
}
}
expr_new.replace(f, e - f, result.c_str());
}
http->end();
delete http;
expr_new.replace(f, e - f, result);
} else if (httpResult != 0) {
EMSESP::logger().warning("URL command failed with https code: %d, response: %s", httpResult, result.c_str());
}
}
f = expr_new.find_first_of('{', e);

View File

@@ -26,18 +26,26 @@
#include <esp_mac.h>
#include "esp_efuse.h"
#include <nvs.h>
#include <mbedtls/base64.h>
#endif
#include <HTTPClient.h>
#include <map>
#include <semver200.h>
#include "EMSESP_Version.h"
#if defined(EMSESP_TEST)
#include "../test/test.h"
#endif
#ifndef NO_TLS_SUPPORT
#define ENABLE_SMTP
#define USE_ESP_SSLCLIENT
#define READYCLIENT_SSL_CLIENT ESP_SSLClient
#define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient
#include <ESP_SSLClient.h>
#include <ReadyMail.h>
#endif
namespace emsesp {
// Languages supported. Note: the order is important
@@ -106,6 +114,115 @@ bool System::command_send(const char * value, const int8_t id) {
return EMSESP::txservice_.send_raw(value); // ignore id
}
bool System::command_sendmail(const char * value, const int8_t id) {
bool enabled = false;
bool ssl, starttls;
uint16_t port;
String server, login, pass, sender, recp, subject;
EMSESP::webSettingsService.read([&](WebSettings & settings) {
enabled = settings.email_enabled;
ssl = settings.email_ssl;
starttls = settings.email_starttls;
server = settings.email_server;
port = settings.email_port;
login = settings.email_login;
pass = settings.email_pass;
sender = settings.email_sender;
recp = settings.email_recp;
subject = settings.email_subject;
});
if (!enabled) {
return false;
}
LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value);
// LOG_DEBUG("Command sendmail port %d called with '%s'", port, value);
bool success = false;
#ifndef NO_TLS_SUPPORT
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ReadyClient * r_client = new ReadyClient(*ssl_client);
SMTPClient * smtp = new SMTPClient(*r_client);
ssl_client->setClient(basic_client);
ssl_client->setInsecure();
ssl_client->setBufferSizes(1024, 1024);
r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text);
// smtp->connect(server, port, sendmailCallback);
smtp->connect(server, port);
if (!smtp->isConnected()) {
LOG_ERROR("Sendmail connection error");
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
return false;
}
// LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str());
smtp->authenticate(login, pass, readymail_auth_password);
if (!smtp->isAuthenticated()) {
LOG_ERROR("Sendmail authenticate error");
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
return false;
}
JsonDocument doc;
String body = value;
if (body.length()) {
auto error = deserializeJson(doc, (const char *)value);
if (!error && doc.as<JsonObject>().size() >= 0) {
subject = doc["subject"] | subject;
recp = doc["to"] | recp;
sender = doc["from"] | sender;
body = doc["body"] | body;
}
}
SMTPMessage & msg = smtp->getMessage();
msg.headers.add(rfc822_subject, subject);
msg.headers.add(rfc822_from, sender);
msg.headers.add(rfc822_to, recp);
// Use addCustom to add custom header e.g. Importance and Priority.
// msg.headers.addCustom("Importance", PRIORITY);
// msg.headers.addCustom("X-MSMail-Priority", PRIORITY);
// msg.headers.addCustom("X-Priority", PRIORITY_NUM);
EMSESP::webSchedulerService.computed_value.clear();
EMSESP::webSchedulerService.raw_value = body.c_str();
for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) {
delay(1);
}
if (!EMSESP::webSchedulerService.computed_value.empty()) {
body = EMSESP::webSchedulerService.computed_value.c_str();
EMSESP::webSchedulerService.computed_value.clear();
EMSESP::webSchedulerService.computed_value.shrink_to_fit(); // free allocated memory
}
msg.text.body(body);
// bodyText.replace("\r\n", "<br>\r\n");
// msg.html.body("<html><body><div style=\"color:#cc0066;\">" + bodyText + "</div></body></html>");
// msg.html.transferEncoding("base64");
// With embedFile function, the html message will send as attachment.
// if (EMBED_MESSAGE)
// msg.html.embedFile(true, "msg.html", embed_message_type_attachment);
msg.timestamp = time(nullptr);
success = smtp->send(msg);
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
#endif
return success;
}
// return string of languages and count
std::string System::languages_string() {
std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages (";
@@ -238,7 +355,8 @@ bool System::command_message(const char * value, const int8_t id, JsonObject out
LOG_INFO("Message: %s", EMSESP::webSchedulerService.computed_value.c_str()); // send to log
Mqtt::queue_publish(F_(message), EMSESP::webSchedulerService.computed_value); // send to MQTT if enabled
output["api_data"] = EMSESP::webSchedulerService.computed_value; // send to API
EMSESP::webSchedulerService.computed_value.clear();
EMSESP::webSchedulerService.computed_value.shrink_to_fit();
return true;
}
@@ -289,11 +407,11 @@ void System::get_partition_info() {
partition_info_.clear(); // clear existing data
#ifdef EMSESP_STANDALONE
// dummy data for standalone mode - version, size, install_date
partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""};
partition_info_["app1"] = {"", 0, ""};
partition_info_["factory"] = {"", 0, ""};
partition_info_["boot"] = {"", 0, ""};
// dummy data for standalone mode - version, size, install_date in UTC epoch
partition_info_["app0"] = {EMSESP_APP_VERSION, 0, 0};
partition_info_["app1"] = {"", 0, 0};
partition_info_["factory"] = {"", 0, 0};
partition_info_["boot"] = {"", 0, 0};
#else
auto current_partition = (const char *)esp_ota_get_running_partition()->label;
@@ -334,10 +452,9 @@ void System::get_partition_info() {
char c[20];
snprintf(c, sizeof(c), "d_%s", (const char *)part->label);
time_t d = EMSESP::nvs_.getULong(c, 0);
char time_string[25];
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
p_info.install_date = d > 1500000000L ? time_string : "";
p_info.install_date = d > 1500000000L ? d : 0; // store UTC epoch; formatted to local time at render
if (!p_info.version.empty()) {
esp_image_metadata_t meta = {};
esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size};
if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) {
@@ -345,9 +462,9 @@ void System::get_partition_info() {
} else {
p_info.size = 0;
}
partition_info_[part->label] = p_info;
}
}
it = esp_partition_next(it); // loop to next partition
}
@@ -355,7 +472,7 @@ void System::get_partition_info() {
#endif
}
// set NTP install time/date for the current partition
// set install time/date for the current partition, in UTC
// assumes NTP is connected and working
void System::set_partition_install_date() {
#ifndef EMSESP_STANDALONE
@@ -463,7 +580,6 @@ void System::system_restart(const char * partitionname) {
Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1
EMSuart::stop(); // stop UART so there is no interference
#ifndef EMSESP_STANDALONE
delay(1000); // wait 1 second
ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called
@@ -565,7 +681,6 @@ void System::store_settings(WebSettings & settings) {
locale_ = settings.locale;
developer_mode_ = settings.developer_mode;
// start services
if (settings.modbus_enabled) {
if (EMSESP::modbus_ == nullptr) {
@@ -607,20 +722,11 @@ void System::start() {
appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_;
refreshHeapMem(); // refresh free heap and max alloc heap
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
temp_sensor_config_t temp_sensor = TSENS_CONFIG_DEFAULT();
temp_sensor_get_config(&temp_sensor);
temp_sensor.dac_offset = TSENS_DAC_DEFAULT; // DEFAULT: range:-10℃ ~ 80℃, error < 1℃.
temp_sensor_set_config(temp_sensor);
temp_sensor_start();
temp_sensor_read_celsius(&temperature_);
#else
temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
temperature_sensor_install(&temp_sensor_config, &temperature_handle_);
temperature_sensor_enable(temperature_handle_);
temperature_sensor_get_celsius(temperature_handle_, &temperature_);
#endif
#endif
#endif
EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
@@ -841,16 +947,9 @@ void System::send_info_mqtt() {
doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
#if ESP_IDF_VERSION_MAJOR < 5
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") {
doc["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
}
#else
if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") {
doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6());
}
#endif
}
#endif
Mqtt::queue_publish_retain(F_(info), doc.as<JsonObject>()); // topic called "info" and it's Retained
@@ -959,12 +1058,11 @@ void System::network_init() {
delay(500);
digitalWrite(eth_power_, HIGH);
}
#if ESP_IDF_VERSION_MAJOR < 5
eth_present_ = ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode);
#else
eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode);
#endif
if (eth_present_) {
// Push hostname to the ETH netif immediately after it's created
ETH.setHostname(hostname_.c_str());
}
#endif
}
@@ -976,13 +1074,9 @@ void System::system_check() {
#ifndef EMSESP_STANDALONE
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
temp_sensor_read_celsius(&temperature_);
#else
temperature_sensor_get_celsius(temperature_handle_, &temperature_);
#endif
#endif
#endif
#ifdef EMSESP_PINGTEST
static uint64_t ping_count = 0;
@@ -1034,6 +1128,7 @@ void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_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_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY);
@@ -1240,11 +1335,18 @@ void System::show_system(uuid::console::Shell & shell) {
if (partition.second.version.empty()) {
continue; // no version, empty string
}
std::string installed;
if (partition.second.install_date > 0) {
char time_string[25];
time_t d = partition.second.install_date;
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
installed = std::string(", installed on ") + time_string;
}
shell.printfln(" %s: v%s (%d KB%s) %s",
partition.first.c_str(),
partition.second.version.c_str(),
partition.second.size,
partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(),
installed.c_str(),
(strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : "");
}
@@ -1275,16 +1377,9 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str());
shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str());
shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str());
#if ESP_IDF_VERSION_MAJOR < 5
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.localIPv6()).c_str());
}
#else
if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str());
}
#endif
break;
case WL_CONNECT_FAILED:
@@ -1315,15 +1410,9 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str());
shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str());
shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str());
#if ESP_IDF_VERSION_MAJOR < 5
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.localIPv6()).c_str());
}
#else
if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str());
}
#endif
}
shell.println();
@@ -1345,10 +1434,10 @@ void System::show_system(uuid::console::Shell & shell) {
}
shell.println();
#endif
}
// see if there is a restore of an older settings file that needs to be applied
// note there can be only one file at a time
bool System::check_restore() {
@@ -1533,8 +1622,8 @@ bool System::check_upgrade() {
settingsVersion = "3.5.0"; // this was the last stable version without version info
}
version::Semver200_version settings_version(settingsVersion);
version::Semver200_version this_version(EMSESP_APP_VERSION);
version::EMSESP_Version settings_version(settingsVersion);
version::EMSESP_Version this_version(EMSESP_APP_VERSION);
std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease());
std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease());
@@ -1648,8 +1737,8 @@ bool System::check_upgrade() {
return false; // no reboot required
}
// map each config filename to its human-readable section key
#ifndef EMSESP_STANDALONE
// map each config filename to its human-readable section key
static const std::pair<const char *, const char *> SECTION_MAP[] = {
{NETWORK_SETTINGS_FILE, "Network"},
{AP_SETTINGS_FILE, "AP"},
@@ -1685,18 +1774,17 @@ void System::exportSettings(const std::string & type, const char * filename, Jso
File settingsFile = LittleFS.open(filename);
if (settingsFile) {
{
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject node = output[section].to<JsonObject>();
for (JsonPair kvp : jsonDocument.as<JsonObject>()) {
node[kvp.key()] = kvp.value();
}
} else {
settingsFile.close(); // close early, we no longer need the file
if (error || !jsonDocument.is<JsonObject>()) {
LOG_ERROR("Failed to deserialize settings file %s", filename);
return;
}
output[section].set(jsonDocument.as<JsonObject>());
}
LOG_DEBUG("Exported %s settings from file %s", section, filename);
settingsFile.close();
} else {
LOG_ERROR("No settings file for %s found", filename);
}
@@ -1748,13 +1836,15 @@ void System::exportSystemBackup(JsonObject output) {
if (file) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, file);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject node = nodes.add<JsonObject>();
node["type"] = "customSupport";
node["data"] = jsonDocument.as<JsonObject>();
}
file.close();
file.close(); // close early, we no longer need the file
if (!error && jsonDocument.is<JsonObject>()) {
JsonObject support_node = nodes.add<JsonObject>();
support_node["type"] = "customSupport";
support_node["data"].set(jsonDocument.as<JsonObject>());
LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE);
} else {
LOG_ERROR("Failed to deserialize custom support file");
}
}
// Backup NVS values
@@ -1763,27 +1853,16 @@ void System::exportSystemBackup(JsonObject output) {
const char * nvs_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs1") ? "nvs1" : "nvs"; // nvs1 is on 16MBs
nvs_iterator_t it = nullptr;
#if ESP_IDF_VERSION_MAJOR < 5
it = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY);
if (it == nullptr) {
#else
esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it);
if (err != ESP_OK) {
#endif
LOG_ERROR("Failed to find NVS entry for %s", nvs_part);
return;
}
JsonArray entries = node["nvs"].to<JsonArray>();
#if ESP_IDF_VERSION_MAJOR < 5
while (it != nullptr) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
#else
while (err == ESP_OK) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
#endif
JsonObject entry = entries.add<JsonObject>();
entry["type"] = info.type;
entry["key"] = info.key;
@@ -1818,14 +1897,8 @@ void System::exportSystemBackup(JsonObject output) {
entry["value"] = EMSESP::nvs_.getString(info.key);
break;
}
#if ESP_IDF_VERSION_MAJOR < 5
it = nvs_entry_next(it);
}
#else
err = nvs_entry_next(&it);
}
#endif
if (it != nullptr) {
nvs_release_iterator(it);
@@ -3323,6 +3396,10 @@ void System::set_valid_system_gpios() {
} else {
valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 20, 24, 28-31");
}
#elif CONFIG_IDF_TARGET_ESP32C6
// https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32c6/api-reference/peripherals/gpio.html
// 24-30 used for flash, 12-13 USB, 16-17 uart0
valid_system_gpios_ = string_range_to_vector("0-30", "12-13, 16-17, 24-30");
#elif defined(EMSESP_STANDALONE)
valid_system_gpios_ = string_range_to_vector("0-39");
#endif

View File

@@ -37,11 +37,7 @@
#include <uuid/log.h>
#include <PButton.h>
#if ESP_ARDUINO_VERSION_MAJOR < 3
#define EMSESP_RGB_WRITE neopixelWrite
#else
#define EMSESP_RGB_WRITE rgbLedWrite
#endif
#if CONFIG_IDF_TARGET_ESP32
// there is no official API available on the original ESP32
@@ -49,12 +45,8 @@ extern "C" {
uint8_t temprature_sens_read();
}
#elif CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
#include "driver/temp_sensor.h"
#else
#include "driver/temperature_sensor.h"
#endif
#endif
using uuid::console::Shell;
@@ -83,7 +75,7 @@ enum FUSE_VALUE : uint8_t { ALL = 0, MFG = 1, MODEL = 2, BOARD = 3, REV = 4, BAT
struct PartitionInfo {
std::string version;
size_t size;
std::string install_date; // optional, only available if NTP is connected
time_t install_date; // UTC epoch seconds; 0 if unknown. Format with localtime() at render time so it honors the current TZ.
};
class System {
@@ -103,6 +95,7 @@ class System {
static bool command_info(const char * value, const int8_t id, JsonObject output);
static bool command_response(const char * value, const int8_t id, JsonObject output);
static bool command_service(const char * cmd, const char * value);
static bool command_sendmail(const char * value, const int8_t id);
static bool command_txpause(const char * value, const int8_t id);
static bool get_value_info(JsonObject root, const char * cmd);
@@ -502,9 +495,7 @@ class System {
uint32_t appfree_;
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR >= 5
temperature_sensor_handle_t temperature_handle_ = NULL;
#endif
#endif
float temperature_ = 0;
};

View File

@@ -163,6 +163,7 @@ class EMSbus {
static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3
static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus
static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning
static constexpr uint8_t EMS_POLL_MATCH_LIMIT = 3; // consecutive poll matches needed before declaring bus connected
static bool is_ht3() {
return (ems_mask_ == EMS_MASK_HT3);
@@ -210,7 +211,6 @@ class EMSbus {
if (!last_bus_activity_) {
bus_uptime_start_ = timestamp;
}
last_bus_activity_ = timestamp;
bus_connected_ = true;
}

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.8.2-dev.14"
#define EMSESP_APP_VERSION "3.9.0-dev.0"

View File

@@ -39,7 +39,7 @@ uint32_t inverse_mask = 0;
// receive task, wait for break and call incoming_telegram
void EMSuart::uart_event_task(void * pvParameters) {
uart_event_t event;
uint8_t telegram[UART_FIFO_LEN + 1]; // same size as in driver_install
uint8_t telegram[UART_HW_FIFO_LEN(EMSUART_NUM) + 1]; // same size as in driver_install
uint8_t length = 0;
while (1) {
@@ -66,18 +66,23 @@ void EMSuart::uart_event_task(void * pvParameters) {
// initialize UART driver
void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t tx_gpio) {
if (tx_mode_ == EMS_TXMODE_INIT) {
#if CONFIG_IDF_TARGET_ESP32C6
uart_config_t uart_config = {.baud_rate = EMSUART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0};
#else
uart_config_t uart_config = {.baud_rate = EMSUART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0,
.source_clk = UART_SCLK_APB
#if ESP_ARDUINO_VERSION_MAJOR >= 3
,
.flags = {0}
.source_clk = UART_SCLK_APB,
.flags = {0}};
#endif
};
#if defined(EMSUART_RX_INVERT)
inverse_mask |= UART_SIGNAL_RXD_INV;
#endif
@@ -87,7 +92,12 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t
uart_param_config(EMSUART_NUM, &uart_config);
uart_set_pin(EMSUART_NUM, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_set_line_inverse(EMSUART_NUM, inverse_mask);
uart_driver_install(EMSUART_NUM, UART_FIFO_LEN + 1, 0, UART_FIFO_LEN + 3, &uart_queue, 0); // buffer must be > fifo, queue can hold data+break+overflow message
uart_driver_install(EMSUART_NUM,
UART_HW_FIFO_LEN(EMSUART_NUM) + 1,
0,
UART_HW_FIFO_LEN(EMSUART_NUM) + 3,
&uart_queue,
0); // buffer must be > fifo, queue can hold data+break+overflow message
uart_set_rx_full_threshold(EMSUART_NUM, 1);
uart_set_rx_timeout(EMSUART_NUM, 0); // disable

View File

@@ -54,10 +54,10 @@ void WebCustomization::read(WebCustomization & customizations, JsonObject root)
JsonArray sensorsJson = root["ts"].to<JsonArray>();
for (const SensorCustomization & sensor : customizations.sensorCustomizations) {
JsonObject sensorJson = sensorsJson.add<JsonObject>();
sensorJson["id"] = (const char *)sensor.id; // ID of chip
sensorJson["id"] = (const char *)sensor.id; // ID of dallas temperature sensor chip
sensorJson["name"] = (const char *)sensor.name; // n
sensorJson["offset"] = sensor.offset; // o
sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage
sensorJson["is_system"] = sensor.is_system; // s for gateway_temperature
}
// Analog Sensor customization

View File

@@ -352,7 +352,6 @@ bool WebSchedulerService::command(const char * name, const std::string & command
// parse json
JsonDocument doc;
if (deserializeJson(doc, cmd) == DeserializationError::Ok) {
HTTPClient * http = new HTTPClient;
std::string url = doc["url"] | "";
// for a GET with parameters replace commands with values
// don't search the complete url, it may contain a devicename in path
@@ -363,40 +362,139 @@ bool WebSchedulerService::command(const char * name, const std::string & command
commands(s, false);
url.replace(q + 1, l, s);
}
if (http->begin(url.c_str())) {
// add any given headers
for (JsonPair p : doc["header"].as<JsonObject>()) {
http->addHeader(p.key().c_str(), p.value().as<String>().c_str());
}
std::string value = doc["value"] | data.c_str(); // extract value if its in the command, or take the data
std::string value = doc["value"] | data; // extract value if its in the command, or take the data
std::string method = doc["method"] | "GET"; // default GET
commands(value, false);
// if there is data, force a POST
if (value.length()) {
method = "POST";
}
std::string result;
int httpResult = 0;
if (value.length() || method == "post") { // we have all lowercase
if (value.find_first_of('{') != std::string::npos) {
#ifndef NO_TLS_SUPPORT
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
ssl_client->setBufferSizes(1024, 1024);
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
url.replace(0, 8, "");
std::string host = url;
auto index = url.find_first_of('/');
if (index != std::string::npos) {
host = url.substr(0, index);
url.replace(0, index, "");
}
// EMSESP::logger().debug("Host: %s, URL: %s", host.c_str(), url.c_str());
ssl_client->setClient(basic_client);
if (ssl_client->connect(host.c_str(), 443)) {
if (value.length() || Helpers::toLower(method) == "post") {
// EMSESP::logger().debug("POST %s HTTP/1.1", url.c_str());
ssl_client->print("POST ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
bool content_set = false;
for (JsonPair p : doc["header"].as<JsonObject>()) {
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
if (!content_set) {
ssl_client->print("Content-Type: ");
if (value.starts_with('{')) {
ssl_client->println(asyncsrv::T_application_json);
} else {
ssl_client->println(asyncsrv::T_text_plain);
}
}
ssl_client->print("Content-Length: ");
ssl_client->println(value.length());
ssl_client->println("Connection: close");
ssl_client->print("\r\n");
ssl_client->print(value.c_str());
} else {
// EMSESP::logger().debug("GET %s HTTP/1.1", url.c_str());
ssl_client->print("GET ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc["header"].as<JsonObject>()) {
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
ssl_client->println("Connection: close");
}
auto ms = millis();
while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) {
delay(0);
}
while (ssl_client->available()) {
result += (char)ssl_client->read();
}
ssl_client->stop();
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
index = result.find_first_of(' ');
if (index != std::string::npos) {
httpResult = stoi(result.substr(index + 1, 3));
// EMSESP::logger().debug("HTTPS code: %i", httpResult);
}
index = result.find("\r\n\r\n");
if (index != std::string::npos) {
result.replace(0, index + 4, "");
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
}
} else {
EMSESP::logger().warning("HTTPS connection failed");
}
delete ssl_client;
delete basic_client;
// check HTTP return code
if (httpResult != 200) {
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
return false;
}
return true;
} else
#endif
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
HTTPClient * http = new HTTPClient;
if (http->begin(url.c_str())) {
bool content_set = false;
for (JsonPair p : doc["header"].as<JsonObject>()) {
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
content_set |= p.key() == "content-type";
}
// if there is data, force a POST
if (Helpers::toLower(method) == "post") { // we have all lowercase
if (!content_set) {
// http->addHeader("Content-Type", value.find_first_of('{') != std::string::npos ? "application/json" : "text/plain");
if (value.starts_with('{')) {
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON
} else {
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_text_plain, false); // auto-set to JSON
}
}
httpResult = http->POST(value.c_str());
} else {
httpResult = http->GET(); // normal GET
if (httpResult > 0) {
result = http->getString().c_str();
}
}
}
http->end();
delete http;
// check HTTP return code
if (httpResult != 200) {
char error[100];
snprintf(error, sizeof(error), "Schedule %s: URL command failed with http code %d", name, httpResult);
EMSESP::logger().warning(error);
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
return false;
}
#if defined(EMSESP_DEBUG)
char msg[100];
snprintf(msg, sizeof(msg), "Schedule %s: URL command successful with http code %d", name, httpResult);
EMSESP::logger().debug(msg);
EMSESP::logger().debug("Schedule %s: URL '%s' command successful with http code %d", name, url.c_str(), httpResult);
#endif
return true;
}

View File

@@ -83,6 +83,20 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["modbus_max_clients"] = settings.modbus_max_clients;
root["modbus_timeout"] = settings.modbus_timeout;
root["developer_mode"] = settings.developer_mode;
#ifndef NO_TLS_SUPPORT
root["email_enabled"] = settings.email_enabled;
#else
root["email_enabled"] = false;
#endif
root["email_ssl"] = settings.email_ssl;
root["email_starttls"] = settings.email_starttls;
root["email_server"] = settings.email_server;
root["email_port"] = settings.email_port;
root["email_login"] = settings.email_login;
root["email_pass"] = settings.email_pass;
root["email_sender"] = settings.email_sender;
root["email_recp"] = settings.email_recp;
root["email_subject"] = settings.email_subject;
}
// call on initialization and also when settings are updated/saved via web or console
@@ -243,13 +257,9 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
// Modbus settings
settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED;
check_flag(original_settings.modbus_enabled, settings.modbus_enabled, ChangeFlags::RESTART);
settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT;
check_flag(original_settings.modbus_port, settings.modbus_port, ChangeFlags::RESTART);
settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS;
check_flag(original_settings.modbus_max_clients, settings.modbus_max_clients, ChangeFlags::RESTART);
settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT;
check_flag(original_settings.modbus_timeout, settings.modbus_timeout, ChangeFlags::RESTART);
//
// these may need mqtt restart to rebuild HA discovery topics
@@ -300,6 +310,20 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL;
settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT;
settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE;
settings.email_ssl = root["email_ssl"] | FACTORY_EMAIL_SSL;
settings.email_starttls = root["email_starttls"] | FACTORY_EMAIL_STARTTLS;
settings.email_server = root["email_server"] | FACTORY_EMAIL_SERVER;
settings.email_port = root["email_port"] | FACTORY_EMAIL_PORT;
settings.email_login = root["email_login"] | FACTORY_EMAIL_LOGIN;
settings.email_pass = root["email_pass"] | FACTORY_EMAIL_PASSWORD;
settings.email_sender = root["email_sender"] | FACTORY_EMAIL_FROM;
settings.email_recp = root["email_recp"] | FACTORY_EMAIL_TO;
settings.email_subject = root["email_subject"] | FACTORY_EMAIL_SUBJECT;
if (settings.email_ssl && settings.email_starttls) {
settings.email_ssl = false;
}
// if no psram limit weblog buffer to 25 messages
if (EMSESP::system_.PSram() > 0) {
settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER;
@@ -458,23 +482,14 @@ void WebSettings::set_board_profile(WebSettings & settings) {
#if CONFIG_IDF_TARGET_ESP32
// check for no PSRAM, could be a E32 or S32?
if (!ESP.getPsramSize()) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) {
#else
if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) {
#endif
settings.board_profile = "E32"; // Ethernet without PSRAM
} else {
settings.board_profile = "S32"; // ESP32 standard WiFi without PSRAM
}
} else {
// check for boards with PSRAM, could be a E32V2 otherwise default back to the S32
#if ESP_ARDUINO_VERSION_MAJOR < 3
if (ETH.begin(0, 15, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_OUT)) {
#else
if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) {
#endif
if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V
settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors
} else {

View File

@@ -26,6 +26,36 @@
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/settings"
#define EMSESP_BOARD_PROFILE_SERVICE_PATH "/rest/boardProfile"
#ifndef FACTORY_EMAIL_ENABLE
#define FACTORY_EMAIL_ENABLE false
#endif
#ifndef FACTORY_EMAIL_SSL
#define FACTORY_EMAIL_SSL false
#endif
#ifndef FACTORY_EMAIL_STARTTLS
#define FACTORY_EMAIL_STARTTLS true
#endif
#ifndef FACTORY_EMAIL_PORT
#define FACTORY_EMAIL_PORT 587
#endif
#ifndef FACTORY_EMAIL_SERVER
#define FACTORY_EMAIL_SERVER "smtp.example.net"
#endif
#ifndef FACTORY_EMAIL_LOGIN
#define FACTORY_EMAIL_LOGIN ""
#endif
#ifndef FACTORY_EMAIL_PASSWORD
#define FACTORY_EMAIL_PASSWORD ""
#endif
#ifndef FACTORY_EMAIL_FROM
#define FACTORY_EMAIL_FROM "ems-esp@example.net"
#endif
#ifndef FACTORY_EMAIL_TO
#define FACTORY_EMAIL_TO ""
#endif
#ifndef FACTORY_EMAIL_SUBJECT
#define FACTORY_EMAIL_SUBJECT "ems-esp notification"
#endif
namespace emsesp {
class WebSettings {
@@ -77,6 +107,16 @@ class WebSettings {
uint16_t modbus_port;
uint8_t modbus_max_clients;
uint32_t modbus_timeout;
bool email_enabled;
bool email_ssl;
bool email_starttls;
String email_server;
uint16_t email_port;
String email_login;
String email_pass;
String email_sender;
String email_recp;
String email_subject;
uint8_t phy_type;
int8_t eth_power; // -1 means disabled

View File

@@ -146,7 +146,9 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
}
// get the partition info for each partition, including the running one
// the partition data is done once in System::start() and stored in partition_info_
// the partition data is gathered once in System::start() and stored in partition_info_
// install_date is stored as a UTC epoch and formatted to local time here so it honors
// the current TZ (which may not have been set yet when System::start() ran).
JsonArray partitions = root["partitions"].to<JsonArray>();
for (const auto & partition : EMSESP::system_.partition_info_) {
// Skip partition if it has no version, or it's size is 0
@@ -157,7 +159,14 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
part["partition"] = partition.first;
part["version"] = partition.second.version;
part["size"] = partition.second.size;
part["install_date"] = partition.second.install_date;
if (partition.second.install_date > 0) {
char time_string[25];
time_t d = partition.second.install_date;
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
part["install_date"] = time_string;
} else {
part["install_date"] = "";
}
}
root["developer_mode"] = EMSESP::system_.developer_mode();
@@ -253,7 +262,7 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
// it's a filename with a .bin or .md extension, try and extract the version from it
// e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2
version::Semver200_version latest_version;
version::EMSESP_Version latest_version;
if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) {
std::string filename = version;
auto pos = filename.find("EMS-ESP-");
@@ -274,18 +283,18 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
std::string major_version = filename.substr(pos, underscore1 - pos);
std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1);
std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1);
latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version);
latest_version = version::EMSESP_Version(major_version + "." + minor_version + "." + patch_version);
} else {
// if it's .json file exit
if (version.find(".json") != std::string::npos) {
return 0;
} else {
// treat it like a version string like "3.9.0"
latest_version = version::Semver200_version(version);
latest_version = version::EMSESP_Version(version);
}
}
version::Semver200_version current_version(current_version_s); // get current version
version::EMSESP_Version current_version(current_version_s); // get current version
if (latest_version > current_version && current_version.minor() < latest_version.minor()) {
return 0; // if it's just a minor version upgrade return 0
@@ -306,9 +315,9 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
// versions holds the latest development version and stable version in one string, comma separated
bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) {
if (!version.empty()) {
version::Semver200_version current_version(current_version_s);
version::Semver200_version latest_dev_version(version.substr(0, version.find(',')));
version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1));
version::EMSESP_Version current_version(current_version_s);
version::EMSESP_Version latest_dev_version(version.substr(0, version.find(',')));
version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1));
bool dev_upgradeable = latest_dev_version > current_version;
bool stable_upgradeable = latest_stable_version > current_version;

View File

@@ -4,7 +4,7 @@
#define EMSESP_SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus"
#define EMSESP_ACTION_SERVICE_PATH "/rest/action"
#include <semver200.h> // for version checking
#include "../core/EMSESP_Version.h"
#include "../emsesp_version.h"
namespace emsesp {