feat: add SD-backed stats archive and dedicated /stats web console
Этот коммит содержится в:
+49
-42
@@ -16,6 +16,7 @@ The tables below are built from the repo's PlatformIO board metadata and variant
|
|||||||
- `RAM` is the MCU's built-in RAM.
|
- `RAM` is the MCU's built-in RAM.
|
||||||
- `PSRAM` is extra memory on some boards. More PSRAM usually means more headroom for UI, MQTT, and future features.
|
- `PSRAM` is extra memory on some boards. More PSRAM usually means more headroom for UI, MQTT, and future features.
|
||||||
- `GPS` uses `✅` when present and is blank when absent.
|
- `GPS` uses `✅` when present and is blank when absent.
|
||||||
|
- `SD` uses `✅` when the board is currently known to support the SD-backed archive path in EastMesh, `🧪` when the hardware likely supports TF/microSD but the board-specific integration still needs validation, and is blank when there is no current SD/archive support note.
|
||||||
|
|
||||||
## Start Here
|
## Start Here
|
||||||
|
|
||||||
@@ -85,51 +86,57 @@ If you are deciding between otherwise similar boards, these target-level setting
|
|||||||
|
|
||||||
This table includes all repeater MQTT targets currently defined in `variants/eastmesh_mqtt/platformio.ini`.
|
This table includes all repeater MQTT targets currently defined in `variants/eastmesh_mqtt/platformio.ini`.
|
||||||
|
|
||||||
| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS |
|
| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS | SD |
|
||||||
| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- |
|
| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- | -- |
|
||||||
| Ebyte_EoRa-S3 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | |
|
| Ebyte_EoRa-S3 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | |
|
||||||
| Generic_E22_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | None | |
|
| Generic_E22_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| Generic_E22_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | None | |
|
| Generic_E22_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | None | | |
|
||||||
| Heltec_ct62 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | |
|
| Heltec_ct62 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| Heltec_E213 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.13") | |
|
| Heltec_E213 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.13") | | |
|
||||||
| Heltec_E290 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.9") | |
|
| Heltec_E290 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.9") | | |
|
||||||
| Heltec_T190 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | TFT (ST7789) | |
|
| Heltec_T190 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | TFT (ST7789) | | |
|
||||||
| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ |
|
| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | |
|
||||||
| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | |
|
| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | | |
|
||||||
| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ |
|
| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ | |
|
||||||
| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | OLED (SSD1306) | ✅ |
|
| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | OLED (SSD1306) | ✅ | |
|
||||||
| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | TFT (ST7789) | ✅ |
|
| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | TFT (ST7789) | ✅ | |
|
||||||
| Heltec_Wireless_Paper | ESP32S3 | 512 KB | No | 8 MB | SX1262 | E-paper (2.13") | |
|
| Heltec_Wireless_Paper | ESP32S3 | 512 KB | No | 8 MB | SX1262 | E-paper (2.13") | | |
|
||||||
| Heltec_Wireless_Tracker | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ |
|
| Heltec_Wireless_Tracker | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | |
|
||||||
| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ |
|
| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | |
|
||||||
| LilyGo_T3S3_sx1262 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | |
|
| LilyGo_T3S3_sx1262 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | 🧪 |
|
||||||
| LilyGo_T3S3_sx1276 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1276 | OLED (SSD1306) | |
|
| LilyGo_T3S3_sx1276 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1276 | OLED (SSD1306) | | 🧪 |
|
||||||
| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ |
|
| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | ✅ |
|
||||||
| LilyGo_TDeck | ESP32S3 | 512 KB | No | 16 MB | SX1262 | TFT (ST7789) | ✅ |
|
| LilyGo_TDeck | ESP32S3 | 512 KB | No | 16 MB | SX1262 | TFT (ST7789) | ✅ | 🧪 |
|
||||||
| LilyGo_Tlora_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | |
|
| LilyGo_Tlora_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| LilyGo_TLora_V2_1_1_6 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ |
|
| LilyGo_TLora_V2_1_1_6 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | |
|
||||||
| M5Stack_Unit_C6L | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | ✅ |
|
| M5Stack_Unit_C6L | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | ✅ | |
|
||||||
| Meshadventurer_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ |
|
| Meshadventurer_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | |
|
||||||
| Meshadventurer_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | OLED (SSD1306) | ✅ |
|
| Meshadventurer_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | OLED (SSD1306) | ✅ | |
|
||||||
| Meshimi | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | |
|
| Meshimi | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| nibble_screen_connect | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SSD1306) | |
|
| nibble_screen_connect | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SSD1306) | | |
|
||||||
| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ |
|
| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | |
|
||||||
| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ |
|
| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | |
|
||||||
| Station_G2_logging | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ |
|
| Station_G2_logging | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | |
|
||||||
| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8 MB | SX1262 | OLED (SH1106) | ✅ |
|
| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8 MB | SX1262 | OLED (SH1106) | ✅ | ✅ |
|
||||||
| Tbeam_SX1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ |
|
| Tbeam_SX1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | |
|
||||||
| Tbeam_SX1276 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ |
|
| Tbeam_SX1276 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | |
|
||||||
| Tenstar_C3_sx1262 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | |
|
| Tenstar_C3_sx1262 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| Tenstar_C3_sx1268 | ESP32C3 | 400 KB | No | 4 MB | SX1268 | None | |
|
| Tenstar_C3_sx1268 | ESP32C3 | 400 KB | No | 4 MB | SX1268 | None | | |
|
||||||
| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SH1106) | |
|
| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SH1106) | | |
|
||||||
| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | E-paper (GxEPD) | ✅ |
|
| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | E-paper (GxEPD) | ✅ | |
|
||||||
| WHY2025_badge | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | |
|
| WHY2025_badge | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| Xiao_C3 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | ✅ |
|
| Xiao_C3 | ESP32C3 | 400 KB | No | 4 MB | SX1262 | None | ✅ | |
|
||||||
| Xiao_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | |
|
| Xiao_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | |
|
||||||
| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ |
|
| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | |
|
||||||
|
|
||||||
Note: all current `repeater_mqtt` boards support the local web panel except `Xiao_C3`, where it is disabled to preserve limited board resources.
|
Note: all current `repeater_mqtt` boards support the local web panel except `Xiao_C3`, where it is disabled to preserve limited board resources.
|
||||||
|
|
||||||
|
SD notes:
|
||||||
|
|
||||||
|
- `✅` means the board is currently known to work with the SD-backed archive path in EastMesh.
|
||||||
|
- `🧪` means the board hardware likely supports TF/microSD, but the EastMesh board-specific integration still needs validation before it should be treated as supported.
|
||||||
|
- For the current stats archive, a `4 GB` `FAT32` microSD card is more than sufficient. Larger cards are also supported.
|
||||||
|
|
||||||
## `companion_radio_wifi` Boards With A Display
|
## `companion_radio_wifi` Boards With A Display
|
||||||
|
|
||||||
These are the Wi-Fi companion targets that have a display configured for local status, setup help, or occasional direct interaction.
|
These are the Wi-Fi companion targets that have a display configured for local status, setup help, or occasional direct interaction.
|
||||||
|
|||||||
@@ -62,8 +62,11 @@ Legacy dotted aliases are also accepted:
|
|||||||
|
|
||||||
- `get web`
|
- `get web`
|
||||||
- `get web.status`: shows whether the local HTTPS panel is available.
|
- `get web.status`: shows whether the local HTTPS panel is available.
|
||||||
|
- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted.
|
||||||
- `set web on|off`
|
- `set web on|off`
|
||||||
- `set.web on|off`: enables or disables the local HTTPS panel.
|
- `set.web on|off`: enables or disables the local HTTPS panel.
|
||||||
|
- `set web.stats on|off`
|
||||||
|
- `set.web.stats on|off`: enables or disables the dedicated `/stats` page and historical stats collection.
|
||||||
|
|
||||||
### Runtime Diagnostics
|
### Runtime Diagnostics
|
||||||
|
|
||||||
@@ -87,9 +90,11 @@ That allowlist currently includes:
|
|||||||
- `get mqtt.status`
|
- `get mqtt.status`
|
||||||
- `get web`
|
- `get web`
|
||||||
- `get web.status`
|
- `get web.status`
|
||||||
|
- `get web.stats.status`
|
||||||
- `advert`
|
- `advert`
|
||||||
- `reboot`
|
- `reboot`
|
||||||
- `start ota`
|
- `start ota`
|
||||||
|
- `set repeat on|off`
|
||||||
- `memory`
|
- `memory`
|
||||||
- `stats-core`
|
- `stats-core`
|
||||||
- `stats-radio`
|
- `stats-radio`
|
||||||
@@ -117,6 +122,7 @@ That allowlist currently includes:
|
|||||||
- `get mqtt.letsmesh-us`
|
- `get mqtt.letsmesh-us`
|
||||||
- `set mqtt.letsmesh-us on|off`
|
- `set mqtt.letsmesh-us on|off`
|
||||||
- `set web on|off`
|
- `set web on|off`
|
||||||
|
- `set web.stats on|off`
|
||||||
- `get name`
|
- `get name`
|
||||||
- `set name <device-name>`
|
- `set name <device-name>`
|
||||||
- `get lat`
|
- `get lat`
|
||||||
|
|||||||
+76
-17
@@ -10,12 +10,13 @@ The repeater web panel is a local HTTPS configuration page served directly by th
|
|||||||
|
|
||||||
It gives you:
|
It gives you:
|
||||||
|
|
||||||
- a password-gated local admin page
|
- a password-gated local admin page at `/app`
|
||||||
|
- a dedicated stats and trends page at `/stats`
|
||||||
- quick `get` commands for common repeater and MQTT checks
|
- quick `get` commands for common repeater and MQTT checks
|
||||||
- a terminal-style CLI panel for allowlisted commands
|
- a terminal-style CLI panel for allowlisted commands
|
||||||
- editable repeater settings
|
- editable repeater settings
|
||||||
- editable MQTT settings
|
- editable MQTT settings
|
||||||
- a stats dashboard with Wi-Fi, core, radio, memory, and packet views
|
- a historical stats view with trends, neighbours, and recent events
|
||||||
|
|
||||||
Operational guidance:
|
Operational guidance:
|
||||||
|
|
||||||
@@ -78,18 +79,42 @@ Recommended practice for repeater deployments:
|
|||||||
- use it again for occasional checks or troubleshooting
|
- use it again for occasional checks or troubleshooting
|
||||||
- disable it with `set web off` when finished so MQTT has the most headroom available
|
- disable it with `set web off` when finished so MQTT has the most headroom available
|
||||||
|
|
||||||
## Actions
|
## Navigation And Actions
|
||||||
|
|
||||||
The Actions panel gives you the most common operational controls:
|
The web console now has two main pages:
|
||||||
|
|
||||||
|
- `/app`: lighter-weight control and configuration view
|
||||||
|
- `/stats`: current status, trends, neighbours, and recent events
|
||||||
|
|
||||||
|
Both pages share the same top navigation and utility actions.
|
||||||
|
|
||||||
|
### `/app`
|
||||||
|
|
||||||
|
The `/app` page is the main admin and configuration surface.
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
|
||||||
|
- navigation to `App` and `Stats`
|
||||||
- `Advert`
|
- `Advert`
|
||||||
- `Start OTA`
|
- `Start OTA`
|
||||||
- `Reboot`
|
- `Reboot`
|
||||||
- `Logout`
|
|
||||||
- theme toggle
|
- theme toggle
|
||||||
|
- `Logout`
|
||||||
|
|
||||||
Use `Start OTA` only when you intend to update firmware.
|
Use `Start OTA` only when you intend to update firmware.
|
||||||
|
|
||||||
|
### `/stats`
|
||||||
|
|
||||||
|
The `/stats` page is the home for current status and historical visibility.
|
||||||
|
|
||||||
|
It includes:
|
||||||
|
|
||||||
|
- navigation to `App` and `Stats`
|
||||||
|
- `Refresh`
|
||||||
|
- `Reboot`
|
||||||
|
- theme toggle
|
||||||
|
- `Logout`
|
||||||
|
|
||||||
## Quick "get" Commands
|
## Quick "get" Commands
|
||||||
|
|
||||||
This section runs common read-only commands for:
|
This section runs common read-only commands for:
|
||||||
@@ -148,19 +173,34 @@ Notes:
|
|||||||
- you can toggle each MQTT server on or off from this panel
|
- you can toggle each MQTT server on or off from this panel
|
||||||
- if all three servers are enabled at once, the panel shows a warning recommending two at most
|
- if all three servers are enabled at once, the panel shows a warning recommending two at most
|
||||||
|
|
||||||
## Stats
|
## `/stats` Overview
|
||||||
|
|
||||||
Press `Get Stats` to load the dashboard.
|
The stats page is loaded separately from `/app` and is intended to keep the main admin page lighter.
|
||||||
|
|
||||||
The stats page currently shows:
|
The `/stats` page currently shows:
|
||||||
|
|
||||||
- Core
|
- `Services`: MQTT, web, archive, card, neighbour count, and archive capacity
|
||||||
- Radio
|
- `Trends`: battery, heap free, packet activity, and signal
|
||||||
- Memory
|
- `Neighbours`: current neighbour table
|
||||||
- Wi-Fi
|
- `Events`: current boot/session events
|
||||||
- Packets
|
|
||||||
|
|
||||||
The dashboard is designed to work well on phones as well as desktop browsers.
|
The trend graphs load sequentially rather than as one large payload:
|
||||||
|
|
||||||
|
1. summary/status
|
||||||
|
2. battery
|
||||||
|
3. memory
|
||||||
|
4. packet activity
|
||||||
|
5. signal
|
||||||
|
|
||||||
|
This keeps browser-side and device-side memory use lower than the previous in-page stats view.
|
||||||
|
|
||||||
|
If `web.stats` is enabled and an SD archive is mounted, trends can restore archived summary points after reboot. Recent live points are still added from in-memory history.
|
||||||
|
|
||||||
|
Useful CLI commands:
|
||||||
|
|
||||||
|
- `set web.stats on`
|
||||||
|
- `set web.stats off`
|
||||||
|
- `get web.stats.status`
|
||||||
|
|
||||||
## Mobile Use
|
## Mobile Use
|
||||||
|
|
||||||
@@ -169,9 +209,9 @@ The page is responsive and should work cleanly on a phone.
|
|||||||
On mobile:
|
On mobile:
|
||||||
|
|
||||||
- quick command buttons collapse into a two-column layout
|
- quick command buttons collapse into a two-column layout
|
||||||
- action buttons stack more cleanly
|
- top navigation and action groups stay compact and touch-friendly
|
||||||
- input rows stay usable for touch interaction
|
- input rows stay usable for touch interaction
|
||||||
- stats cards reorganize into single-column sections where needed
|
- trend cards reorganize into single-column sections where needed
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
@@ -180,7 +220,7 @@ On mobile:
|
|||||||
1. Open the panel.
|
1. Open the panel.
|
||||||
2. Press `wifi.status` in Quick `get` Commands.
|
2. Press `wifi.status` in Quick `get` Commands.
|
||||||
3. Press `mqtt.status` in Quick `get` Commands.
|
3. Press `mqtt.status` in Quick `get` Commands.
|
||||||
4. Press `Get Stats` for the dashboard view.
|
4. Open `/stats` from the top navigation for the historical stats view.
|
||||||
|
|
||||||
### Change Device Name
|
### Change Device Name
|
||||||
|
|
||||||
@@ -201,6 +241,14 @@ On mobile:
|
|||||||
2. Confirm the action.
|
2. Confirm the action.
|
||||||
3. Continue with your normal OTA workflow.
|
3. Continue with your normal OTA workflow.
|
||||||
|
|
||||||
|
### Use Historical Stats
|
||||||
|
|
||||||
|
1. Enable stats if needed with `set web.stats on`.
|
||||||
|
2. Open `/stats` from the top navigation.
|
||||||
|
3. Review `Services` for archive and runtime state.
|
||||||
|
4. Review `Trends` for recent graph history.
|
||||||
|
5. Use `Refresh` to reload the stats page.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### The browser warns about the certificate
|
### The browser warns about the certificate
|
||||||
@@ -241,9 +289,20 @@ The panel intentionally limits what can be run from the browser. Use the serial
|
|||||||
Try:
|
Try:
|
||||||
|
|
||||||
- refreshing the browser tab
|
- refreshing the browser tab
|
||||||
|
- using `Refresh` on `/stats`
|
||||||
- logging out and back in
|
- logging out and back in
|
||||||
- checking WiFi stability with `get wifi.status`
|
- checking WiFi stability with `get wifi.status`
|
||||||
|
|
||||||
|
### `/stats` is unavailable
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
- `get web.status`
|
||||||
|
- `get web.stats.status`
|
||||||
|
- whether `set web.stats on` has been applied
|
||||||
|
|
||||||
|
If `web.stats` is off, `/stats` will stay disabled and the historical graph requests will not run.
|
||||||
|
|
||||||
## Related Docs
|
## Related Docs
|
||||||
|
|
||||||
- [Custom CLI Commands](./custom-cli.md)
|
- [Custom CLI Commands](./custom-cli.md)
|
||||||
|
|||||||
@@ -1,6 +1,129 @@
|
|||||||
#include "MyMesh.h"
|
#include "MyMesh.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#if defined(ESP32) && WITH_WEB_PANEL
|
||||||
|
#include <WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ARCHIVE_DEBUG
|
||||||
|
#if defined(MQTT_DEBUG) && MQTT_DEBUG
|
||||||
|
#define ARCHIVE_DEBUG 1
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_DEBUG 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ARCHIVE_DEBUG
|
||||||
|
#define ARCHIVE_LOG(fmt, ...) Serial.printf("[ARCHIVE] " fmt "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_LOG(...) do { } while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr unsigned long kArchiveNeighboursFlushIntervalMs = 60UL * 1000UL;
|
||||||
|
constexpr const char* kArchiveNeighboursSnapshotPath = "/stats/neighbours.snapshot";
|
||||||
|
|
||||||
|
File openArchiveWrite(FILESYSTEM* fs, const char* filename) {
|
||||||
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||||
|
fs->remove(filename);
|
||||||
|
return fs->open(filename, FILE_O_WRITE);
|
||||||
|
#elif defined(RP2040_PLATFORM)
|
||||||
|
return fs->open(filename, "w");
|
||||||
|
#else
|
||||||
|
fs->remove(filename);
|
||||||
|
return fs->open(filename, FILE_WRITE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveRead(FILESYSTEM* fs, const char* filename) {
|
||||||
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||||
|
return fs->open(filename, "r");
|
||||||
|
#else
|
||||||
|
return fs->open(filename, FILE_READ);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveWriteWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||||
|
if (archive == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
File file = openArchiveWrite(fs, filename);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (!archive->recover()) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
fs = archive->getFS();
|
||||||
|
return fs != nullptr ? openArchiveWrite(fs, filename) : File();
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveReadWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||||
|
if (archive == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
File file = openArchiveRead(fs, filename);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (!archive->recover()) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
fs = archive->getFS();
|
||||||
|
return fs != nullptr ? openArchiveRead(fs, filename) : File();
|
||||||
|
}
|
||||||
|
|
||||||
|
void escapeJsonString(const char* input, char* output, size_t output_size) {
|
||||||
|
if (output == nullptr || output_size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t oi = 0;
|
||||||
|
for (size_t i = 0; input != nullptr && input[i] != 0 && oi + 1 < output_size; ++i) {
|
||||||
|
const char c = input[i];
|
||||||
|
const char* escape = nullptr;
|
||||||
|
switch (c) {
|
||||||
|
case '\\':
|
||||||
|
escape = "\\\\";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
escape = "\\\"";
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
escape = "\\n";
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
escape = "\\r";
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
escape = "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escape != nullptr) {
|
||||||
|
while (*escape != 0 && oi + 1 < output_size) {
|
||||||
|
output[oi++] = *escape++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output[oi++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output[oi] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/* ------------------------------ Config -------------------------------- */
|
/* ------------------------------ Config -------------------------------- */
|
||||||
|
|
||||||
#ifndef LORA_FREQ
|
#ifndef LORA_FREQ
|
||||||
@@ -84,6 +207,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
|
|||||||
neighbour->advert_timestamp = timestamp;
|
neighbour->advert_timestamp = timestamp;
|
||||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||||
neighbour->snr = (int8_t)(snr * 4);
|
neighbour->snr = (int8_t)(snr * 4);
|
||||||
|
_archive_neighbours_dirty = true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,12 +976,17 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
|||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
last_millis = 0;
|
last_millis = 0;
|
||||||
|
_archive = nullptr;
|
||||||
uptime_millis = 0;
|
uptime_millis = 0;
|
||||||
|
next_archive_neighbours_flush_ms = 0;
|
||||||
|
next_history_sample_ms = 0;
|
||||||
next_local_advert = next_flood_advert = 0;
|
next_local_advert = next_flood_advert = 0;
|
||||||
dirty_contacts_expiry = 0;
|
dirty_contacts_expiry = 0;
|
||||||
set_radio_at = revert_radio_at = 0;
|
set_radio_at = revert_radio_at = 0;
|
||||||
_logging = false;
|
_logging = false;
|
||||||
|
_archive_neighbours_dirty = false;
|
||||||
region_load_active = false;
|
region_load_active = false;
|
||||||
|
memset(&_stats_state, 0, sizeof(_stats_state));
|
||||||
|
|
||||||
#if MAX_NEIGHBOURS
|
#if MAX_NEIGHBOURS
|
||||||
memset(neighbours, 0, sizeof(neighbours));
|
memset(neighbours, 0, sizeof(neighbours));
|
||||||
@@ -912,9 +1041,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
|||||||
pending_discover_until = 0;
|
pending_discover_until = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::begin(FILESYSTEM *fs) {
|
void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
|
||||||
mesh::Mesh::begin();
|
mesh::Mesh::begin();
|
||||||
_fs = fs;
|
_fs = fs;
|
||||||
|
_archive = archive;
|
||||||
|
last_millis = millis();
|
||||||
// load persisted prefs
|
// load persisted prefs
|
||||||
_cli.loadPrefs(_fs);
|
_cli.loadPrefs(_fs);
|
||||||
acl.load(_fs, self_id);
|
acl.load(_fs, self_id);
|
||||||
@@ -926,10 +1057,45 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
|||||||
bridge.begin();
|
bridge.begin();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if defined(WITH_MQTT_UPLINK)
|
#if defined(ESP_PLATFORM)
|
||||||
|
uint8_t legacy_wifi_powersave = 0;
|
||||||
|
const char* legacy_wifi_ssid = nullptr;
|
||||||
|
const char* legacy_wifi_pwd = nullptr;
|
||||||
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
MQTTPrefs legacy_mqtt_prefs{};
|
||||||
|
MQTTPrefsStore::setDefaults(legacy_mqtt_prefs);
|
||||||
|
MQTTPrefsStore::load(_fs, legacy_mqtt_prefs);
|
||||||
|
legacy_wifi_powersave = legacy_mqtt_prefs.legacy_wifi_powersave;
|
||||||
|
legacy_wifi_ssid = legacy_mqtt_prefs.legacy_wifi_ssid;
|
||||||
|
legacy_wifi_pwd = legacy_mqtt_prefs.legacy_wifi_pwd;
|
||||||
|
#endif
|
||||||
|
network.begin(_fs, legacy_wifi_powersave, legacy_wifi_ssid, legacy_wifi_pwd);
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
board.setInhibitSleep(true);
|
board.setInhibitSleep(true);
|
||||||
|
web.setCommandRunner(this);
|
||||||
|
web.setNetworkStateProvider(&network);
|
||||||
|
web.begin(_fs);
|
||||||
|
_stats_history.begin(web.isWebStatsEnabled(), _archive);
|
||||||
|
if (web.isWebStatsEnabled() && _archive != nullptr && _archive->isMounted()) {
|
||||||
|
restoreArchiveNeighbours();
|
||||||
|
next_archive_neighbours_flush_ms = millis() + kArchiveNeighboursFlushIntervalMs;
|
||||||
|
}
|
||||||
|
if (web.isWebStatsEnabled()) {
|
||||||
|
recordStatsEvent(HISTORY_EVENT_BOOT);
|
||||||
|
if (_archive != nullptr) {
|
||||||
|
recordStatsEvent(_archive->isMounted() ? HISTORY_EVENT_ARCHIVE_MOUNTED : HISTORY_EVENT_ARCHIVE_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if defined(WITH_MQTT_UPLINK) && !(defined(ESP_PLATFORM) && WITH_WEB_PANEL)
|
||||||
|
board.setInhibitSleep(true);
|
||||||
|
#endif
|
||||||
|
#ifdef WITH_MQTT_UPLINK
|
||||||
mqtt.setNodeNameSource(_prefs.node_name);
|
mqtt.setNodeNameSource(_prefs.node_name);
|
||||||
mqtt.setWebCommandRunner(this);
|
#if defined(ESP_PLATFORM)
|
||||||
|
mqtt.setNetworkStateProvider(&network);
|
||||||
|
#endif
|
||||||
mqtt.begin(_fs);
|
mqtt.begin(_fs);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -949,6 +1115,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
|||||||
#if ENV_INCLUDE_GPS == 1
|
#if ENV_INCLUDE_GPS == 1
|
||||||
applyGpsPrefs();
|
applyGpsPrefs();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
next_history_sample_ms = futureMillis(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
||||||
@@ -1079,6 +1247,7 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
|
|||||||
NeighbourInfo *neighbour = &neighbours[i];
|
NeighbourInfo *neighbour = &neighbours[i];
|
||||||
if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) {
|
if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) {
|
||||||
neighbours[i] = NeighbourInfo(); // clear neighbour entry
|
neighbours[i] = NeighbourInfo(); // clear neighbour entry
|
||||||
|
_archive_neighbours_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1101,6 +1270,370 @@ void MyMesh::formatMemoryReply(char *reply, size_t reply_size) {
|
|||||||
StatsFormatHelper::formatMemoryStats(reply, reply_size);
|
StatsFormatHelper::formatMemoryStats(reply, reply_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t MyMesh::getNeighbourCount() const {
|
||||||
|
#if MAX_NEIGHBOURS
|
||||||
|
size_t count = 0;
|
||||||
|
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||||
|
if (neighbours[i].heard_timestamp > 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyMesh::restoreArchiveNeighbours() {
|
||||||
|
#if MAX_NEIGHBOURS
|
||||||
|
if (_archive == nullptr || !_archive->isMounted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr || !fs->exists(kArchiveNeighboursSnapshotPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveReadWithRecovery(_archive, kArchiveNeighboursSnapshotPath);
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("neighbours restore open failed path=%s", kArchiveNeighboursSnapshotPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(neighbours, 0, sizeof(neighbours));
|
||||||
|
char line[128];
|
||||||
|
size_t line_len = 0;
|
||||||
|
size_t restored = 0;
|
||||||
|
while (file.available()) {
|
||||||
|
const int raw = file.read();
|
||||||
|
if (raw < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char ch = static_cast<char>(raw);
|
||||||
|
if (ch == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\n') {
|
||||||
|
line[line_len] = 0;
|
||||||
|
if (line_len > 0 && restored < MAX_NEIGHBOURS) {
|
||||||
|
char full_hex[65];
|
||||||
|
unsigned long advert_timestamp = 0;
|
||||||
|
unsigned long heard_timestamp = 0;
|
||||||
|
int snr = 0;
|
||||||
|
memset(full_hex, 0, sizeof(full_hex));
|
||||||
|
if (sscanf(line, "%64[^,],%lu,%lu,%d", full_hex, &advert_timestamp, &heard_timestamp, &snr) == 4) {
|
||||||
|
uint8_t pub_key[PUB_KEY_SIZE];
|
||||||
|
if (mesh::Utils::fromHex(pub_key, PUB_KEY_SIZE, full_hex)) {
|
||||||
|
neighbours[restored].id = mesh::Identity(pub_key);
|
||||||
|
neighbours[restored].advert_timestamp = static_cast<uint32_t>(advert_timestamp);
|
||||||
|
neighbours[restored].heard_timestamp = static_cast<uint32_t>(heard_timestamp);
|
||||||
|
neighbours[restored].snr = static_cast<int8_t>(constrain(snr, -128, 127));
|
||||||
|
restored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line_len = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line_len + 1 < sizeof(line)) {
|
||||||
|
line[line_len++] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line_len > 0 && restored < MAX_NEIGHBOURS) {
|
||||||
|
line[line_len] = 0;
|
||||||
|
char full_hex[65];
|
||||||
|
unsigned long advert_timestamp = 0;
|
||||||
|
unsigned long heard_timestamp = 0;
|
||||||
|
int snr = 0;
|
||||||
|
memset(full_hex, 0, sizeof(full_hex));
|
||||||
|
if (sscanf(line, "%64[^,],%lu,%lu,%d", full_hex, &advert_timestamp, &heard_timestamp, &snr) == 4) {
|
||||||
|
uint8_t pub_key[PUB_KEY_SIZE];
|
||||||
|
if (mesh::Utils::fromHex(pub_key, PUB_KEY_SIZE, full_hex)) {
|
||||||
|
neighbours[restored].id = mesh::Identity(pub_key);
|
||||||
|
neighbours[restored].advert_timestamp = static_cast<uint32_t>(advert_timestamp);
|
||||||
|
neighbours[restored].heard_timestamp = static_cast<uint32_t>(heard_timestamp);
|
||||||
|
neighbours[restored].snr = static_cast<int8_t>(constrain(snr, -128, 127));
|
||||||
|
restored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
_archive_neighbours_dirty = false;
|
||||||
|
return restored > 0;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyMesh::flushArchiveNeighbours() {
|
||||||
|
#if MAX_NEIGHBOURS
|
||||||
|
if (_archive == nullptr || !_archive->isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t neighbours_count = 0;
|
||||||
|
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||||
|
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||||
|
if (neighbours[i].heard_timestamp > 0) {
|
||||||
|
sorted_neighbours[neighbours_count++] = &neighbours[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||||
|
return a->heard_timestamp > b->heard_timestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
File file = openArchiveWriteWithRecovery(_archive, kArchiveNeighboursSnapshotPath);
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("neighbours open failed path=%s", kArchiveNeighboursSnapshotPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total_written = 0;
|
||||||
|
for (int i = 0; i < neighbours_count; ++i) {
|
||||||
|
char full_hex[65];
|
||||||
|
mesh::Utils::toHex(full_hex, sorted_neighbours[i]->id.pub_key, PUB_KEY_SIZE);
|
||||||
|
total_written += file.printf("%s,%lu,%lu,%d\n",
|
||||||
|
full_hex,
|
||||||
|
static_cast<unsigned long>(sorted_neighbours[i]->advert_timestamp),
|
||||||
|
static_cast<unsigned long>(sorted_neighbours[i]->heard_timestamp),
|
||||||
|
static_cast<int>(sorted_neighbours[i]->snr));
|
||||||
|
}
|
||||||
|
file.flush();
|
||||||
|
file.close();
|
||||||
|
ARCHIVE_LOG("neighbours flushed path=%s bytes=%u count=%d",
|
||||||
|
kArchiveNeighboursSnapshotPath,
|
||||||
|
static_cast<unsigned>(total_written),
|
||||||
|
static_cast<int>(neighbours_count));
|
||||||
|
_archive_neighbours_dirty = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyMesh::maybeFlushArchiveNeighbours(unsigned long now_ms) {
|
||||||
|
#if MAX_NEIGHBOURS
|
||||||
|
if (!_archive_neighbours_dirty || _archive == nullptr || !_archive->isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (next_archive_neighbours_flush_ms == 0 || millisHasNowPassed(next_archive_neighbours_flush_ms)) {
|
||||||
|
flushArchiveNeighbours();
|
||||||
|
next_archive_neighbours_flush_ms = now_ms + kArchiveNeighboursFlushIntervalMs;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)now_ms;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyMesh::recordStatsEvent(uint8_t type, int16_t value) {
|
||||||
|
_stats_history.recordEvent(type, getRTCClock()->getCurrentTime(), static_cast<uint32_t>(uptime_millis / 1000), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyMesh::updateStatsHistory(unsigned long now_ms) {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
constexpr uint32_t kLowMemoryEnterBytes = 32UL * 1024UL;
|
||||||
|
constexpr uint32_t kLowMemoryClearBytes = 48UL * 1024UL;
|
||||||
|
constexpr uint32_t kLowMemoryEventCooldownSecs = 5UL * 60UL;
|
||||||
|
_stats_history.setArchive(_archive);
|
||||||
|
_stats_history.setEnabled(web.isWebStatsEnabled());
|
||||||
|
if (!_stats_history.isEnabled()) {
|
||||||
|
_stats_state.initialized = false;
|
||||||
|
_archive_neighbours_dirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool wifi_connected = network.isWifiConnected();
|
||||||
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
|
||||||
|
#else
|
||||||
|
const bool mqtt_connected = false;
|
||||||
|
#endif
|
||||||
|
const bool web_panel_up = web.isPanelRunning();
|
||||||
|
const bool archive_mounted = (_archive != nullptr) && _archive->isMounted();
|
||||||
|
#if defined(ESP32)
|
||||||
|
const uint32_t free_heap = ESP.getFreeHeap();
|
||||||
|
const uint32_t uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||||
|
bool low_memory = _stats_state.low_memory;
|
||||||
|
if (!_stats_state.initialized) {
|
||||||
|
low_memory = free_heap <= kLowMemoryEnterBytes;
|
||||||
|
} else if (low_memory) {
|
||||||
|
low_memory = free_heap <= kLowMemoryClearBytes;
|
||||||
|
} else {
|
||||||
|
low_memory = free_heap <= kLowMemoryEnterBytes;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
const bool low_memory = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!_stats_state.initialized) {
|
||||||
|
_stats_state.initialized = true;
|
||||||
|
_stats_state.wifi_connected = wifi_connected;
|
||||||
|
_stats_state.mqtt_connected = mqtt_connected;
|
||||||
|
_stats_state.web_panel_up = web_panel_up;
|
||||||
|
_stats_state.archive_mounted = archive_mounted;
|
||||||
|
_stats_state.low_memory = low_memory;
|
||||||
|
_stats_state.last_low_memory_event_uptime_secs = 0;
|
||||||
|
} else {
|
||||||
|
if (_stats_state.mqtt_connected != mqtt_connected) {
|
||||||
|
recordStatsEvent(mqtt_connected ? HISTORY_EVENT_MQTT_CONNECTED : HISTORY_EVENT_MQTT_DISCONNECTED);
|
||||||
|
_stats_state.mqtt_connected = mqtt_connected;
|
||||||
|
}
|
||||||
|
if (_stats_state.web_panel_up != web_panel_up) {
|
||||||
|
recordStatsEvent(web_panel_up ? HISTORY_EVENT_WEB_STARTED : HISTORY_EVENT_WEB_STOPPED);
|
||||||
|
_stats_state.web_panel_up = web_panel_up;
|
||||||
|
}
|
||||||
|
if (_stats_state.archive_mounted != archive_mounted) {
|
||||||
|
recordStatsEvent(archive_mounted ? HISTORY_EVENT_ARCHIVE_MOUNTED : HISTORY_EVENT_ARCHIVE_UNAVAILABLE);
|
||||||
|
_stats_state.archive_mounted = archive_mounted;
|
||||||
|
if (archive_mounted) {
|
||||||
|
if (getNeighbourCount() == 0) {
|
||||||
|
restoreArchiveNeighbours();
|
||||||
|
}
|
||||||
|
next_archive_neighbours_flush_ms = now_ms + kArchiveNeighboursFlushIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_stats_state.low_memory && low_memory) {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (_stats_state.last_low_memory_event_uptime_secs == 0 ||
|
||||||
|
(uptime_secs - _stats_state.last_low_memory_event_uptime_secs) >= kLowMemoryEventCooldownSecs) {
|
||||||
|
recordStatsEvent(HISTORY_EVENT_LOW_MEMORY, static_cast<int16_t>(min<uint32_t>(free_heap / 1024, 32767)));
|
||||||
|
_stats_state.last_low_memory_event_uptime_secs = uptime_secs;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
_stats_state.wifi_connected = wifi_connected;
|
||||||
|
_stats_state.low_memory = low_memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next_history_sample_ms == 0 || millisHasNowPassed(next_history_sample_ms)) {
|
||||||
|
HistorySample sample{};
|
||||||
|
sample.epoch_secs = getRTCClock()->getCurrentTime();
|
||||||
|
sample.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||||
|
sample.packets_sent = radio_driver.getPacketsSent();
|
||||||
|
sample.packets_recv = radio_driver.getPacketsRecv();
|
||||||
|
sample.battery_mv = board.getBattMilliVolts();
|
||||||
|
sample.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
|
||||||
|
sample.error_flags = _err_flags;
|
||||||
|
sample.recv_errors = static_cast<uint16_t>(min<uint32_t>(radio_driver.getPacketsRecvErrors(), 0xFFFF));
|
||||||
|
sample.neighbour_count = static_cast<uint16_t>(min<size_t>(getNeighbourCount(), 0xFFFF));
|
||||||
|
sample.direct_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF));
|
||||||
|
sample.flood_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF));
|
||||||
|
sample.last_rssi_x4 = static_cast<int16_t>(radio_driver.getLastRSSI() * 4.0f);
|
||||||
|
sample.last_snr_x4 = static_cast<int16_t>(radio_driver.getLastSNR() * 4.0f);
|
||||||
|
sample.noise_floor = static_cast<int16_t>(_radio->getNoiseFloor());
|
||||||
|
sample.battery_pct = static_cast<int8_t>(board.getBatteryPercent());
|
||||||
|
#if defined(ESP32)
|
||||||
|
sample.heap_free = ESP.getFreeHeap();
|
||||||
|
sample.heap_min = ESP.getMinFreeHeap();
|
||||||
|
sample.psram_free = ESP.getFreePsram();
|
||||||
|
sample.psram_min = ESP.getMinFreePsram();
|
||||||
|
#endif
|
||||||
|
if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER;
|
||||||
|
if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING;
|
||||||
|
if (board.isVbusPresent()) sample.flags |= HISTORY_FLAG_VBUS;
|
||||||
|
if (wifi_connected) sample.flags |= HISTORY_FLAG_WIFI_CONNECTED;
|
||||||
|
if (mqtt_connected) sample.flags |= HISTORY_FLAG_MQTT_CONNECTED;
|
||||||
|
if (web.isWebEnabled()) sample.flags |= HISTORY_FLAG_WEB_ENABLED;
|
||||||
|
if (web_panel_up) sample.flags |= HISTORY_FLAG_WEB_PANEL_UP;
|
||||||
|
if (archive_mounted) sample.flags |= HISTORY_FLAG_ARCHIVE_MOUNTED;
|
||||||
|
_stats_history.pushSample(sample);
|
||||||
|
next_history_sample_ms = now_ms + 60000UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats_history.maybeFlush(now_ms);
|
||||||
|
maybeFlushArchiveNeighbours(now_ms);
|
||||||
|
#else
|
||||||
|
(void)now_ms;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyMesh::appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const {
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, "\"events\":[");
|
||||||
|
const size_t max_events = min<size_t>(_stats_history.getEventCount(), 6);
|
||||||
|
const uint32_t now_epoch_secs = getRTCClock()->getCurrentTime();
|
||||||
|
const uint32_t now_uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||||
|
for (size_t i = 0; i < max_events; ++i) {
|
||||||
|
HistoryEvent event{};
|
||||||
|
if (!_stats_history.getRecentEvent(i, event)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const uint32_t age_secs = (now_epoch_secs >= event.epoch_secs && event.epoch_secs > 0)
|
||||||
|
? (now_epoch_secs - event.epoch_secs)
|
||||||
|
: ((now_uptime_secs >= event.uptime_secs) ? (now_uptime_secs - event.uptime_secs) : event.uptime_secs);
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset,
|
||||||
|
"%s{\"t\":%lu,\"type\":\"%s\",\"value\":%d}",
|
||||||
|
i == 0 ? "" : ",",
|
||||||
|
static_cast<unsigned long>(age_secs),
|
||||||
|
StatsHistory::getEventTypeName(event.type),
|
||||||
|
static_cast<int>(event.value));
|
||||||
|
if (offset >= reply_size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, "]");
|
||||||
|
return offset < reply_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyMesh::appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const {
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, "\"neighbors_detail\":[");
|
||||||
|
if (offset >= reply_size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MAX_NEIGHBOURS
|
||||||
|
constexpr size_t kMaxNeighboursJson = 10;
|
||||||
|
int16_t neighbours_count = 0;
|
||||||
|
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||||
|
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||||
|
auto neighbour = const_cast<NeighbourInfo*>(&neighbours[i]);
|
||||||
|
if (neighbour->heard_timestamp > 0) {
|
||||||
|
sorted_neighbours[neighbours_count++] = neighbour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||||
|
return a->heard_timestamp > b->heard_timestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
const size_t emit_count = min<size_t>(neighbours_count, kMaxNeighboursJson);
|
||||||
|
const uint32_t now_secs = getRTCClock()->getCurrentTime();
|
||||||
|
for (size_t i = 0; i < emit_count; ++i) {
|
||||||
|
const NeighbourInfo* neighbour = sorted_neighbours[i];
|
||||||
|
char hex[7];
|
||||||
|
char full_hex[65];
|
||||||
|
mesh::Utils::toHex(hex, neighbour->id.pub_key, 3);
|
||||||
|
mesh::Utils::toHex(full_hex, neighbour->id.pub_key, PUB_KEY_SIZE);
|
||||||
|
const uint32_t heard_secs_ago = now_secs - neighbour->heard_timestamp;
|
||||||
|
const uint32_t advert_secs_ago = now_secs - neighbour->advert_timestamp;
|
||||||
|
ClientInfo* client = const_cast<ClientACL&>(acl).getClient(neighbour->id.pub_key, PUB_KEY_SIZE);
|
||||||
|
const bool route_known = (client != nullptr && client->out_path_len != OUT_PATH_UNKNOWN);
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset,
|
||||||
|
"%s{\"id\":\"%s\",\"full_id\":\"%s\",\"heard_secs_ago\":%lu,\"advert_secs_ago\":%lu,\"snr_db\":%.2f,\"route\":\"%s\"}",
|
||||||
|
i == 0 ? "" : ",",
|
||||||
|
hex,
|
||||||
|
full_hex,
|
||||||
|
static_cast<unsigned long>(heard_secs_ago),
|
||||||
|
static_cast<unsigned long>(advert_secs_ago),
|
||||||
|
static_cast<double>(neighbour->snr) / 4.0,
|
||||||
|
route_known ? "known" : "unknown");
|
||||||
|
if (offset >= reply_size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, "]");
|
||||||
|
return offset < reply_size;
|
||||||
|
}
|
||||||
|
|
||||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||||
IdentityStore store(*_fs, "");
|
IdentityStore store(*_fs, "");
|
||||||
@@ -1307,6 +1840,75 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
|||||||
sendNodeDiscoverReq();
|
sendNodeDiscoverReq();
|
||||||
strcpy(reply, "OK - Discover sent");
|
strcpy(reply, "OK - Discover sent");
|
||||||
}
|
}
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
|
||||||
|
web.formatWebStatusReply(reply, 160);
|
||||||
|
} else if (strcmp(command, "get web.stats.status") == 0) {
|
||||||
|
snprintf(reply, 160,
|
||||||
|
"> enabled:%s history:%s psram:%s degraded:%s samples:%u/%u events:%u/%u archive:%s logical:%s path:%s",
|
||||||
|
web.isWebStatsEnabled() ? "on" : "off",
|
||||||
|
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "active" : "inactive",
|
||||||
|
_stats_history.isPsramBacked() ? "yes" : "no",
|
||||||
|
_stats_history.isDegraded() ? "yes" : "no",
|
||||||
|
static_cast<unsigned>(_stats_history.getSampleCount()),
|
||||||
|
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
||||||
|
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||||
|
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||||
|
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable",
|
||||||
|
(_archive != nullptr) ? _archive->getLogicalName() : "archive",
|
||||||
|
(_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats");
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
||||||
|
network.formatWifiStatusReply(reply, 160);
|
||||||
|
} else if (memcmp(command, "get wifi.ssid", 13) == 0) {
|
||||||
|
sprintf(reply, "> %s", network.getWifiSSID()[0] ? network.getWifiSSID() : "-");
|
||||||
|
} else if (memcmp(command, "get wifi.powersaving", 20) == 0) {
|
||||||
|
sprintf(reply, "> %s", network.getWifiPowerSave());
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
} else if (memcmp(command, "set web ", 8) == 0) {
|
||||||
|
web.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else if (memcmp(command, "set.web ", 8) == 0) {
|
||||||
|
web.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else if (memcmp(command, "set web.stats ", 14) == 0 || memcmp(command, "set.web.stats ", 15) == 0) {
|
||||||
|
const char* value = (memcmp(command, "set web.stats ", 14) == 0) ? &command[14] : &command[15];
|
||||||
|
const bool enabled = memcmp(value, "on", 2) == 0;
|
||||||
|
if (web.setWebStatsEnabled(enabled)) {
|
||||||
|
_stats_history.setEnabled(enabled);
|
||||||
|
recordStatsEvent(enabled ? HISTORY_EVENT_STATS_ENABLED : HISTORY_EVENT_STATS_DISABLED);
|
||||||
|
if (enabled) {
|
||||||
|
next_history_sample_ms = millis();
|
||||||
|
} else {
|
||||||
|
_stats_state.initialized = false;
|
||||||
|
}
|
||||||
|
strcpy(reply, enabled ? "OK - web.stats on" : "OK - web.stats off");
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "Err - unable to update web.stats");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
} else if (memcmp(command, "set wifi.ssid ", 14) == 0) {
|
||||||
|
if (network.setWifiSSID(&command[14])) {
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "Err - bad wifi.ssid");
|
||||||
|
}
|
||||||
|
} else if (memcmp(command, "set wifi.pwd ", 13) == 0) {
|
||||||
|
if (network.setWifiPassword(&command[13])) {
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "Err - bad wifi.pwd");
|
||||||
|
}
|
||||||
|
} else if (memcmp(command, "set wifi.powersaving ", 21) == 0) {
|
||||||
|
if (network.setWifiPowerSave(&command[21])) {
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "Err - use none|min|max");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
} else if (memcmp(command, "mqtt.owner ", 11) == 0) {
|
} else if (memcmp(command, "mqtt.owner ", 11) == 0) {
|
||||||
if (mqtt.setOwnerPublicKey(&command[11])) {
|
if (mqtt.setOwnerPublicKey(&command[11])) {
|
||||||
@@ -1330,14 +1932,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
|||||||
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
|
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
|
||||||
} else if (strcmp(command, "get mqtt.status") == 0) {
|
} else if (strcmp(command, "get mqtt.status") == 0) {
|
||||||
mqtt.formatStatusReply(reply, 160);
|
mqtt.formatStatusReply(reply, 160);
|
||||||
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
|
|
||||||
mqtt.formatWebStatusReply(reply, 160);
|
|
||||||
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
|
||||||
mqtt.formatWifiStatusReply(reply, 160);
|
|
||||||
} else if (memcmp(command, "get wifi.ssid", 13) == 0) {
|
|
||||||
sprintf(reply, "> %s", mqtt.getWifiSSID()[0] ? mqtt.getWifiSSID() : "-");
|
|
||||||
} else if (memcmp(command, "get wifi.powersaving", 20) == 0) {
|
|
||||||
sprintf(reply, "> %s", mqtt.getWifiPowerSave());
|
|
||||||
} else if (memcmp(command, "get mqtt.iata", 13) == 0) {
|
} else if (memcmp(command, "get mqtt.iata", 13) == 0) {
|
||||||
sprintf(reply, "> %s", mqtt.getIata());
|
sprintf(reply, "> %s", mqtt.getIata());
|
||||||
} else if (memcmp(command, "get mqtt.owner", 14) == 0) {
|
} else if (memcmp(command, "get mqtt.owner", 14) == 0) {
|
||||||
@@ -1359,30 +1953,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
|||||||
} else if (memcmp(command, "set mqtt.tx ", 12) == 0) {
|
} else if (memcmp(command, "set mqtt.tx ", 12) == 0) {
|
||||||
mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0);
|
mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0);
|
||||||
strcpy(reply, "OK");
|
strcpy(reply, "OK");
|
||||||
} else if (memcmp(command, "set web ", 8) == 0) {
|
|
||||||
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else if (memcmp(command, "set.web ", 8) == 0) {
|
|
||||||
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else if (memcmp(command, "set wifi.ssid ", 14) == 0) {
|
|
||||||
if (mqtt.setWifiSSID(&command[14])) {
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else {
|
|
||||||
strcpy(reply, "Err - bad wifi.ssid");
|
|
||||||
}
|
|
||||||
} else if (memcmp(command, "set wifi.pwd ", 13) == 0) {
|
|
||||||
if (mqtt.setWifiPassword(&command[13])) {
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else {
|
|
||||||
strcpy(reply, "Err - bad wifi.pwd");
|
|
||||||
}
|
|
||||||
} else if (memcmp(command, "set wifi.powersaving ", 21) == 0) {
|
|
||||||
if (mqtt.setWifiPowerSave(&command[21])) {
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else {
|
|
||||||
strcpy(reply, "Err - use none|min|max");
|
|
||||||
}
|
|
||||||
} else if (memcmp(command, "set mqtt.iata ", 14) == 0) {
|
} else if (memcmp(command, "set mqtt.iata ", 14) == 0) {
|
||||||
if (mqtt.setIata(&command[14])) {
|
if (mqtt.setIata(&command[14])) {
|
||||||
strcpy(reply, "OK");
|
strcpy(reply, "OK");
|
||||||
@@ -1457,6 +2027,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
|||||||
matches_exact("clock") ||
|
matches_exact("clock") ||
|
||||||
matches_exact("get mqtt.status") ||
|
matches_exact("get mqtt.status") ||
|
||||||
matches_exact("get web.status") ||
|
matches_exact("get web.status") ||
|
||||||
|
matches_exact("get web.stats.status") ||
|
||||||
matches_exact("get web") ||
|
matches_exact("get web") ||
|
||||||
matches_exact("advert") ||
|
matches_exact("advert") ||
|
||||||
matches_exact("reboot") ||
|
matches_exact("reboot") ||
|
||||||
@@ -1489,6 +2060,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
|||||||
matches_exact("get public.key") ||
|
matches_exact("get public.key") ||
|
||||||
matches_exact("get advert.interval") ||
|
matches_exact("get advert.interval") ||
|
||||||
matches_exact("get flood.advert.interval") ||
|
matches_exact("get flood.advert.interval") ||
|
||||||
|
matches_exact("get repeat") ||
|
||||||
matches_exact("get flood.max") ||
|
matches_exact("get flood.max") ||
|
||||||
matches_exact("get path.hash.mode") ||
|
matches_exact("get path.hash.mode") ||
|
||||||
matches_exact("get owner.info") ||
|
matches_exact("get owner.info") ||
|
||||||
@@ -1505,6 +2077,8 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
|||||||
matches_prefix("set mqtt.tx ") ||
|
matches_prefix("set mqtt.tx ") ||
|
||||||
matches_prefix("set web ") ||
|
matches_prefix("set web ") ||
|
||||||
matches_prefix("set.web ") ||
|
matches_prefix("set.web ") ||
|
||||||
|
matches_prefix("set web.stats ") ||
|
||||||
|
matches_prefix("set.web.stats ") ||
|
||||||
matches_prefix("set mqtt.eastmesh-au ") ||
|
matches_prefix("set mqtt.eastmesh-au ") ||
|
||||||
matches_prefix("set mqtt.eastmesh.au ") ||
|
matches_prefix("set mqtt.eastmesh.au ") ||
|
||||||
matches_prefix("set mqtt.letsmesh-eu ") ||
|
matches_prefix("set mqtt.letsmesh-eu ") ||
|
||||||
@@ -1520,6 +2094,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
|||||||
matches_prefix("set prv.key ") ||
|
matches_prefix("set prv.key ") ||
|
||||||
matches_prefix("set advert.interval ") ||
|
matches_prefix("set advert.interval ") ||
|
||||||
matches_prefix("set flood.advert.interval ") ||
|
matches_prefix("set flood.advert.interval ") ||
|
||||||
|
matches_prefix("set repeat ") ||
|
||||||
matches_prefix("set flood.max ") ||
|
matches_prefix("set flood.max ") ||
|
||||||
matches_prefix("set path.hash.mode ") ||
|
matches_prefix("set path.hash.mode ") ||
|
||||||
matches_prefix("time ") ||
|
matches_prefix("time ") ||
|
||||||
@@ -1538,11 +2113,191 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
|||||||
reply[reply_size - 1] = 0;
|
reply[reply_size - 1] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MyMesh::isWebStatsEnabled() const {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
return web.isWebStatsEnabled();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
||||||
|
if (reply == nullptr || reply_size == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reply[0] = 0;
|
||||||
|
|
||||||
|
#if !defined(ESP_PLATFORM) || !WITH_WEB_PANEL
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
char wifi_ssid[48];
|
||||||
|
char wifi_status[20];
|
||||||
|
char wifi_ip[20];
|
||||||
|
char wifi_powersave[12];
|
||||||
|
escapeJsonString(network.getWifiSSID()[0] ? network.getWifiSSID() : "-", wifi_ssid, sizeof(wifi_ssid));
|
||||||
|
escapeJsonString(network.getWifiPowerSave(), wifi_powersave, sizeof(wifi_powersave));
|
||||||
|
int wifi_rssi = 0;
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (network.getWifiSSID()[0] == 0) {
|
||||||
|
strncpy(wifi_status, "unconfigured", sizeof(wifi_status) - 1);
|
||||||
|
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||||
|
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||||
|
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||||
|
} else if (network.isWifiConnected()) {
|
||||||
|
strncpy(wifi_status, "connected", sizeof(wifi_status) - 1);
|
||||||
|
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||||
|
String ip = WiFi.localIP().toString();
|
||||||
|
escapeJsonString(ip.c_str(), wifi_ip, sizeof(wifi_ip));
|
||||||
|
wifi_rssi = WiFi.RSSI();
|
||||||
|
} else {
|
||||||
|
strncpy(wifi_status, "connecting", sizeof(wifi_status) - 1);
|
||||||
|
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||||
|
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||||
|
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
strncpy(wifi_status, "unsupported", sizeof(wifi_status) - 1);
|
||||||
|
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||||
|
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||||
|
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const int battery_pct = board.getBatteryPercent();
|
||||||
|
const bool archive_available = (_archive != nullptr) && _archive->isMounted();
|
||||||
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
|
||||||
|
#else
|
||||||
|
const bool mqtt_connected = false;
|
||||||
|
#endif
|
||||||
|
const bool web_panel_up = web.isPanelRunning();
|
||||||
|
const char* archive_name = (_archive != nullptr) ? _archive->getLogicalName() : "archive";
|
||||||
|
const char* archive_path = (_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats";
|
||||||
|
const char* archive_type = (_archive != nullptr) ? _archive->getCardTypeName() : "unavailable";
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset,
|
||||||
|
"{\"enabled\":true,"
|
||||||
|
"\"history\":{\"active\":%s,\"psram\":%s,\"degraded\":%s,\"samples\":%u,\"sample_capacity\":%u,\"sample_interval_secs\":%lu,"
|
||||||
|
"\"archive_restored\":%s,\"archive_restored_samples\":%u,\"archive_summary_interval_secs\":%lu,"
|
||||||
|
"\"events\":%u,\"event_capacity\":%u},"
|
||||||
|
"\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
|
||||||
|
"\"total_bytes\":%llu,\"used_bytes\":%llu},"
|
||||||
|
"\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u,"
|
||||||
|
"\"external_power\":%s,\"charging\":%s,\"vbus\":%s},"
|
||||||
|
"\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu},"
|
||||||
|
"\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,"
|
||||||
|
"\"recv_errors\":%u,\"direct_dups\":%u,\"flood_dups\":%u,\"neighbors\":%u},"
|
||||||
|
"\"memory\":{\"heap_free\":%u,\"heap_min\":%u,\"heap_max\":%u,\"psram_free\":%u,\"psram_min\":%u,\"psram_max\":%u},"
|
||||||
|
"\"wifi\":{\"ssid\":\"%s\",\"status\":\"%s\",\"connected\":%s,\"ip\":\"%s\",\"rssi\":%d,\"powersave\":\"%s\"},"
|
||||||
|
"\"services\":{\"mqtt_connected\":%s,\"web_enabled\":%s,\"web_panel_up\":%s,\"web_auth\":\"%s\","
|
||||||
|
"\"archive_available\":%s}",
|
||||||
|
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "true" : "false",
|
||||||
|
_stats_history.isPsramBacked() ? "true" : "false",
|
||||||
|
_stats_history.isDegraded() ? "true" : "false",
|
||||||
|
static_cast<unsigned>(_stats_history.getSampleCount()),
|
||||||
|
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
||||||
|
static_cast<unsigned long>(StatsHistory::getSampleIntervalSecs()),
|
||||||
|
_stats_history.hasArchiveRestore() ? "true" : "false",
|
||||||
|
static_cast<unsigned>(_stats_history.getRestoredSampleCount()),
|
||||||
|
static_cast<unsigned long>(StatsHistory::getArchiveSummaryIntervalSecs()),
|
||||||
|
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||||
|
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||||
|
archive_name,
|
||||||
|
archive_available ? "true" : "false",
|
||||||
|
archive_path,
|
||||||
|
archive_type,
|
||||||
|
static_cast<unsigned long long>(_archive != nullptr ? _archive->getTotalBytes() : 0),
|
||||||
|
static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
|
||||||
|
board.getBattMilliVolts(),
|
||||||
|
battery_pct,
|
||||||
|
static_cast<unsigned long>(uptime_millis / 1000),
|
||||||
|
_err_flags,
|
||||||
|
static_cast<unsigned>(_mgr->getOutboundTotal()),
|
||||||
|
board.isExternalPowered() ? "true" : "false",
|
||||||
|
board.isCharging() ? "true" : "false",
|
||||||
|
board.isVbusPresent() ? "true" : "false",
|
||||||
|
static_cast<int>(_radio->getNoiseFloor()),
|
||||||
|
radio_driver.getLastRSSI(),
|
||||||
|
radio_driver.getLastSNR(),
|
||||||
|
static_cast<unsigned long>(getTotalAirTime() / 1000),
|
||||||
|
static_cast<unsigned long>(getReceiveAirTime() / 1000),
|
||||||
|
static_cast<unsigned>(radio_driver.getPacketsRecv()),
|
||||||
|
static_cast<unsigned>(radio_driver.getPacketsSent()),
|
||||||
|
static_cast<unsigned>(getNumSentFlood()),
|
||||||
|
static_cast<unsigned>(getNumSentDirect()),
|
||||||
|
static_cast<unsigned>(getNumRecvFlood()),
|
||||||
|
static_cast<unsigned>(getNumRecvDirect()),
|
||||||
|
static_cast<unsigned>(radio_driver.getPacketsRecvErrors()),
|
||||||
|
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumDirectDups()),
|
||||||
|
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumFloodDups()),
|
||||||
|
static_cast<unsigned>(getNeighbourCount()),
|
||||||
|
ESP.getFreeHeap(),
|
||||||
|
ESP.getMinFreeHeap(),
|
||||||
|
ESP.getMaxAllocHeap(),
|
||||||
|
ESP.getFreePsram(),
|
||||||
|
ESP.getMinFreePsram(),
|
||||||
|
ESP.getMaxAllocPsram(),
|
||||||
|
wifi_ssid,
|
||||||
|
wifi_status,
|
||||||
|
network.isWifiConnected() ? "true" : "false",
|
||||||
|
wifi_ip,
|
||||||
|
wifi_rssi,
|
||||||
|
wifi_powersave,
|
||||||
|
mqtt_connected ? "true" : "false",
|
||||||
|
web.isWebEnabled() ? "true" : "false",
|
||||||
|
web_panel_up ? "true" : "false",
|
||||||
|
web.isPanelUnlocked() ? "unlocked" : "locked",
|
||||||
|
archive_available ? "true" : "false");
|
||||||
|
if (offset >= reply_size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, ",");
|
||||||
|
if (!appendJsonEvents(reply, reply_size, offset)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, ",");
|
||||||
|
if (!appendJsonNeighbours(reply, reply_size, offset)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += snprintf(&reply[offset], reply_size - offset, "}");
|
||||||
|
return offset < reply_size;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyMesh::formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
if (!web.isWebStatsEnabled()) {
|
||||||
|
if (reply != nullptr && reply_size > 0) {
|
||||||
|
reply[0] = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _stats_history.buildSeriesJson(
|
||||||
|
series,
|
||||||
|
reply,
|
||||||
|
reply_size,
|
||||||
|
getRTCClock()->getCurrentTime(),
|
||||||
|
static_cast<uint32_t>(uptime_millis / 1000));
|
||||||
|
#else
|
||||||
|
(void)series;
|
||||||
|
if (reply != nullptr && reply_size > 0) {
|
||||||
|
reply[0] = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void MyMesh::loop() {
|
void MyMesh::loop() {
|
||||||
#ifdef WITH_BRIDGE
|
#ifdef WITH_BRIDGE
|
||||||
bridge.loop();
|
bridge.loop();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
const uint32_t now = millis();
|
||||||
|
uptime_millis += now - last_millis;
|
||||||
|
last_millis = now;
|
||||||
|
|
||||||
mesh::Mesh::loop();
|
mesh::Mesh::loop();
|
||||||
|
|
||||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||||
@@ -1577,6 +2332,19 @@ void MyMesh::loop() {
|
|||||||
dirty_contacts_expiry = 0;
|
dirty_contacts_expiry = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
bool network_required = false;
|
||||||
|
#if WITH_WEB_PANEL
|
||||||
|
network_required = web.isWebEnabled();
|
||||||
|
#endif
|
||||||
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
network_required = network_required || mqtt.isActive();
|
||||||
|
#endif
|
||||||
|
network.loop(network_required);
|
||||||
|
#if WITH_WEB_PANEL
|
||||||
|
web.loop();
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
MQTTStatusSnapshot mqtt_status{};
|
MQTTStatusSnapshot mqtt_status{};
|
||||||
mqtt_status.battery_mv = static_cast<int>(board.getBattMilliVolts());
|
mqtt_status.battery_mv = static_cast<int>(board.getBattMilliVolts());
|
||||||
@@ -1593,11 +2361,9 @@ void MyMesh::loop() {
|
|||||||
mqtt_status.radio_cr = _prefs.cr;
|
mqtt_status.radio_cr = _prefs.cr;
|
||||||
mqtt.loop(mqtt_status);
|
mqtt.loop(mqtt_status);
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
// update uptime
|
updateStatsHistory(now);
|
||||||
uint32_t now = millis();
|
#endif
|
||||||
uptime_millis += now - last_millis;
|
|
||||||
last_millis = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// To check if there is pending work
|
// To check if there is pending work
|
||||||
@@ -1607,6 +2373,9 @@ bool MyMesh::hasPendingWork() const {
|
|||||||
#endif
|
#endif
|
||||||
#if defined(WITH_MQTT_UPLINK)
|
#if defined(WITH_MQTT_UPLINK)
|
||||||
if (mqtt.isActive()) return true;
|
if (mqtt.isActive()) return true;
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
if (web.isWebEnabled()) return true;
|
||||||
#endif
|
#endif
|
||||||
return _mgr->getOutboundTotal() > 0;
|
return _mgr->getOutboundTotal() > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,17 @@
|
|||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
#include <helpers/mqtt/MQTTUplink.h>
|
#include <helpers/mqtt/MQTTUplink.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <helpers/NetworkService.h>
|
||||||
|
#include <helpers/web/WebService.h>
|
||||||
|
|
||||||
#include <helpers/AdvertDataHelpers.h>
|
#include <helpers/AdvertDataHelpers.h>
|
||||||
|
#include <helpers/ArchiveStorage.h>
|
||||||
#include <helpers/ArduinoHelpers.h>
|
#include <helpers/ArduinoHelpers.h>
|
||||||
#include <helpers/ClientACL.h>
|
#include <helpers/ClientACL.h>
|
||||||
#include <helpers/CommonCLI.h>
|
#include <helpers/CommonCLI.h>
|
||||||
#include <helpers/IdentityStore.h>
|
#include <helpers/IdentityStore.h>
|
||||||
#include <helpers/SimpleMeshTables.h>
|
#include <helpers/SimpleMeshTables.h>
|
||||||
|
#include <helpers/StatsHistory.h>
|
||||||
#include <helpers/StaticPoolPacketManager.h>
|
#include <helpers/StaticPoolPacketManager.h>
|
||||||
#include <helpers/StatsFormatHelper.h>
|
#include <helpers/StatsFormatHelper.h>
|
||||||
#include <helpers/TxtDataHelpers.h>
|
#include <helpers/TxtDataHelpers.h>
|
||||||
@@ -84,12 +88,16 @@ struct NeighbourInfo {
|
|||||||
|
|
||||||
#define PACKET_LOG_FILE "/packet_log"
|
#define PACKET_LOG_FILE "/packet_log"
|
||||||
|
|
||||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public MQTTWebCommandRunner {
|
class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelCommandRunner {
|
||||||
FILESYSTEM* _fs;
|
FILESYSTEM* _fs;
|
||||||
|
ArchiveStorage* _archive;
|
||||||
uint32_t last_millis;
|
uint32_t last_millis;
|
||||||
uint64_t uptime_millis;
|
uint64_t uptime_millis;
|
||||||
|
unsigned long next_archive_neighbours_flush_ms;
|
||||||
|
unsigned long next_history_sample_ms;
|
||||||
unsigned long next_local_advert, next_flood_advert;
|
unsigned long next_local_advert, next_flood_advert;
|
||||||
bool _logging;
|
bool _logging;
|
||||||
|
bool _archive_neighbours_dirty;
|
||||||
NodePrefs _prefs;
|
NodePrefs _prefs;
|
||||||
ClientACL acl;
|
ClientACL acl;
|
||||||
CommonCLI _cli;
|
CommonCLI _cli;
|
||||||
@@ -124,8 +132,32 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public MQTTWebComma
|
|||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
MQTTUplink mqtt;
|
MQTTUplink mqtt;
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
NetworkService network;
|
||||||
|
#endif
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
WebService web;
|
||||||
|
#endif
|
||||||
|
StatsHistory _stats_history;
|
||||||
|
struct {
|
||||||
|
bool initialized;
|
||||||
|
bool wifi_connected;
|
||||||
|
bool mqtt_connected;
|
||||||
|
bool web_panel_up;
|
||||||
|
bool archive_mounted;
|
||||||
|
bool low_memory;
|
||||||
|
uint32_t last_low_memory_event_uptime_secs;
|
||||||
|
} _stats_state;
|
||||||
|
|
||||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||||
|
size_t getNeighbourCount() const;
|
||||||
|
void updateStatsHistory(unsigned long now_ms);
|
||||||
|
bool restoreArchiveNeighbours();
|
||||||
|
void flushArchiveNeighbours();
|
||||||
|
void maybeFlushArchiveNeighbours(unsigned long now_ms);
|
||||||
|
void recordStatsEvent(uint8_t type, int16_t value = 0);
|
||||||
|
bool appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const;
|
||||||
|
bool appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const;
|
||||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||||
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||||
@@ -182,7 +214,7 @@ protected:
|
|||||||
public:
|
public:
|
||||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||||
|
|
||||||
void begin(FILESYSTEM* fs);
|
void begin(FILESYSTEM* fs, ArchiveStorage* archive = nullptr);
|
||||||
void sendNodeDiscoverReq();
|
void sendNodeDiscoverReq();
|
||||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||||
@@ -224,6 +256,9 @@ public:
|
|||||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||||
void runWebCommand(const char* command, char* reply, size_t reply_size) override;
|
void runWebCommand(const char* command, char* reply, size_t reply_size) override;
|
||||||
const char* getWebAdminPassword() const override { return _prefs.password; }
|
const char* getWebAdminPassword() const override { return _prefs.password; }
|
||||||
|
bool isWebStatsEnabled() const override;
|
||||||
|
bool formatWebStatsSummaryJson(char* reply, size_t reply_size) override;
|
||||||
|
bool formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) override;
|
||||||
void loop();
|
void loop();
|
||||||
|
|
||||||
#if defined(WITH_BRIDGE)
|
#if defined(WITH_BRIDGE)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
|
|
||||||
#include "MyMesh.h"
|
#include "MyMesh.h"
|
||||||
|
#include <helpers/ArchiveStorage.h>
|
||||||
|
|
||||||
#ifdef DISPLAY_CLASS
|
#ifdef DISPLAY_CLASS
|
||||||
#include "UITask.h"
|
#include "UITask.h"
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
|
|
||||||
StdRNG fast_rng;
|
StdRNG fast_rng;
|
||||||
SimpleMeshTables tables;
|
SimpleMeshTables tables;
|
||||||
|
ArchiveStorage archive;
|
||||||
|
|
||||||
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
|
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ void setup() {
|
|||||||
delay(1000);
|
delay(1000);
|
||||||
|
|
||||||
board.begin();
|
board.begin();
|
||||||
|
archive.begin();
|
||||||
|
|
||||||
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
|
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
|
||||||
// give some extra time for serial to settle so
|
// give some extra time for serial to settle so
|
||||||
@@ -93,7 +96,7 @@ void setup() {
|
|||||||
|
|
||||||
sensors.begin();
|
sensors.begin();
|
||||||
|
|
||||||
the_mesh.begin(fs);
|
the_mesh.begin(fs, &archive);
|
||||||
|
|
||||||
#ifdef DISPLAY_CLASS
|
#ifdef DISPLAY_CLASS
|
||||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ public:
|
|||||||
virtual bool supportsBatteryReporting() const { return false; }
|
virtual bool supportsBatteryReporting() const { return false; }
|
||||||
virtual bool setBatteryReporting(bool enabled) { (void)enabled; return false; }
|
virtual bool setBatteryReporting(bool enabled) { (void)enabled; return false; }
|
||||||
virtual bool isBatteryReportingEnabled() const { return true; }
|
virtual bool isBatteryReportingEnabled() const { return true; }
|
||||||
|
virtual int getBatteryPercent() { return -1; }
|
||||||
|
virtual bool isCharging() { return false; }
|
||||||
|
virtual bool isVbusPresent() { return false; }
|
||||||
virtual float getMCUTemperature() { return NAN; }
|
virtual float getMCUTemperature() { return NAN; }
|
||||||
virtual bool setAdcMultiplier(float multiplier) { return false; };
|
virtual bool setAdcMultiplier(float multiplier) { return false; };
|
||||||
virtual float getAdcMultiplier() const { return 0.0f; }
|
virtual float getAdcMultiplier() const { return 0.0f; }
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
#include "ArchiveStorage.h"
|
||||||
|
|
||||||
|
#include <MeshCore.h>
|
||||||
|
|
||||||
|
#ifndef ARCHIVE_DEBUG
|
||||||
|
#if defined(MQTT_DEBUG) && MQTT_DEBUG
|
||||||
|
#define ARCHIVE_DEBUG 1
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_DEBUG 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ARCHIVE_DEBUG
|
||||||
|
#define ARCHIVE_LOG(fmt, ...) Serial.printf("[ARCHIVE] " fmt "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_LOG(...) do { } while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
const char* cardTypeName(uint8_t type) {
|
||||||
|
switch (type) {
|
||||||
|
case CARD_MMC:
|
||||||
|
return "mmc";
|
||||||
|
case CARD_SD:
|
||||||
|
return "sdsc";
|
||||||
|
case CARD_SDHC:
|
||||||
|
return "sdhc";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ArchiveStorage::ArchiveStorage()
|
||||||
|
: _attempted(false), _mounted(false), _mount_failed(false), _supported(false), _card_type(0), _spi_bus(HSPI),
|
||||||
|
_cs_pin(0xFF), _sck_pin(0xFF),
|
||||||
|
_miso_pin(0xFF), _mosi_pin(0xFF), _card_size_bytes(0), _total_bytes(0), _used_bytes(0)
|
||||||
|
#if defined(ESP32)
|
||||||
|
, _spi(nullptr)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool mountArchiveSd(SPIClass* spi,
|
||||||
|
uint8_t spi_bus,
|
||||||
|
uint8_t cs_pin,
|
||||||
|
uint8_t sck_pin,
|
||||||
|
uint8_t miso_pin,
|
||||||
|
uint8_t mosi_pin) {
|
||||||
|
if (spi == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#if defined(P_BOARD_IMU_CS)
|
||||||
|
pinMode(P_BOARD_IMU_CS, OUTPUT);
|
||||||
|
digitalWrite(P_BOARD_IMU_CS, HIGH);
|
||||||
|
#endif
|
||||||
|
pinMode(cs_pin, OUTPUT);
|
||||||
|
digitalWrite(cs_pin, HIGH);
|
||||||
|
spi->begin(sck_pin, miso_pin, mosi_pin, cs_pin);
|
||||||
|
return SD.begin(cs_pin, *spi, 10000000U);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ArchiveStorage::begin() {
|
||||||
|
if (_attempted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_attempted = true;
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
_supported = resolvePins();
|
||||||
|
if (!_supported) {
|
||||||
|
ARCHIVE_LOG("unsupported: no SD pin mapping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHIVE_LOG("init bus=%u cs=%u sck=%u miso=%u mosi=%u",
|
||||||
|
static_cast<unsigned>(_spi_bus),
|
||||||
|
static_cast<unsigned>(_cs_pin),
|
||||||
|
static_cast<unsigned>(_sck_pin),
|
||||||
|
static_cast<unsigned>(_miso_pin),
|
||||||
|
static_cast<unsigned>(_mosi_pin));
|
||||||
|
|
||||||
|
_spi = new SPIClass(_spi_bus);
|
||||||
|
if (_spi == nullptr) {
|
||||||
|
_mount_failed = true;
|
||||||
|
ARCHIVE_LOG("mount failed: SPI alloc failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) {
|
||||||
|
_mount_failed = true;
|
||||||
|
ARCHIVE_LOG("mount failed bus=%u cs=%u sck=%u miso=%u mosi=%u",
|
||||||
|
static_cast<unsigned>(_spi_bus),
|
||||||
|
static_cast<unsigned>(_cs_pin),
|
||||||
|
static_cast<unsigned>(_sck_pin),
|
||||||
|
static_cast<unsigned>(_miso_pin),
|
||||||
|
static_cast<unsigned>(_mosi_pin));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mounted = true;
|
||||||
|
if (!SD.exists(getFsStatsPath())) {
|
||||||
|
SD.mkdir(getFsStatsPath());
|
||||||
|
}
|
||||||
|
refreshCardInfo();
|
||||||
|
ARCHIVE_LOG("mounted type=%s card=%llu total=%llu used=%llu path=%s",
|
||||||
|
getCardTypeName(),
|
||||||
|
static_cast<unsigned long long>(_card_size_bytes),
|
||||||
|
static_cast<unsigned long long>(_total_bytes),
|
||||||
|
static_cast<unsigned long long>(_used_bytes),
|
||||||
|
getLogicalStatsPath());
|
||||||
|
#else
|
||||||
|
_supported = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArchiveStorage::recover() {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (!_supported || _spi == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHIVE_LOG("recover begin bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin));
|
||||||
|
SD.end();
|
||||||
|
_spi->end();
|
||||||
|
_mounted = false;
|
||||||
|
_mount_failed = false;
|
||||||
|
_card_size_bytes = 0;
|
||||||
|
_total_bytes = 0;
|
||||||
|
_used_bytes = 0;
|
||||||
|
|
||||||
|
if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) {
|
||||||
|
_mount_failed = true;
|
||||||
|
ARCHIVE_LOG("recover failed bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mounted = true;
|
||||||
|
if (!SD.exists(getFsStatsPath())) {
|
||||||
|
SD.mkdir(getFsStatsPath());
|
||||||
|
}
|
||||||
|
refreshCardInfo();
|
||||||
|
ARCHIVE_LOG("recover mounted type=%s card=%llu total=%llu used=%llu path=%s",
|
||||||
|
getCardTypeName(),
|
||||||
|
static_cast<unsigned long long>(_card_size_bytes),
|
||||||
|
static_cast<unsigned long long>(_total_bytes),
|
||||||
|
static_cast<unsigned long long>(_used_bytes),
|
||||||
|
getLogicalStatsPath());
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArchiveStorage::isSupported() const {
|
||||||
|
return _supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* ArchiveStorage::getFS() {
|
||||||
|
#if defined(ESP32)
|
||||||
|
return _mounted ? &SD : nullptr;
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ArchiveStorage::getCardTypeName() const {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (!_mounted) {
|
||||||
|
return "unavailable";
|
||||||
|
}
|
||||||
|
return cardTypeName(_card_type);
|
||||||
|
#else
|
||||||
|
return "unsupported";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
bool ArchiveStorage::resolvePins() {
|
||||||
|
#if defined(P_BOARD_SPI_SCK) && defined(P_BOARD_SPI_MISO) && defined(P_BOARD_SPI_MOSI) && defined(P_BOARD_SPI_CS)
|
||||||
|
_sck_pin = static_cast<uint8_t>(P_BOARD_SPI_SCK);
|
||||||
|
_miso_pin = static_cast<uint8_t>(P_BOARD_SPI_MISO);
|
||||||
|
_mosi_pin = static_cast<uint8_t>(P_BOARD_SPI_MOSI);
|
||||||
|
_cs_pin = static_cast<uint8_t>(P_BOARD_SPI_CS);
|
||||||
|
#if defined(TBEAM_SUPREME_SX1262) && defined(FSPI)
|
||||||
|
_spi_bus = FSPI;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
#elif defined(TBEAM_SUPREME_SX1262)
|
||||||
|
// T-Beam S3 Supreme SD/IMU shared SPI bus pins.
|
||||||
|
_sck_pin = 36;
|
||||||
|
_miso_pin = 37;
|
||||||
|
_mosi_pin = 35;
|
||||||
|
_cs_pin = 47;
|
||||||
|
#if defined(FSPI)
|
||||||
|
_spi_bus = FSPI;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
#elif defined(HAS_SDCARD) && defined(SDCARD_CS)
|
||||||
|
_cs_pin = static_cast<uint8_t>(SDCARD_CS);
|
||||||
|
#if defined(SPI_SCK) && defined(SPI_MISO) && defined(SPI_MOSI)
|
||||||
|
_sck_pin = static_cast<uint8_t>(SPI_SCK);
|
||||||
|
_miso_pin = static_cast<uint8_t>(SPI_MISO);
|
||||||
|
_mosi_pin = static_cast<uint8_t>(SPI_MOSI);
|
||||||
|
return true;
|
||||||
|
#elif defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
|
||||||
|
_sck_pin = static_cast<uint8_t>(P_LORA_SCLK);
|
||||||
|
_miso_pin = static_cast<uint8_t>(P_LORA_MISO);
|
||||||
|
_mosi_pin = static_cast<uint8_t>(P_LORA_MOSI);
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArchiveStorage::refreshCardInfo() {
|
||||||
|
if (!_mounted) {
|
||||||
|
_card_type = 0;
|
||||||
|
_card_size_bytes = 0;
|
||||||
|
_total_bytes = 0;
|
||||||
|
_used_bytes = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_card_type == 0) {
|
||||||
|
_card_type = SD.cardType();
|
||||||
|
}
|
||||||
|
_card_size_bytes = SD.cardSize();
|
||||||
|
_total_bytes = SD.totalBytes();
|
||||||
|
_used_bytes = SD.usedBytes();
|
||||||
|
if (_total_bytes == 0) {
|
||||||
|
_total_bytes = _card_size_bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
#include <FS.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <SPI.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class ArchiveStorage {
|
||||||
|
public:
|
||||||
|
ArchiveStorage();
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
bool recover();
|
||||||
|
|
||||||
|
bool isSupported() const;
|
||||||
|
bool isAttempted() const { return _attempted; }
|
||||||
|
bool isMounted() const { return _mounted; }
|
||||||
|
bool hadMountFailure() const { return _mount_failed; }
|
||||||
|
|
||||||
|
const char* getLogicalName() const { return "archive"; }
|
||||||
|
const char* getLogicalStatsPath() const { return "archive:/stats"; }
|
||||||
|
const char* getFsStatsPath() const { return "/stats"; }
|
||||||
|
|
||||||
|
FILESYSTEM* getFS();
|
||||||
|
|
||||||
|
uint64_t getCardSizeBytes() const { return _card_size_bytes; }
|
||||||
|
uint64_t getTotalBytes() const { return _total_bytes; }
|
||||||
|
uint64_t getUsedBytes() const { return _used_bytes; }
|
||||||
|
const char* getCardTypeName() const;
|
||||||
|
uint8_t getChipSelectPin() const { return _cs_pin; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(ESP32)
|
||||||
|
bool resolvePins();
|
||||||
|
void refreshCardInfo();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool _attempted;
|
||||||
|
bool _mounted;
|
||||||
|
bool _mount_failed;
|
||||||
|
bool _supported;
|
||||||
|
uint8_t _card_type;
|
||||||
|
uint8_t _spi_bus;
|
||||||
|
uint8_t _cs_pin;
|
||||||
|
uint8_t _sck_pin;
|
||||||
|
uint8_t _miso_pin;
|
||||||
|
uint8_t _mosi_pin;
|
||||||
|
uint64_t _card_size_bytes;
|
||||||
|
uint64_t _total_bytes;
|
||||||
|
uint64_t _used_bytes;
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
SPIClass* _spi;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#include "NetworkPrefs.h"
|
||||||
|
|
||||||
|
#include <helpers/TxtDataHelpers.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LegacyWebPrefsV1 {
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t web_enabled;
|
||||||
|
uint8_t web_stats_enabled;
|
||||||
|
uint8_t wifi_powersave;
|
||||||
|
uint8_t reserved;
|
||||||
|
char wifi_ssid[33];
|
||||||
|
char wifi_pwd[65];
|
||||||
|
};
|
||||||
|
|
||||||
|
bool loadLegacyWebWifiPrefs(FILESYSTEM* fs, NetworkPrefs& prefs) {
|
||||||
|
if (fs == nullptr || !fs->exists("/web_prefs")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
File file = fs->open("/web_prefs", "r");
|
||||||
|
#else
|
||||||
|
File file = fs->open("/web_prefs");
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<size_t>(file.size()) < sizeof(LegacyWebPrefsV1)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyWebPrefsV1 legacy{};
|
||||||
|
bool ok = file.read(reinterpret_cast<uint8_t*>(&legacy), sizeof(legacy)) == sizeof(legacy);
|
||||||
|
file.close();
|
||||||
|
if (!ok || legacy.magic != 0x57454250) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.wifi_powersave = legacy.wifi_powersave <= 2 ? legacy.wifi_powersave : 0;
|
||||||
|
StrHelper::strncpy(prefs.wifi_ssid, legacy.wifi_ssid, sizeof(prefs.wifi_ssid));
|
||||||
|
StrHelper::strncpy(prefs.wifi_pwd, legacy.wifi_pwd, sizeof(prefs.wifi_pwd));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void NetworkPrefsStore::setDefaults(NetworkPrefs& prefs) {
|
||||||
|
memset(&prefs, 0, sizeof(prefs));
|
||||||
|
prefs.magic = kMagic;
|
||||||
|
prefs.wifi_powersave = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs,
|
||||||
|
uint8_t legacy_wifi_powersave,
|
||||||
|
const char* legacy_wifi_ssid,
|
||||||
|
const char* legacy_wifi_pwd) {
|
||||||
|
setDefaults(prefs);
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs->exists(kFilename)) {
|
||||||
|
prefs.wifi_powersave = legacy_wifi_powersave <= 2 ? legacy_wifi_powersave : 0;
|
||||||
|
if (legacy_wifi_ssid != nullptr) {
|
||||||
|
StrHelper::strncpy(prefs.wifi_ssid, legacy_wifi_ssid, sizeof(prefs.wifi_ssid));
|
||||||
|
}
|
||||||
|
if (legacy_wifi_pwd != nullptr) {
|
||||||
|
StrHelper::strncpy(prefs.wifi_pwd, legacy_wifi_pwd, sizeof(prefs.wifi_pwd));
|
||||||
|
}
|
||||||
|
loadLegacyWebWifiPrefs(fs, prefs);
|
||||||
|
save(fs, prefs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
File file = fs->open(kFilename, "r");
|
||||||
|
#else
|
||||||
|
File file = fs->open(kFilename);
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkPrefs persisted{};
|
||||||
|
size_t bytes_to_read = min(static_cast<size_t>(file.size()), sizeof(persisted));
|
||||||
|
bool ok = bytes_to_read >= sizeof(persisted.magic) &&
|
||||||
|
file.read(reinterpret_cast<uint8_t*>(&persisted), bytes_to_read) == bytes_to_read;
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (!ok || persisted.magic != kMagic) {
|
||||||
|
fs->remove(kFilename);
|
||||||
|
save(fs, prefs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs = persisted;
|
||||||
|
if (prefs.wifi_powersave > 2) {
|
||||||
|
prefs.wifi_powersave = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkPrefsStore::save(FILESYSTEM* fs, const NetworkPrefs& prefs) {
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fs->exists(kFilename) && !fs->remove(kFilename)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
File file = fs->open(kFilename, "w");
|
||||||
|
#else
|
||||||
|
File file = fs->open(kFilename, "w", true);
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok = file.write(reinterpret_cast<const uint8_t*>(&prefs), sizeof(prefs)) == sizeof(prefs);
|
||||||
|
file.close();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct NetworkPrefs {
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t wifi_powersave;
|
||||||
|
uint8_t reserved[3];
|
||||||
|
char wifi_ssid[33];
|
||||||
|
char wifi_pwd[65];
|
||||||
|
};
|
||||||
|
|
||||||
|
class NetworkPrefsStore {
|
||||||
|
public:
|
||||||
|
static void setDefaults(NetworkPrefs& prefs);
|
||||||
|
static bool load(FILESYSTEM* fs, NetworkPrefs& prefs,
|
||||||
|
uint8_t legacy_wifi_powersave = 0,
|
||||||
|
const char* legacy_wifi_ssid = nullptr,
|
||||||
|
const char* legacy_wifi_pwd = nullptr);
|
||||||
|
static bool save(FILESYSTEM* fs, const NetworkPrefs& prefs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t kMagic = 0x4E455450;
|
||||||
|
static constexpr const char* kFilename = "/network_prefs";
|
||||||
|
};
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
#include "NetworkService.h"
|
||||||
|
|
||||||
|
#include <helpers/TxtDataHelpers.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_sntp.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
constexpr unsigned long kWifiRetryMillis = 15000;
|
||||||
|
constexpr unsigned long kWifiConnectTimeoutMillis = 45000;
|
||||||
|
constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z
|
||||||
|
|
||||||
|
int getWifiQualityPercent(int rssi_dbm) {
|
||||||
|
if (rssi_dbm <= -100) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (rssi_dbm >= -50) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return 2 * (rssi_dbm + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* getWifiQualityLabel(int rssi_dbm) {
|
||||||
|
if (rssi_dbm >= -60) {
|
||||||
|
return "excellent";
|
||||||
|
}
|
||||||
|
if (rssi_dbm >= -67) {
|
||||||
|
return "good";
|
||||||
|
}
|
||||||
|
if (rssi_dbm >= -75) {
|
||||||
|
return "fair";
|
||||||
|
}
|
||||||
|
return "poor";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
NetworkService::NetworkService()
|
||||||
|
: _fs(nullptr), _prefs{}, _wifi_started(false), _sntp_started(false), _have_time_sync(false), _last_wifi_attempt(0) {
|
||||||
|
NetworkPrefsStore::setDefaults(_prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::begin(FILESYSTEM* fs,
|
||||||
|
uint8_t legacy_wifi_powersave,
|
||||||
|
const char* legacy_wifi_ssid,
|
||||||
|
const char* legacy_wifi_pwd) {
|
||||||
|
_fs = fs;
|
||||||
|
NetworkPrefsStore::load(_fs, _prefs, legacy_wifi_powersave, legacy_wifi_ssid, legacy_wifi_pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::end() {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
if (_wifi_started) {
|
||||||
|
WiFi.disconnect(true, true);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
_wifi_started = false;
|
||||||
|
_sntp_started = false;
|
||||||
|
_have_time_sync = false;
|
||||||
|
_last_wifi_attempt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::loop(bool network_required) {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
ensureWifi(network_required);
|
||||||
|
updateTimeSync();
|
||||||
|
#else
|
||||||
|
(void)network_required;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkService::savePrefs() {
|
||||||
|
return NetworkPrefsStore::save(_fs, _prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkService::setWifiSSID(const char* ssid) {
|
||||||
|
if (ssid == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
StrHelper::strncpy(_prefs.wifi_ssid, ssid, sizeof(_prefs.wifi_ssid));
|
||||||
|
bool ok = savePrefs();
|
||||||
|
reconnectWifi();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkService::setWifiPassword(const char* pwd) {
|
||||||
|
if (pwd == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
StrHelper::strncpy(_prefs.wifi_pwd, pwd, sizeof(_prefs.wifi_pwd));
|
||||||
|
bool ok = savePrefs();
|
||||||
|
reconnectWifi();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkService::setWifiPowerSave(const char* mode) {
|
||||||
|
if (mode == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t next_mode;
|
||||||
|
if (strcmp(mode, "none") == 0) {
|
||||||
|
next_mode = 0;
|
||||||
|
} else if (strcmp(mode, "min") == 0) {
|
||||||
|
next_mode = 1;
|
||||||
|
} else if (strcmp(mode, "max") == 0) {
|
||||||
|
next_mode = 2;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_prefs.wifi_powersave = next_mode;
|
||||||
|
bool ok = savePrefs();
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
if (_wifi_started) {
|
||||||
|
ok = WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave)) && ok;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* NetworkService::getWifiPowerSave() const {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
return getPowerSaveLabel(_prefs.wifi_powersave);
|
||||||
|
#else
|
||||||
|
return "unsupported";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::formatWifiStatusReply(char* reply, size_t reply_size) const {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
const char* status = "disconnected";
|
||||||
|
const char* state = "disconnected";
|
||||||
|
wl_status_t wifi_status = WiFi.status();
|
||||||
|
if (_prefs.wifi_ssid[0] == 0) {
|
||||||
|
status = "unconfigured";
|
||||||
|
state = "unconfigured";
|
||||||
|
} else if (wifi_status == WL_CONNECTED) {
|
||||||
|
status = "connected";
|
||||||
|
state = "connected";
|
||||||
|
} else if (_wifi_started) {
|
||||||
|
status = "connecting";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (wifi_status) {
|
||||||
|
case WL_IDLE_STATUS:
|
||||||
|
state = "idle";
|
||||||
|
break;
|
||||||
|
case WL_NO_SSID_AVAIL:
|
||||||
|
state = "no_ssid";
|
||||||
|
break;
|
||||||
|
case WL_SCAN_COMPLETED:
|
||||||
|
state = "scan_completed";
|
||||||
|
break;
|
||||||
|
case WL_CONNECTED:
|
||||||
|
state = "connected";
|
||||||
|
break;
|
||||||
|
case WL_CONNECT_FAILED:
|
||||||
|
state = "connect_failed";
|
||||||
|
break;
|
||||||
|
case WL_CONNECTION_LOST:
|
||||||
|
state = "connection_lost";
|
||||||
|
break;
|
||||||
|
case WL_DISCONNECTED:
|
||||||
|
state = "disconnected";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = "unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wifi_status == WL_CONNECTED) {
|
||||||
|
const int rssi_dbm = WiFi.RSSI();
|
||||||
|
snprintf(reply, reply_size,
|
||||||
|
"> ssid:%s status:%s code:%d state:%s ip:%s rssi:%d quality:%d%% signal:%s",
|
||||||
|
_prefs.wifi_ssid, status, static_cast<int>(wifi_status), state, WiFi.localIP().toString().c_str(),
|
||||||
|
rssi_dbm, getWifiQualityPercent(rssi_dbm), getWifiQualityLabel(rssi_dbm));
|
||||||
|
} else {
|
||||||
|
snprintf(reply, reply_size, "> ssid:%s status:%s code:%d state:%s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "-",
|
||||||
|
status, static_cast<int>(wifi_status), state);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
snprintf(reply, reply_size, "> wifi:unsupported");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::reconnectWifi() {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
if (_wifi_started) {
|
||||||
|
WiFi.disconnect(true, true);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
_wifi_started = false;
|
||||||
|
_sntp_started = false;
|
||||||
|
_have_time_sync = false;
|
||||||
|
_last_wifi_attempt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkService::isWifiConnected() const {
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
return _wifi_started && WiFi.status() == WL_CONNECTED;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
wifi_ps_type_t NetworkService::toEspPowerSave(uint8_t mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case 1:
|
||||||
|
return WIFI_PS_MIN_MODEM;
|
||||||
|
case 2:
|
||||||
|
return WIFI_PS_MAX_MODEM;
|
||||||
|
default:
|
||||||
|
return WIFI_PS_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* NetworkService::getPowerSaveLabel(uint8_t mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case 1:
|
||||||
|
return "min";
|
||||||
|
case 2:
|
||||||
|
return "max";
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::ensureWifi(bool network_required) {
|
||||||
|
if (!network_required) {
|
||||||
|
if (_wifi_started) {
|
||||||
|
WiFi.disconnect(true, true);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
_wifi_started = false;
|
||||||
|
_sntp_started = false;
|
||||||
|
_have_time_sync = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_prefs.wifi_ssid[0] == 0) {
|
||||||
|
reconnectWifi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long now_ms = millis();
|
||||||
|
wl_status_t status = WiFi.status();
|
||||||
|
if (_wifi_started) {
|
||||||
|
if (_last_wifi_attempt != 0 && status == WL_IDLE_STATUS && now_ms - _last_wifi_attempt < kWifiConnectTimeoutMillis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now_ms - _last_wifi_attempt < kWifiRetryMillis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_wifi_started) {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave));
|
||||||
|
_wifi_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_last_wifi_attempt = now_ms;
|
||||||
|
WiFi.begin(_prefs.wifi_ssid, _prefs.wifi_pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::updateTimeSync() {
|
||||||
|
bool prev_have_time_sync = _have_time_sync;
|
||||||
|
if (!_wifi_started || WiFi.status() != WL_CONNECTED) {
|
||||||
|
_have_time_sync = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_sntp_started) {
|
||||||
|
configTzTime("UTC0", "au.pool.ntp.org", "time.google.com", "time.cloudflare.com");
|
||||||
|
_sntp_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sntp_sync_status_t sync_status = sntp_get_sync_status();
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
bool sane_time = now >= kMinSaneEpoch;
|
||||||
|
bool sync_ready = sync_status == SNTP_SYNC_STATUS_COMPLETED || sync_status == SNTP_SYNC_STATUS_IN_PROGRESS;
|
||||||
|
_have_time_sync = sane_time && (sync_ready || prev_have_time_sync);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <helpers/NetworkStateProvider.h>
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
#include <WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "NetworkPrefs.h"
|
||||||
|
|
||||||
|
class NetworkService : public NetworkStateProvider {
|
||||||
|
public:
|
||||||
|
NetworkService();
|
||||||
|
|
||||||
|
void begin(FILESYSTEM* fs,
|
||||||
|
uint8_t legacy_wifi_powersave = 0,
|
||||||
|
const char* legacy_wifi_ssid = nullptr,
|
||||||
|
const char* legacy_wifi_pwd = nullptr);
|
||||||
|
void end();
|
||||||
|
void loop(bool network_required);
|
||||||
|
|
||||||
|
bool setWifiSSID(const char* ssid);
|
||||||
|
bool setWifiPassword(const char* pwd);
|
||||||
|
const char* getWifiSSID() const { return _prefs.wifi_ssid; }
|
||||||
|
bool setWifiPowerSave(const char* mode);
|
||||||
|
const char* getWifiPowerSave() const;
|
||||||
|
void formatWifiStatusReply(char* reply, size_t reply_size) const;
|
||||||
|
void reconnectWifi();
|
||||||
|
|
||||||
|
bool isWifiConnected() const override;
|
||||||
|
bool hasTimeSync() const override { return _have_time_sync; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(ESP_PLATFORM)
|
||||||
|
static wifi_ps_type_t toEspPowerSave(uint8_t mode);
|
||||||
|
static const char* getPowerSaveLabel(uint8_t mode);
|
||||||
|
void ensureWifi(bool network_required);
|
||||||
|
void updateTimeSync();
|
||||||
|
#endif
|
||||||
|
bool savePrefs();
|
||||||
|
|
||||||
|
FILESYSTEM* _fs;
|
||||||
|
NetworkPrefs _prefs;
|
||||||
|
bool _wifi_started;
|
||||||
|
bool _sntp_started;
|
||||||
|
bool _have_time_sync;
|
||||||
|
unsigned long _last_wifi_attempt;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class NetworkStateProvider {
|
||||||
|
public:
|
||||||
|
virtual ~NetworkStateProvider() = default;
|
||||||
|
virtual bool isWifiConnected() const = 0;
|
||||||
|
virtual bool hasTimeSync() const = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,889 @@
|
|||||||
|
#include "StatsHistory.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ARCHIVE_DEBUG
|
||||||
|
#if defined(MQTT_DEBUG) && MQTT_DEBUG
|
||||||
|
#define ARCHIVE_DEBUG 1
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_DEBUG 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ARCHIVE_DEBUG
|
||||||
|
#define ARCHIVE_LOG(fmt, ...) Serial.printf("[ARCHIVE] " fmt "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define ARCHIVE_LOG(...) do { } while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t kSummaryFlushIntervalMs = 5UL * 60UL * 1000UL;
|
||||||
|
constexpr uint32_t kEventFlushIntervalMs = 60UL * 1000UL;
|
||||||
|
constexpr size_t kMaxSeriesPoints = 64;
|
||||||
|
constexpr size_t kSummaryRestoreWindowBytes = 16384;
|
||||||
|
constexpr size_t kEventsRestoreWindowBytes = 4096;
|
||||||
|
|
||||||
|
struct HistoryCapacityBucket {
|
||||||
|
size_t sample_capacity;
|
||||||
|
size_t event_capacity;
|
||||||
|
};
|
||||||
|
|
||||||
|
HistoryCapacityBucket getHistoryCapacityBucket(bool want_psram) {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (!want_psram) {
|
||||||
|
return {96, 32};
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t psram_size = ESP.getPsramSize();
|
||||||
|
if (psram_size >= (8UL * 1024UL * 1024UL)) {
|
||||||
|
return {720, 288};
|
||||||
|
}
|
||||||
|
if (psram_size >= (4UL * 1024UL * 1024UL)) {
|
||||||
|
return {480, 192};
|
||||||
|
}
|
||||||
|
return {240, 96};
|
||||||
|
#else
|
||||||
|
(void)want_psram;
|
||||||
|
return {96, 32};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T* allocHistoryBuffer(size_t count) {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (psramFound()) {
|
||||||
|
void* ptr = heap_caps_calloc(count, sizeof(T), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if (ptr != nullptr) {
|
||||||
|
return static_cast<T*>(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<T*>(calloc(count, sizeof(T)));
|
||||||
|
#else
|
||||||
|
return static_cast<T*>(calloc(count, sizeof(T)));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeHistoryBuffer(void* ptr) {
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (ptr != nullptr) {
|
||||||
|
free(ptr);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
free(ptr);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveRead(FILESYSTEM* fs, const char* filename) {
|
||||||
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||||
|
return fs->open(filename, "r");
|
||||||
|
#else
|
||||||
|
return fs->open(filename, FILE_READ);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveAppend(FILESYSTEM* fs, const char* filename) {
|
||||||
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||||
|
return fs->open(filename, "a");
|
||||||
|
#else
|
||||||
|
return fs->open(filename, FILE_APPEND, true);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveWrite(FILESYSTEM* fs, const char* filename) {
|
||||||
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||||
|
if (fs->exists(filename)) {
|
||||||
|
fs->remove(filename);
|
||||||
|
}
|
||||||
|
return fs->open(filename, FILE_O_WRITE);
|
||||||
|
#elif defined(RP2040_PLATFORM)
|
||||||
|
return fs->open(filename, "w");
|
||||||
|
#else
|
||||||
|
if (fs->exists(filename)) {
|
||||||
|
fs->remove(filename);
|
||||||
|
}
|
||||||
|
return fs->open(filename, FILE_WRITE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveReadWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||||
|
if (archive == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
File file = openArchiveRead(fs, filename);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (!archive->recover()) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
fs = archive->getFS();
|
||||||
|
return fs != nullptr ? openArchiveRead(fs, filename) : File();
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveAppendWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||||
|
if (archive == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
File file = openArchiveAppend(fs, filename);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (!archive->recover()) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
fs = archive->getFS();
|
||||||
|
return fs != nullptr ? openArchiveAppend(fs, filename) : File();
|
||||||
|
}
|
||||||
|
|
||||||
|
File openArchiveWriteWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||||
|
if (archive == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
File file = openArchiveWrite(fs, filename);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (!archive->recover()) {
|
||||||
|
return File();
|
||||||
|
}
|
||||||
|
fs = archive->getFS();
|
||||||
|
return fs != nullptr ? openArchiveWrite(fs, filename) : File();
|
||||||
|
}
|
||||||
|
|
||||||
|
int buildPointValue(const HistorySample& sample, const HistorySample* previous, const char* series) {
|
||||||
|
if (strcmp(series, "battery") == 0) {
|
||||||
|
return static_cast<int>(sample.battery_mv);
|
||||||
|
}
|
||||||
|
if (strcmp(series, "memory") == 0) {
|
||||||
|
return static_cast<int>(sample.heap_free);
|
||||||
|
}
|
||||||
|
if (strcmp(series, "signal") == 0) {
|
||||||
|
return static_cast<int>(sample.last_rssi_x4);
|
||||||
|
}
|
||||||
|
if (strcmp(series, "packets") == 0) {
|
||||||
|
if (previous == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const uint32_t curr_total = sample.packets_sent + sample.packets_recv;
|
||||||
|
const uint32_t prev_total = previous->packets_sent + previous->packets_recv;
|
||||||
|
return static_cast<int>(curr_total >= prev_total ? (curr_total - prev_total) : 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* seriesTitle(const char* series) {
|
||||||
|
if (strcmp(series, "battery") == 0) {
|
||||||
|
return "Battery";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "memory") == 0) {
|
||||||
|
return "Heap Free";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "packets") == 0) {
|
||||||
|
return "Packet Activity";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "signal") == 0) {
|
||||||
|
return "Signal";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* seriesUnit(const char* series) {
|
||||||
|
if (strcmp(series, "battery") == 0) {
|
||||||
|
return "mV";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "memory") == 0) {
|
||||||
|
return "bytes";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "packets") == 0) {
|
||||||
|
return "pkts";
|
||||||
|
}
|
||||||
|
if (strcmp(series, "signal") == 0) {
|
||||||
|
return "rssi_x4";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t sampleAgeSecs(const HistorySample& sample, uint32_t now_epoch_secs, uint32_t now_uptime_secs) {
|
||||||
|
if (sample.epoch_secs > 0 && now_epoch_secs >= sample.epoch_secs) {
|
||||||
|
return now_epoch_secs - sample.epoch_secs;
|
||||||
|
}
|
||||||
|
if (now_uptime_secs >= sample.uptime_secs) {
|
||||||
|
return now_uptime_secs - sample.uptime_secs;
|
||||||
|
}
|
||||||
|
return sample.uptime_secs;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t eventTypeFromName(const char* name) {
|
||||||
|
if (name == nullptr || name[0] == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "boot") == 0) return HISTORY_EVENT_BOOT;
|
||||||
|
if (strcmp(name, "web_started") == 0) return HISTORY_EVENT_WEB_STARTED;
|
||||||
|
if (strcmp(name, "web_stopped") == 0) return HISTORY_EVENT_WEB_STOPPED;
|
||||||
|
if (strcmp(name, "mqtt_connected") == 0) return HISTORY_EVENT_MQTT_CONNECTED;
|
||||||
|
if (strcmp(name, "mqtt_disconnected") == 0) return HISTORY_EVENT_MQTT_DISCONNECTED;
|
||||||
|
if (strcmp(name, "archive_mounted") == 0) return HISTORY_EVENT_ARCHIVE_MOUNTED;
|
||||||
|
if (strcmp(name, "archive_unavailable") == 0) return HISTORY_EVENT_ARCHIVE_UNAVAILABLE;
|
||||||
|
if (strcmp(name, "low_memory") == 0) return HISTORY_EVENT_LOW_MEMORY;
|
||||||
|
if (strcmp(name, "stats_enabled") == 0) return HISTORY_EVENT_STATS_ENABLED;
|
||||||
|
if (strcmp(name, "stats_disabled") == 0) return HISTORY_EVENT_STATS_DISABLED;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
StatsHistory::StatsHistory()
|
||||||
|
: _enabled(false), _psram_backed(false), _degraded(false), _summary_dirty(false), _next_summary_flush_ms(0),
|
||||||
|
_next_event_flush_ms(0), _sample_capacity(0), _sample_head(0), _sample_count(0), _event_capacity(0),
|
||||||
|
_event_head(0), _event_count(0), _samples(nullptr), _events(nullptr), _archive(nullptr), _restored_sample_count(0),
|
||||||
|
_pending_event_count(0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsHistory::~StatsHistory() {
|
||||||
|
freeHistoryBuffer(_samples);
|
||||||
|
freeHistoryBuffer(_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::begin(bool enabled, ArchiveStorage* archive) {
|
||||||
|
_archive = archive;
|
||||||
|
_enabled = enabled;
|
||||||
|
ensureBuffers();
|
||||||
|
if (_enabled && _sample_count == 0 && isArchiveAvailable()) {
|
||||||
|
restoreSummaryLog();
|
||||||
|
}
|
||||||
|
_next_summary_flush_ms = millis() + kSummaryFlushIntervalMs;
|
||||||
|
_next_event_flush_ms = millis() + kEventFlushIntervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::setArchive(ArchiveStorage* archive) {
|
||||||
|
_archive = archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::setEnabled(bool enabled) {
|
||||||
|
if (enabled && !ensureBuffers()) {
|
||||||
|
_enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_enabled = enabled;
|
||||||
|
if (_enabled && _sample_count == 0 && isArchiveAvailable()) {
|
||||||
|
restoreSummaryLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::isArchiveAvailable() const {
|
||||||
|
return _archive != nullptr && _archive->isMounted();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::ensureBuffers() {
|
||||||
|
if (_samples != nullptr && _events != nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
const bool want_psram = psramFound();
|
||||||
|
#else
|
||||||
|
const bool want_psram = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const HistoryCapacityBucket bucket = getHistoryCapacityBucket(want_psram);
|
||||||
|
_sample_capacity = bucket.sample_capacity;
|
||||||
|
_event_capacity = bucket.event_capacity;
|
||||||
|
_samples = allocHistoryBuffer<HistorySample>(_sample_capacity);
|
||||||
|
_events = allocHistoryBuffer<HistoryEvent>(_event_capacity);
|
||||||
|
_psram_backed = want_psram;
|
||||||
|
_degraded = !want_psram;
|
||||||
|
|
||||||
|
if (_samples == nullptr || _events == nullptr) {
|
||||||
|
freeHistoryBuffer(_samples);
|
||||||
|
freeHistoryBuffer(_events);
|
||||||
|
_samples = allocHistoryBuffer<HistorySample>(64);
|
||||||
|
_events = allocHistoryBuffer<HistoryEvent>(24);
|
||||||
|
_sample_capacity = (_samples != nullptr) ? 64 : 0;
|
||||||
|
_event_capacity = (_events != nullptr) ? 24 : 0;
|
||||||
|
_psram_backed = false;
|
||||||
|
_degraded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _samples != nullptr && _events != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::storeSample(const HistorySample& sample, bool mark_dirty) {
|
||||||
|
if (!ensureBuffers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_samples[_sample_head] = sample;
|
||||||
|
_sample_head = (_sample_head + 1) % _sample_capacity;
|
||||||
|
if (_sample_count < _sample_capacity) {
|
||||||
|
_sample_count++;
|
||||||
|
}
|
||||||
|
if (mark_dirty) {
|
||||||
|
_summary_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) const {
|
||||||
|
if (line == nullptr || line[0] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long epoch_secs = 0;
|
||||||
|
unsigned long uptime_secs = 0;
|
||||||
|
unsigned battery_mv = 0;
|
||||||
|
unsigned queue_len = 0;
|
||||||
|
int last_rssi_x4 = 0;
|
||||||
|
int last_snr_x4 = 0;
|
||||||
|
unsigned long packets_sent = 0;
|
||||||
|
unsigned long packets_recv = 0;
|
||||||
|
unsigned long heap_free = 0;
|
||||||
|
unsigned long psram_free = 0;
|
||||||
|
unsigned error_flags = 0;
|
||||||
|
unsigned recv_errors = 0;
|
||||||
|
unsigned neighbour_count = 0;
|
||||||
|
unsigned direct_dups = 0;
|
||||||
|
unsigned flood_dups = 0;
|
||||||
|
unsigned flags = 0;
|
||||||
|
|
||||||
|
const int parsed = sscanf(line,
|
||||||
|
"%lu,%lu,%u,%u,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u",
|
||||||
|
&epoch_secs,
|
||||||
|
&uptime_secs,
|
||||||
|
&battery_mv,
|
||||||
|
&queue_len,
|
||||||
|
&last_rssi_x4,
|
||||||
|
&last_snr_x4,
|
||||||
|
&packets_sent,
|
||||||
|
&packets_recv,
|
||||||
|
&heap_free,
|
||||||
|
&psram_free,
|
||||||
|
&error_flags,
|
||||||
|
&recv_errors,
|
||||||
|
&neighbour_count,
|
||||||
|
&direct_dups,
|
||||||
|
&flood_dups,
|
||||||
|
&flags);
|
||||||
|
if (parsed != 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&sample, 0, sizeof(sample));
|
||||||
|
sample.epoch_secs = static_cast<uint32_t>(epoch_secs);
|
||||||
|
sample.uptime_secs = static_cast<uint32_t>(uptime_secs);
|
||||||
|
sample.packets_sent = static_cast<uint32_t>(packets_sent);
|
||||||
|
sample.packets_recv = static_cast<uint32_t>(packets_recv);
|
||||||
|
sample.heap_free = static_cast<uint32_t>(heap_free);
|
||||||
|
sample.heap_min = static_cast<uint32_t>(heap_free);
|
||||||
|
sample.psram_free = static_cast<uint32_t>(psram_free);
|
||||||
|
sample.psram_min = static_cast<uint32_t>(psram_free);
|
||||||
|
sample.battery_mv = static_cast<uint16_t>(battery_mv);
|
||||||
|
sample.queue_len = static_cast<uint16_t>(queue_len);
|
||||||
|
sample.error_flags = static_cast<uint16_t>(error_flags);
|
||||||
|
sample.recv_errors = static_cast<uint16_t>(recv_errors);
|
||||||
|
sample.neighbour_count = static_cast<uint16_t>(neighbour_count);
|
||||||
|
sample.direct_dups = static_cast<uint16_t>(direct_dups);
|
||||||
|
sample.flood_dups = static_cast<uint16_t>(flood_dups);
|
||||||
|
sample.last_rssi_x4 = static_cast<int16_t>(last_rssi_x4);
|
||||||
|
sample.last_snr_x4 = static_cast<int16_t>(last_snr_x4);
|
||||||
|
sample.flags = static_cast<uint8_t>(flags);
|
||||||
|
sample.battery_pct = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::restoreSummaryLog() {
|
||||||
|
if (!isArchiveAvailable() || _sample_capacity == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr || !fs->exists("/stats/summary.log")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveReadWithRecovery(_archive, "/stats/summary.log");
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("summary restore open failed path=%s", "/stats/summary.log");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_restored_sample_count = 0;
|
||||||
|
const size_t size = static_cast<size_t>(file.size());
|
||||||
|
const size_t start = (size > kSummaryRestoreWindowBytes) ? (size - kSummaryRestoreWindowBytes) : 0;
|
||||||
|
if (start > 0 && !file.seek(start)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[192];
|
||||||
|
size_t line_len = 0;
|
||||||
|
bool skip_partial = (start > 0);
|
||||||
|
while (file.available()) {
|
||||||
|
const int raw = file.read();
|
||||||
|
if (raw < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const char ch = static_cast<char>(raw);
|
||||||
|
if (skip_partial) {
|
||||||
|
if (ch == '\n') {
|
||||||
|
skip_partial = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\n') {
|
||||||
|
line[line_len] = 0;
|
||||||
|
HistorySample sample{};
|
||||||
|
if (parseSummaryLine(line, sample)) {
|
||||||
|
storeSample(sample, false);
|
||||||
|
_restored_sample_count++;
|
||||||
|
}
|
||||||
|
line_len = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line_len + 1 < sizeof(line)) {
|
||||||
|
line[line_len++] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skip_partial && line_len > 0) {
|
||||||
|
line[line_len] = 0;
|
||||||
|
HistorySample sample{};
|
||||||
|
if (parseSummaryLine(line, sample)) {
|
||||||
|
storeSample(sample, false);
|
||||||
|
_restored_sample_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return _restored_sample_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::parseEventLine(const char* line, HistoryEvent& event) const {
|
||||||
|
if (line == nullptr || line[0] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long epoch_secs = 0;
|
||||||
|
unsigned long uptime_secs = 0;
|
||||||
|
char type_name[32];
|
||||||
|
int value = 0;
|
||||||
|
memset(type_name, 0, sizeof(type_name));
|
||||||
|
|
||||||
|
const int parsed = sscanf(line, "%lu,%lu,%31[^,],%d", &epoch_secs, &uptime_secs, type_name, &value);
|
||||||
|
if (parsed != 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t type = eventTypeFromName(type_name);
|
||||||
|
if (type == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&event, 0, sizeof(event));
|
||||||
|
event.type = type;
|
||||||
|
event.epoch_secs = static_cast<uint32_t>(epoch_secs);
|
||||||
|
event.uptime_secs = static_cast<uint32_t>(uptime_secs);
|
||||||
|
event.value = static_cast<int16_t>(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::restoreEventsLog() {
|
||||||
|
if (!isArchiveAvailable() || _event_capacity == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr || !fs->exists("/stats/events.log")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveRead(fs, "/stats/events.log");
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t size = static_cast<size_t>(file.size());
|
||||||
|
const size_t start = (size > kEventsRestoreWindowBytes) ? (size - kEventsRestoreWindowBytes) : 0;
|
||||||
|
if (start > 0 && !file.seek(start)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[160];
|
||||||
|
size_t line_len = 0;
|
||||||
|
bool skip_partial = (start > 0);
|
||||||
|
while (file.available()) {
|
||||||
|
const int raw = file.read();
|
||||||
|
if (raw < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const char ch = static_cast<char>(raw);
|
||||||
|
if (skip_partial) {
|
||||||
|
if (ch == '\n') {
|
||||||
|
skip_partial = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\n') {
|
||||||
|
line[line_len] = 0;
|
||||||
|
HistoryEvent event{};
|
||||||
|
if (parseEventLine(line, event)) {
|
||||||
|
storeEvent(event, false);
|
||||||
|
}
|
||||||
|
line_len = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line_len + 1 < sizeof(line)) {
|
||||||
|
line[line_len++] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skip_partial && line_len > 0) {
|
||||||
|
line[line_len] = 0;
|
||||||
|
HistoryEvent event{};
|
||||||
|
if (parseEventLine(line, event)) {
|
||||||
|
storeEvent(event, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return _event_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::pushSample(const HistorySample& sample) {
|
||||||
|
if (!_enabled || !ensureBuffers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeSample(sample, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::appendPendingEvent(const HistoryEvent& event) {
|
||||||
|
if (_pending_event_count < (sizeof(_pending_events) / sizeof(_pending_events[0]))) {
|
||||||
|
_pending_events[_pending_event_count++] = event;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memmove(&_pending_events[0], &_pending_events[1], sizeof(_pending_events) - sizeof(_pending_events[0]));
|
||||||
|
_pending_events[(sizeof(_pending_events) / sizeof(_pending_events[0])) - 1] = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::storeEvent(const HistoryEvent& event, bool queue_pending) {
|
||||||
|
if (!ensureBuffers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_events[_event_head] = event;
|
||||||
|
_event_head = (_event_head + 1) % _event_capacity;
|
||||||
|
if (_event_count < _event_capacity) {
|
||||||
|
_event_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue_pending) {
|
||||||
|
appendPendingEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::recordEvent(uint8_t type, uint32_t epoch_secs, uint32_t uptime_secs, int16_t value) {
|
||||||
|
if (!ensureBuffers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryEvent event{};
|
||||||
|
event.type = type;
|
||||||
|
event.epoch_secs = epoch_secs;
|
||||||
|
event.uptime_secs = uptime_secs;
|
||||||
|
event.value = value;
|
||||||
|
storeEvent(event, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::maybeFlush(uint32_t now_ms) {
|
||||||
|
if (!_enabled || !isArchiveAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_summary_dirty && (now_ms >= _next_summary_flush_ms || _pending_event_count >= 8)) {
|
||||||
|
flushSummaryLog();
|
||||||
|
_summary_dirty = false;
|
||||||
|
_next_summary_flush_ms = now_ms + kSummaryFlushIntervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pending_event_count > 0 && now_ms >= _next_event_flush_ms) {
|
||||||
|
flushEventsLog();
|
||||||
|
_next_event_flush_ms = now_ms + kEventFlushIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::flushSummaryLog() {
|
||||||
|
if (_archive == nullptr || _sample_count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HistorySample latest{};
|
||||||
|
if (!getSampleFromOldest(_sample_count - 1, latest)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveAppendWithRecovery(_archive, "/stats/summary.log");
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("summary open failed path=%s", "/stats/summary.log");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"%lu,%lu,%u,%u,%d,%d,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u\n",
|
||||||
|
static_cast<unsigned long>(latest.epoch_secs),
|
||||||
|
static_cast<unsigned long>(latest.uptime_secs),
|
||||||
|
static_cast<unsigned>(latest.battery_mv),
|
||||||
|
static_cast<unsigned>(latest.queue_len),
|
||||||
|
static_cast<int>(latest.last_rssi_x4),
|
||||||
|
static_cast<int>(latest.last_snr_x4),
|
||||||
|
static_cast<unsigned>(latest.packets_sent),
|
||||||
|
static_cast<unsigned>(latest.packets_recv),
|
||||||
|
static_cast<unsigned>(latest.heap_free),
|
||||||
|
static_cast<unsigned>(latest.psram_free),
|
||||||
|
static_cast<unsigned>(latest.error_flags),
|
||||||
|
static_cast<unsigned>(latest.recv_errors),
|
||||||
|
static_cast<unsigned>(latest.neighbour_count),
|
||||||
|
static_cast<unsigned>(latest.direct_dups),
|
||||||
|
static_cast<unsigned>(latest.flood_dups),
|
||||||
|
static_cast<unsigned>(latest.flags));
|
||||||
|
const size_t written = file.print(line);
|
||||||
|
file.flush();
|
||||||
|
file.close();
|
||||||
|
ARCHIVE_LOG("summary flushed path=%s bytes=%u sample_count=%u",
|
||||||
|
"/stats/summary.log",
|
||||||
|
static_cast<unsigned>(written),
|
||||||
|
static_cast<unsigned>(_sample_count));
|
||||||
|
writeMetaFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::flushEventsLog() {
|
||||||
|
if (_archive == nullptr || _pending_event_count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveAppendWithRecovery(_archive, "/stats/events.log");
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("events open failed path=%s", "/stats/events.log");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total_written = 0;
|
||||||
|
for (size_t i = 0; i < _pending_event_count; ++i) {
|
||||||
|
const HistoryEvent& event = _pending_events[i];
|
||||||
|
char line[160];
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"%lu,%lu,%s,%d\n",
|
||||||
|
static_cast<unsigned long>(event.epoch_secs),
|
||||||
|
static_cast<unsigned long>(event.uptime_secs),
|
||||||
|
getEventTypeName(event.type),
|
||||||
|
static_cast<int>(event.value));
|
||||||
|
total_written += file.print(line);
|
||||||
|
}
|
||||||
|
file.flush();
|
||||||
|
file.close();
|
||||||
|
ARCHIVE_LOG("events flushed path=%s bytes=%u count=%u",
|
||||||
|
"/stats/events.log",
|
||||||
|
static_cast<unsigned>(total_written),
|
||||||
|
static_cast<unsigned>(_pending_event_count));
|
||||||
|
_pending_event_count = 0;
|
||||||
|
writeMetaFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsHistory::writeMetaFile() const {
|
||||||
|
if (_archive == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FILESYSTEM* fs = _archive->getFS();
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = openArchiveWriteWithRecovery(_archive, "/stats/meta.json");
|
||||||
|
if (!file) {
|
||||||
|
ARCHIVE_LOG("meta open failed path=%s", "/stats/meta.json");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char json[256];
|
||||||
|
snprintf(json, sizeof(json),
|
||||||
|
"{\"logical\":\"%s\",\"path\":\"%s\",\"samples\":{\"count\":%u,\"capacity\":%u},"
|
||||||
|
"\"events\":{\"count\":%u,\"capacity\":%u},\"psram\":%s,\"degraded\":%s}\n",
|
||||||
|
_archive->getLogicalName(),
|
||||||
|
_archive->getLogicalStatsPath(),
|
||||||
|
static_cast<unsigned>(_sample_count),
|
||||||
|
static_cast<unsigned>(_sample_capacity),
|
||||||
|
static_cast<unsigned>(_event_count),
|
||||||
|
static_cast<unsigned>(_event_capacity),
|
||||||
|
_psram_backed ? "true" : "false",
|
||||||
|
_degraded ? "true" : "false");
|
||||||
|
const size_t written = file.print(json);
|
||||||
|
file.flush();
|
||||||
|
file.close();
|
||||||
|
ARCHIVE_LOG("meta flushed path=%s bytes=%u",
|
||||||
|
"/stats/meta.json",
|
||||||
|
static_cast<unsigned>(written));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::getSampleFromOldest(size_t index, HistorySample& sample) const {
|
||||||
|
if (_samples == nullptr || index >= _sample_count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t oldest = (_sample_head + _sample_capacity - _sample_count) % _sample_capacity;
|
||||||
|
const size_t slot = (oldest + index) % _sample_capacity;
|
||||||
|
sample = _samples[slot];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::buildSeriesJson(const char* series, char* buffer, size_t buffer_size, uint32_t now_epoch_secs, uint32_t now_uptime_secs) const {
|
||||||
|
if (buffer == nullptr || buffer_size == 0 || series == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buffer[0] = 0;
|
||||||
|
if (seriesTitle(series)[0] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sample_count == 0) {
|
||||||
|
snprintf(buffer, buffer_size,
|
||||||
|
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":0,\"oldest_age_secs\":0,\"latest_age_secs\":0,\"points\":[]}",
|
||||||
|
series,
|
||||||
|
seriesTitle(series),
|
||||||
|
seriesUnit(series),
|
||||||
|
static_cast<unsigned long>(kSampleIntervalSecs));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t points = min(_sample_count, kMaxSeriesPoints);
|
||||||
|
const size_t step = (_sample_count > kMaxSeriesPoints) ? ((_sample_count + kMaxSeriesPoints - 1) / kMaxSeriesPoints) : 1;
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
HistorySample sample{};
|
||||||
|
HistorySample previous{};
|
||||||
|
bool have_previous = false;
|
||||||
|
int current_value = 0;
|
||||||
|
|
||||||
|
offset += snprintf(&buffer[offset], buffer_size - offset,
|
||||||
|
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":",
|
||||||
|
series,
|
||||||
|
seriesTitle(series),
|
||||||
|
seriesUnit(series),
|
||||||
|
static_cast<unsigned long>(kSampleIntervalSecs));
|
||||||
|
|
||||||
|
getSampleFromOldest(_sample_count - 1, sample);
|
||||||
|
if (strcmp(series, "packets") == 0 && _sample_count >= 2) {
|
||||||
|
getSampleFromOldest(_sample_count - 2, previous);
|
||||||
|
current_value = buildPointValue(sample, &previous, series);
|
||||||
|
} else {
|
||||||
|
current_value = buildPointValue(sample, nullptr, series);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t last_index = 0;
|
||||||
|
for (size_t i = 0, emitted = 0; i < _sample_count && emitted < points; i += step, ++emitted) {
|
||||||
|
last_index = i;
|
||||||
|
}
|
||||||
|
HistorySample oldest_sample{};
|
||||||
|
HistorySample latest_emitted_sample{};
|
||||||
|
const bool have_oldest_sample = getSampleFromOldest(0, oldest_sample);
|
||||||
|
const bool have_latest_emitted_sample = getSampleFromOldest(last_index, latest_emitted_sample);
|
||||||
|
const uint32_t oldest_age_secs = have_oldest_sample ? sampleAgeSecs(oldest_sample, now_epoch_secs, now_uptime_secs) : 0;
|
||||||
|
const uint32_t latest_age_secs = have_latest_emitted_sample ? sampleAgeSecs(latest_emitted_sample, now_epoch_secs, now_uptime_secs) : 0;
|
||||||
|
|
||||||
|
offset += snprintf(&buffer[offset], buffer_size - offset,
|
||||||
|
"%d,\"oldest_age_secs\":%lu,\"latest_age_secs\":%lu,\"points\":[",
|
||||||
|
current_value,
|
||||||
|
static_cast<unsigned long>(oldest_age_secs),
|
||||||
|
static_cast<unsigned long>(latest_age_secs));
|
||||||
|
|
||||||
|
size_t emitted = 0;
|
||||||
|
for (size_t i = 0; i < _sample_count && emitted < points; i += step, ++emitted) {
|
||||||
|
if (!getSampleFromOldest(i, sample)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int value = buildPointValue(sample, have_previous ? &previous : nullptr, series);
|
||||||
|
offset += snprintf(&buffer[offset], buffer_size - offset,
|
||||||
|
"%s[%lu,%d]",
|
||||||
|
emitted == 0 ? "" : ",",
|
||||||
|
static_cast<unsigned long>(sample.uptime_secs),
|
||||||
|
value);
|
||||||
|
previous = sample;
|
||||||
|
have_previous = true;
|
||||||
|
if (offset + 24 >= buffer_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(&buffer[offset], buffer_size - offset, "]}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::getRecentEvent(size_t reverse_index, HistoryEvent& event) const {
|
||||||
|
if (_events == nullptr || reverse_index >= _event_count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t newest = (_event_head + _event_capacity - 1) % _event_capacity;
|
||||||
|
const size_t slot = (newest + _event_capacity - reverse_index) % _event_capacity;
|
||||||
|
event = _events[slot];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* StatsHistory::getEventTypeName(uint8_t type) {
|
||||||
|
switch (type) {
|
||||||
|
case HISTORY_EVENT_BOOT:
|
||||||
|
return "boot";
|
||||||
|
case HISTORY_EVENT_WEB_STARTED:
|
||||||
|
return "web_started";
|
||||||
|
case HISTORY_EVENT_WEB_STOPPED:
|
||||||
|
return "web_stopped";
|
||||||
|
case HISTORY_EVENT_MQTT_CONNECTED:
|
||||||
|
return "mqtt_connected";
|
||||||
|
case HISTORY_EVENT_MQTT_DISCONNECTED:
|
||||||
|
return "mqtt_disconnected";
|
||||||
|
case HISTORY_EVENT_ARCHIVE_MOUNTED:
|
||||||
|
return "archive_mounted";
|
||||||
|
case HISTORY_EVENT_ARCHIVE_UNAVAILABLE:
|
||||||
|
return "archive_unavailable";
|
||||||
|
case HISTORY_EVENT_LOW_MEMORY:
|
||||||
|
return "low_memory";
|
||||||
|
case HISTORY_EVENT_STATS_ENABLED:
|
||||||
|
return "stats_enabled";
|
||||||
|
case HISTORY_EVENT_STATS_DISABLED:
|
||||||
|
return "stats_disabled";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <MeshCore.h>
|
||||||
|
|
||||||
|
#include "ArchiveStorage.h"
|
||||||
|
|
||||||
|
struct HistorySample {
|
||||||
|
uint32_t epoch_secs;
|
||||||
|
uint32_t uptime_secs;
|
||||||
|
uint32_t packets_sent;
|
||||||
|
uint32_t packets_recv;
|
||||||
|
uint32_t heap_free;
|
||||||
|
uint32_t heap_min;
|
||||||
|
uint32_t psram_free;
|
||||||
|
uint32_t psram_min;
|
||||||
|
uint16_t battery_mv;
|
||||||
|
uint16_t queue_len;
|
||||||
|
uint16_t error_flags;
|
||||||
|
uint16_t recv_errors;
|
||||||
|
uint16_t neighbour_count;
|
||||||
|
uint16_t direct_dups;
|
||||||
|
uint16_t flood_dups;
|
||||||
|
int16_t last_rssi_x4;
|
||||||
|
int16_t last_snr_x4;
|
||||||
|
int16_t noise_floor;
|
||||||
|
uint8_t flags;
|
||||||
|
int8_t battery_pct;
|
||||||
|
uint16_t reserved;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HistoryEvent {
|
||||||
|
uint32_t epoch_secs;
|
||||||
|
uint32_t uptime_secs;
|
||||||
|
uint8_t type;
|
||||||
|
int16_t value;
|
||||||
|
uint8_t reserved;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum HistorySampleFlags : uint8_t {
|
||||||
|
HISTORY_FLAG_EXTERNAL_POWER = 1 << 0,
|
||||||
|
HISTORY_FLAG_CHARGING = 1 << 1,
|
||||||
|
HISTORY_FLAG_VBUS = 1 << 2,
|
||||||
|
HISTORY_FLAG_WIFI_CONNECTED = 1 << 3,
|
||||||
|
HISTORY_FLAG_MQTT_CONNECTED = 1 << 4,
|
||||||
|
HISTORY_FLAG_WEB_ENABLED = 1 << 5,
|
||||||
|
HISTORY_FLAG_WEB_PANEL_UP = 1 << 6,
|
||||||
|
HISTORY_FLAG_ARCHIVE_MOUNTED = 1 << 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum HistoryEventType : uint8_t {
|
||||||
|
HISTORY_EVENT_BOOT = 1,
|
||||||
|
HISTORY_EVENT_WEB_STARTED = 2,
|
||||||
|
HISTORY_EVENT_WEB_STOPPED = 3,
|
||||||
|
HISTORY_EVENT_MQTT_CONNECTED = 4,
|
||||||
|
HISTORY_EVENT_MQTT_DISCONNECTED = 5,
|
||||||
|
HISTORY_EVENT_ARCHIVE_MOUNTED = 6,
|
||||||
|
HISTORY_EVENT_ARCHIVE_UNAVAILABLE = 7,
|
||||||
|
HISTORY_EVENT_LOW_MEMORY = 8,
|
||||||
|
HISTORY_EVENT_STATS_ENABLED = 9,
|
||||||
|
HISTORY_EVENT_STATS_DISABLED = 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
class StatsHistory {
|
||||||
|
public:
|
||||||
|
static constexpr uint32_t kSampleIntervalSecs = 60;
|
||||||
|
static constexpr uint32_t kArchiveSummaryIntervalSecs = 300;
|
||||||
|
|
||||||
|
StatsHistory();
|
||||||
|
~StatsHistory();
|
||||||
|
|
||||||
|
void begin(bool enabled, ArchiveStorage* archive);
|
||||||
|
void setArchive(ArchiveStorage* archive);
|
||||||
|
void setEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool isEnabled() const { return _enabled; }
|
||||||
|
bool isRecentHistoryAvailable() const { return _samples != nullptr; }
|
||||||
|
bool isPsramBacked() const { return _psram_backed; }
|
||||||
|
bool isDegraded() const { return _degraded; }
|
||||||
|
bool isArchiveAvailable() const;
|
||||||
|
bool hasArchiveRestore() const { return _restored_sample_count > 0; }
|
||||||
|
|
||||||
|
size_t getSampleCapacity() const { return _sample_capacity; }
|
||||||
|
size_t getSampleCount() const { return _sample_count; }
|
||||||
|
size_t getEventCapacity() const { return _event_capacity; }
|
||||||
|
size_t getEventCount() const { return _event_count; }
|
||||||
|
size_t getRestoredSampleCount() const { return _restored_sample_count; }
|
||||||
|
|
||||||
|
void pushSample(const HistorySample& sample);
|
||||||
|
void recordEvent(uint8_t type, uint32_t epoch_secs, uint32_t uptime_secs, int16_t value = 0);
|
||||||
|
void maybeFlush(uint32_t now_ms);
|
||||||
|
|
||||||
|
bool buildSeriesJson(const char* series, char* buffer, size_t buffer_size, uint32_t now_epoch_secs, uint32_t now_uptime_secs) const;
|
||||||
|
bool getRecentEvent(size_t reverse_index, HistoryEvent& event) const;
|
||||||
|
|
||||||
|
static const char* getEventTypeName(uint8_t type);
|
||||||
|
static constexpr uint32_t getSampleIntervalSecs() { return kSampleIntervalSecs; }
|
||||||
|
static constexpr uint32_t getArchiveSummaryIntervalSecs() { return kArchiveSummaryIntervalSecs; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ensureBuffers();
|
||||||
|
bool restoreSummaryLog();
|
||||||
|
bool restoreEventsLog();
|
||||||
|
bool parseSummaryLine(const char* line, HistorySample& sample) const;
|
||||||
|
bool parseEventLine(const char* line, HistoryEvent& event) const;
|
||||||
|
void storeSample(const HistorySample& sample, bool mark_dirty);
|
||||||
|
void storeEvent(const HistoryEvent& event, bool queue_pending);
|
||||||
|
void appendPendingEvent(const HistoryEvent& event);
|
||||||
|
void flushSummaryLog();
|
||||||
|
void flushEventsLog();
|
||||||
|
void writeMetaFile() const;
|
||||||
|
bool getSampleFromOldest(size_t index, HistorySample& sample) const;
|
||||||
|
|
||||||
|
bool _enabled;
|
||||||
|
bool _psram_backed;
|
||||||
|
bool _degraded;
|
||||||
|
bool _summary_dirty;
|
||||||
|
uint32_t _next_summary_flush_ms;
|
||||||
|
uint32_t _next_event_flush_ms;
|
||||||
|
size_t _sample_capacity;
|
||||||
|
size_t _sample_head;
|
||||||
|
size_t _sample_count;
|
||||||
|
size_t _event_capacity;
|
||||||
|
size_t _event_head;
|
||||||
|
size_t _event_count;
|
||||||
|
HistorySample* _samples;
|
||||||
|
HistoryEvent* _events;
|
||||||
|
ArchiveStorage* _archive;
|
||||||
|
size_t _restored_sample_count;
|
||||||
|
HistoryEvent _pending_events[16];
|
||||||
|
size_t _pending_event_count;
|
||||||
|
};
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
#define P_BOARD_SPI_MOSI 35 //SPI for SD Card and QMI8653 (IMU)
|
#define P_BOARD_SPI_MOSI 35 //SPI for SD Card and QMI8653 (IMU)
|
||||||
#define P_BOARD_SPI_MISO 37 //SPI for SD Card and QMI8653 (IMU)
|
#define P_BOARD_SPI_MISO 37 //SPI for SD Card and QMI8653 (IMU)
|
||||||
#define P_BOARD_SPI_SCK 36 //SPI for SD Card and QMI8653 (IMU)
|
#define P_BOARD_SPI_SCK 36 //SPI for SD Card and QMI8653 (IMU)
|
||||||
#define P_BPARD_SPI_CS 47 //Pin for SD Card CS
|
#define P_BOARD_SPI_CS 47 //Pin for SD Card CS
|
||||||
#define P_BOARD_IMU_CS 34 //Pin for QMI8653 (IMU) CS
|
#define P_BOARD_IMU_CS 34 //Pin for QMI8653 (IMU) CS
|
||||||
|
|
||||||
#define P_BOARD_IMU_INT 33 //IMU Int pin
|
#define P_BOARD_IMU_INT 33 //IMU Int pin
|
||||||
@@ -154,8 +154,24 @@ public:
|
|||||||
esp_deep_sleep_start(); // CPU halts here and never returns!
|
esp_deep_sleep_start(); // CPU halts here and never returns!
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getBattMilliVolts(){
|
uint16_t getBattMilliVolts() override {
|
||||||
return PMU->getBattVoltage();
|
return PMU != NULL ? PMU->getBattVoltage() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getBatteryPercent() override {
|
||||||
|
return (PMU != NULL && PMU->isBatteryConnect()) ? PMU->getBatteryPercent() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCharging() override {
|
||||||
|
return PMU != NULL && PMU->isCharging();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isVbusPresent() override {
|
||||||
|
return PMU != NULL && PMU->isVbusIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isExternalPowered() override {
|
||||||
|
return isVbusPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* getManufacturerName() const{
|
const char* getManufacturerName() const{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
#include <helpers/TxtDataHelpers.h>
|
#include <helpers/TxtDataHelpers.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -17,15 +18,16 @@ void MQTTPrefsStore::setDefaults(MQTTPrefs& prefs) {
|
|||||||
prefs.raw_enabled = 0;
|
prefs.raw_enabled = 0;
|
||||||
prefs.status_enabled = 1;
|
prefs.status_enabled = 1;
|
||||||
prefs.tx_enabled = 0;
|
prefs.tx_enabled = 0;
|
||||||
prefs.web_enabled = 1;
|
prefs.deprecated_web_enabled = 0;
|
||||||
prefs.wifi_powersave = 0;
|
prefs.deprecated_web_stats_enabled = 0;
|
||||||
|
prefs.legacy_wifi_powersave = 0;
|
||||||
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
||||||
StrHelper::strncpy(prefs.iata, MQTT_DEFAULT_IATA, sizeof(prefs.iata));
|
StrHelper::strncpy(prefs.iata, MQTT_DEFAULT_IATA, sizeof(prefs.iata));
|
||||||
#ifdef WIFI_SSID
|
#ifdef WIFI_SSID
|
||||||
StrHelper::strncpy(prefs.wifi_ssid, WIFI_SSID, sizeof(prefs.wifi_ssid));
|
StrHelper::strncpy(prefs.legacy_wifi_ssid, WIFI_SSID, sizeof(prefs.legacy_wifi_ssid));
|
||||||
#endif
|
#endif
|
||||||
#ifdef WIFI_PWD
|
#ifdef WIFI_PWD
|
||||||
StrHelper::strncpy(prefs.wifi_pwd, WIFI_PWD, sizeof(prefs.wifi_pwd));
|
StrHelper::strncpy(prefs.legacy_wifi_pwd, WIFI_PWD, sizeof(prefs.legacy_wifi_pwd));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +60,9 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
prefs = persisted;
|
prefs = persisted;
|
||||||
if (prefs.wifi_powersave > 2) {
|
if (prefs.legacy_wifi_powersave > 2) {
|
||||||
prefs.wifi_powersave = 0;
|
prefs.legacy_wifi_powersave = 0;
|
||||||
}
|
}
|
||||||
prefs.web_enabled = prefs.web_enabled ? 1 : 0;
|
|
||||||
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
||||||
prefs.enabled_mask &= 0x07;
|
prefs.enabled_mask &= 0x07;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ struct MQTTPrefs {
|
|||||||
uint8_t raw_enabled;
|
uint8_t raw_enabled;
|
||||||
uint8_t status_enabled;
|
uint8_t status_enabled;
|
||||||
uint8_t tx_enabled;
|
uint8_t tx_enabled;
|
||||||
uint8_t web_enabled;
|
uint8_t deprecated_web_enabled;
|
||||||
uint8_t wifi_powersave;
|
uint8_t legacy_wifi_powersave;
|
||||||
uint32_t status_interval_ms;
|
uint32_t status_interval_ms;
|
||||||
char iata[8];
|
char iata[8];
|
||||||
char wifi_ssid[33];
|
char legacy_wifi_ssid[33];
|
||||||
char wifi_pwd[65];
|
char legacy_wifi_pwd[65];
|
||||||
char owner_public_key[65];
|
char owner_public_key[65];
|
||||||
char owner_email[96];
|
char owner_email[96];
|
||||||
|
uint8_t deprecated_web_stats_enabled;
|
||||||
|
uint8_t reserved[3];
|
||||||
};
|
};
|
||||||
|
|
||||||
class MQTTPrefsStore {
|
class MQTTPrefsStore {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#include <esp_idf_version.h>
|
#include <esp_idf_version.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_sntp.h>
|
|
||||||
#include <helpers/TxtDataHelpers.h>
|
#include <helpers/TxtDataHelpers.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -35,21 +34,13 @@
|
|||||||
|
|
||||||
#if MQTT_DEBUG
|
#if MQTT_DEBUG
|
||||||
#define LOG_CAT(tag, fmt, ...) Serial.printf("[" tag "] " fmt "\n", ##__VA_ARGS__)
|
#define LOG_CAT(tag, fmt, ...) Serial.printf("[" tag "] " fmt "\n", ##__VA_ARGS__)
|
||||||
#define WIFI_LOG(fmt, ...) LOG_CAT("WIFI", fmt, ##__VA_ARGS__)
|
|
||||||
#define WEB_LOG(fmt, ...) LOG_CAT("WEB", fmt, ##__VA_ARGS__)
|
|
||||||
#define TIME_LOG(fmt, ...) LOG_CAT("TIME", fmt, ##__VA_ARGS__)
|
|
||||||
#define MQTT_LOG(fmt, ...) LOG_CAT("MQTT", fmt, ##__VA_ARGS__)
|
#define MQTT_LOG(fmt, ...) LOG_CAT("MQTT", fmt, ##__VA_ARGS__)
|
||||||
#else
|
#else
|
||||||
#define LOG_CAT(...) do { } while (0)
|
#define LOG_CAT(...) do { } while (0)
|
||||||
#define WIFI_LOG(...) do { } while (0)
|
|
||||||
#define WEB_LOG(...) do { } while (0)
|
|
||||||
#define TIME_LOG(...) do { } while (0)
|
|
||||||
#define MQTT_LOG(...) do { } while (0)
|
#define MQTT_LOG(...) do { } while (0)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long kWifiRetryMillis = 15000;
|
|
||||||
constexpr unsigned long kWifiConnectTimeoutMillis = 45000;
|
|
||||||
constexpr unsigned long kBrokerRetryBaseMillis = 10000;
|
constexpr unsigned long kBrokerRetryBaseMillis = 10000;
|
||||||
constexpr unsigned long kBrokerRetryMaxMillis = 300000;
|
constexpr unsigned long kBrokerRetryMaxMillis = 300000;
|
||||||
constexpr size_t kBrokerTokenSize = 640;
|
constexpr size_t kBrokerTokenSize = 640;
|
||||||
@@ -57,29 +48,6 @@ constexpr time_t kTokenLifetimeSecs = 3600;
|
|||||||
constexpr time_t kTokenRefreshSlackSecs = 300;
|
constexpr time_t kTokenRefreshSlackSecs = 300;
|
||||||
constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z
|
constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z
|
||||||
|
|
||||||
int getWifiQualityPercent(int rssi_dbm) {
|
|
||||||
if (rssi_dbm <= -100) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (rssi_dbm >= -50) {
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
return 2 * (rssi_dbm + 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* getWifiQualityLabel(int rssi_dbm) {
|
|
||||||
if (rssi_dbm >= -60) {
|
|
||||||
return "excellent";
|
|
||||||
}
|
|
||||||
if (rssi_dbm >= -67) {
|
|
||||||
return "good";
|
|
||||||
}
|
|
||||||
if (rssi_dbm >= -75) {
|
|
||||||
return "fair";
|
|
||||||
}
|
|
||||||
return "poor";
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long getBrokerRetryDelayMillis(uint8_t failures) {
|
unsigned long getBrokerRetryDelayMillis(uint8_t failures) {
|
||||||
unsigned long delay_ms = kBrokerRetryBaseMillis;
|
unsigned long delay_ms = kBrokerRetryBaseMillis;
|
||||||
if (failures > 0) {
|
if (failures > 0) {
|
||||||
@@ -106,20 +74,6 @@ void freeScratchBuffer(void* ptr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* getWifiStateLabel(const MQTTPrefs& prefs, bool wifi_started) {
|
|
||||||
if (prefs.wifi_ssid[0] == 0) {
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
wl_status_t status = WiFi.status();
|
|
||||||
if (status == WL_CONNECTED) {
|
|
||||||
return "up";
|
|
||||||
}
|
|
||||||
if (wifi_started) {
|
|
||||||
return "conn";
|
|
||||||
}
|
|
||||||
return "down";
|
|
||||||
}
|
|
||||||
|
|
||||||
#if MQTT_DEBUG
|
#if MQTT_DEBUG
|
||||||
void logMqttMemorySnapshot(const char* phase, const char* broker_label = nullptr) {
|
void logMqttMemorySnapshot(const char* phase, const char* broker_label = nullptr) {
|
||||||
MQTT_LOG("mem phase=%s broker=%s uptime_ms=%lu heap_free=%u heap_min=%u heap_max=%u psram_free=%u psram_min=%u "
|
MQTT_LOG("mem phase=%s broker=%s uptime_ms=%lu heap_free=%u heap_min=%u heap_max=%u psram_free=%u psram_min=%u "
|
||||||
@@ -150,10 +104,8 @@ const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
|
MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
|
||||||
: _fs(nullptr), _rtc(&rtc), _identity(&identity), _running(false), _wifi_started(false), _sntp_started(false),
|
: _fs(nullptr), _rtc(&rtc), _identity(&identity), _running(false), _last_status_publish(0), _last_status{},
|
||||||
_have_time_sync(false), _last_wifi_attempt(0), _last_status_publish(0), _last_status{},
|
_node_name(nullptr), _network(nullptr)
|
||||||
_node_name(nullptr),
|
|
||||||
_web_runner(nullptr)
|
|
||||||
{
|
{
|
||||||
memset(_device_id, 0, sizeof(_device_id));
|
memset(_device_id, 0, sizeof(_device_id));
|
||||||
MQTTPrefsStore::setDefaults(_prefs);
|
MQTTPrefsStore::setDefaults(_prefs);
|
||||||
@@ -164,11 +116,6 @@ MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
|
|||||||
MQTT_LOG("uplink init");
|
MQTT_LOG("uplink init");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::setWebCommandRunner(MQTTWebCommandRunner* runner) {
|
|
||||||
_web_runner = runner;
|
|
||||||
_web_panel.setCommandRunner(runner);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::savePrefs() {
|
bool MQTTUplink::savePrefs() {
|
||||||
return MQTTPrefsStore::save(_fs, _prefs);
|
return MQTTPrefsStore::save(_fs, _prefs);
|
||||||
}
|
}
|
||||||
@@ -203,24 +150,8 @@ bool MQTTUplink::isActive() const {
|
|||||||
return _running && hasEnabledBroker();
|
return _running && hasEnabledBroker();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::reconnectWifi() {
|
|
||||||
WIFI_LOG("reset");
|
|
||||||
stopWebServer();
|
|
||||||
for (BrokerState& broker : _brokers) {
|
|
||||||
destroyBroker(broker);
|
|
||||||
}
|
|
||||||
if (_wifi_started) {
|
|
||||||
WiFi.disconnect(true, true);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
}
|
|
||||||
_wifi_started = false;
|
|
||||||
_sntp_started = false;
|
|
||||||
_have_time_sync = false;
|
|
||||||
_last_wifi_attempt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::sendStatusNow() {
|
bool MQTTUplink::sendStatusNow() {
|
||||||
if (!_running || !_have_time_sync || WiFi.status() != WL_CONNECTED) {
|
if (!_running || _network == nullptr || !_network->hasTimeSync() || !_network->isWifiConnected()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,28 +258,6 @@ void MQTTUplink::escapeJsonString(const char* input, char* output, size_t output
|
|||||||
output[oi] = 0;
|
output[oi] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
wifi_ps_type_t MQTTUplink::toEspPowerSave(uint8_t mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case 1:
|
|
||||||
return WIFI_PS_MIN_MODEM;
|
|
||||||
case 2:
|
|
||||||
return WIFI_PS_MAX_MODEM;
|
|
||||||
default:
|
|
||||||
return WIFI_PS_NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* MQTTUplink::getPowerSaveLabel(uint8_t mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case 1:
|
|
||||||
return "min";
|
|
||||||
case 2:
|
|
||||||
return "max";
|
|
||||||
default:
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTUplink::refreshIdentityStrings() {
|
void MQTTUplink::refreshIdentityStrings() {
|
||||||
bytesToHexUpper(_identity->pub_key, PUB_KEY_SIZE, _device_id, sizeof(_device_id));
|
bytesToHexUpper(_identity->pub_key, PUB_KEY_SIZE, _device_id, sizeof(_device_id));
|
||||||
for (BrokerState& broker : _brokers) {
|
for (BrokerState& broker : _brokers) {
|
||||||
@@ -388,7 +297,7 @@ void MQTTUplink::refreshBrokerState(BrokerState& broker) {
|
|||||||
bool MQTTUplink::refreshToken(BrokerState& broker) {
|
bool MQTTUplink::refreshToken(BrokerState& broker) {
|
||||||
time_t now = time(nullptr);
|
time_t now = time(nullptr);
|
||||||
if (now < kMinSaneEpoch) {
|
if (now < kMinSaneEpoch) {
|
||||||
TIME_LOG("%s token skipped: clock not ready (%lu)", broker.spec->label, static_cast<unsigned long>(now));
|
MQTT_LOG("%s token skipped: clock not ready (%lu)", broker.spec->label, static_cast<unsigned long>(now));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,94 +594,6 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::stopWebServer() {
|
|
||||||
_web_panel.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTUplink::ensureWebServer() {
|
|
||||||
if (_web_runner == nullptr || _prefs.web_enabled == 0 || !_wifi_started || WiFi.status() != WL_CONNECTED) {
|
|
||||||
stopWebServer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_web_panel.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTUplink::ensureWifi() {
|
|
||||||
if (_prefs.wifi_ssid[0] == 0) {
|
|
||||||
WIFI_LOG("disabled: no ssid");
|
|
||||||
stopWebServer();
|
|
||||||
reconnectWifi();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasEnabledBroker() && _web_runner == nullptr) {
|
|
||||||
WIFI_LOG("disabled: no mqtt endpoints and no web runner");
|
|
||||||
stopWebServer();
|
|
||||||
if (_wifi_started) {
|
|
||||||
WiFi.disconnect(true, true);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
_wifi_started = false;
|
|
||||||
_sntp_started = false;
|
|
||||||
_have_time_sync = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long now_ms = millis();
|
|
||||||
wl_status_t status = WiFi.status();
|
|
||||||
if (_wifi_started) {
|
|
||||||
if (_last_wifi_attempt != 0 && status == WL_IDLE_STATUS && now_ms - _last_wifi_attempt < kWifiConnectTimeoutMillis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (now_ms - _last_wifi_attempt < kWifiRetryMillis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_wifi_started) {
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave));
|
|
||||||
_wifi_started = true;
|
|
||||||
WIFI_LOG("sta start powersaving=%s", getPowerSaveLabel(_prefs.wifi_powersave));
|
|
||||||
} else {
|
|
||||||
WIFI_LOG("retry status=%d", static_cast<int>(status));
|
|
||||||
}
|
|
||||||
_last_wifi_attempt = now_ms;
|
|
||||||
WIFI_LOG("begin ssid=%s", _prefs.wifi_ssid);
|
|
||||||
WiFi.begin(_prefs.wifi_ssid, _prefs.wifi_pwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTUplink::updateTimeSync() {
|
|
||||||
bool prev_have_time_sync = _have_time_sync;
|
|
||||||
if (!_wifi_started || WiFi.status() != WL_CONNECTED) {
|
|
||||||
_have_time_sync = false;
|
|
||||||
if (prev_have_time_sync != _have_time_sync) {
|
|
||||||
TIME_LOG("sntp lost");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_sntp_started) {
|
|
||||||
configTzTime("UTC0", "au.pool.ntp.org", "time.google.com", "time.cloudflare.com");
|
|
||||||
_sntp_started = true;
|
|
||||||
TIME_LOG("sntp start servers=au.pool.ntp.org,time.google.com,time.cloudflare.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
sntp_sync_status_t sync_status = sntp_get_sync_status();
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
bool sane_time = now >= kMinSaneEpoch;
|
|
||||||
bool sync_ready = sync_status == SNTP_SYNC_STATUS_COMPLETED || sync_status == SNTP_SYNC_STATUS_IN_PROGRESS;
|
|
||||||
_have_time_sync = sane_time && (sync_ready || prev_have_time_sync);
|
|
||||||
if (prev_have_time_sync != _have_time_sync) {
|
|
||||||
TIME_LOG("sntp %s epoch=%lu status=%ld", _have_time_sync ? "ready" : "waiting",
|
|
||||||
static_cast<unsigned long>(now), static_cast<long>(sync_status));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
||||||
if (broker.spec == nullptr) {
|
if (broker.spec == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -787,7 +608,7 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_have_time_sync || WiFi.status() != WL_CONNECTED) {
|
if (_network == nullptr || !_network->hasTimeSync() || !_network->isWifiConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,24 +720,15 @@ void MQTTUplink::begin(FILESYSTEM* fs) {
|
|||||||
refreshIdentityStrings();
|
refreshIdentityStrings();
|
||||||
_running = true;
|
_running = true;
|
||||||
_last_status_publish = millis();
|
_last_status_publish = millis();
|
||||||
MQTT_LOG("begin iata=%s enabled_mask=0x%02X wifi_ssid=%s", _prefs.iata, _prefs.enabled_mask, _prefs.wifi_ssid);
|
MQTT_LOG("begin iata=%s enabled_mask=0x%02X", _prefs.iata, _prefs.enabled_mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::end() {
|
void MQTTUplink::end() {
|
||||||
MQTT_LOG("end");
|
MQTT_LOG("end");
|
||||||
publishStatus(false);
|
publishStatus(false);
|
||||||
stopWebServer();
|
|
||||||
for (BrokerState& broker : _brokers) {
|
for (BrokerState& broker : _brokers) {
|
||||||
destroyBroker(broker);
|
destroyBroker(broker);
|
||||||
}
|
}
|
||||||
if (_wifi_started) {
|
|
||||||
WiFi.disconnect(true, true);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
}
|
|
||||||
_wifi_started = false;
|
|
||||||
_sntp_started = false;
|
|
||||||
_have_time_sync = false;
|
|
||||||
_last_wifi_attempt = 0;
|
|
||||||
_running = false;
|
_running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,13 +738,6 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_last_status = snapshot;
|
_last_status = snapshot;
|
||||||
ensureWifi();
|
|
||||||
updateTimeSync();
|
|
||||||
ensureWebServer();
|
|
||||||
if (_web_panel.isRunning() && _web_panel.shouldAutoLock(millis())) {
|
|
||||||
WEB_LOG("idle lock");
|
|
||||||
_web_panel.lockSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
BrokerState* active_connecting_broker = nullptr;
|
BrokerState* active_connecting_broker = nullptr;
|
||||||
for (BrokerState& broker : _brokers) {
|
for (BrokerState& broker : _brokers) {
|
||||||
@@ -958,7 +763,7 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_prefs.status_enabled && hasEnabledBroker() && _have_time_sync &&
|
if (_prefs.status_enabled && hasEnabledBroker() && _network != nullptr && _network->hasTimeSync() &&
|
||||||
millis() - _last_status_publish >= _prefs.status_interval_ms) {
|
millis() - _last_status_publish >= _prefs.status_interval_ms) {
|
||||||
publishStatus(true);
|
publishStatus(true);
|
||||||
_last_status_publish = millis();
|
_last_status_publish = millis();
|
||||||
@@ -966,7 +771,8 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::publishPacket(const mesh::Packet& packet, bool is_tx, int rssi, float snr, int score, int duration) {
|
void MQTTUplink::publishPacket(const mesh::Packet& packet, bool is_tx, int rssi, float snr, int score, int duration) {
|
||||||
if (!_running || !_have_time_sync || !hasEnabledBroker() || !_prefs.packets_enabled) {
|
if (!_running || _network == nullptr || !_network->hasTimeSync() || !_network->isWifiConnected() || !hasEnabledBroker() ||
|
||||||
|
!_prefs.packets_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (is_tx && !_prefs.tx_enabled) {
|
if (is_tx && !_prefs.tx_enabled) {
|
||||||
@@ -1035,7 +841,7 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
|
|||||||
if (broker->connected) {
|
if (broker->connected) {
|
||||||
return "up";
|
return "up";
|
||||||
}
|
}
|
||||||
if (WiFi.status() != WL_CONNECTED || !_have_time_sync) {
|
if (_network == nullptr || !_network->isWifiConnected() || !_network->hasTimeSync()) {
|
||||||
return "wait";
|
return "wait";
|
||||||
}
|
}
|
||||||
if (broker->client != nullptr) {
|
if (broker->client != nullptr) {
|
||||||
@@ -1048,36 +854,13 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
|
|||||||
};
|
};
|
||||||
|
|
||||||
snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s status:%s tx:%s",
|
snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s status:%s tx:%s",
|
||||||
getWifiStateLabel(_prefs, _wifi_started), _have_time_sync ? "up" : "wait", _prefs.iata,
|
(_network != nullptr && _network->isWifiConnected()) ? "up" : "down",
|
||||||
|
(_network != nullptr && _network->hasTimeSync()) ? "up" : "wait",
|
||||||
|
_prefs.iata,
|
||||||
broker_state(kEastmeshBit), broker_state(kLetsmeshEuBit), broker_state(kLetsmeshUsBit),
|
broker_state(kEastmeshBit), broker_state(kLetsmeshEuBit), broker_state(kLetsmeshUsBit),
|
||||||
_prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off");
|
_prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::formatWebStatusReply(char* reply, size_t reply_size) const {
|
|
||||||
#if WITH_WEB_PANEL
|
|
||||||
if (_web_runner == nullptr) {
|
|
||||||
snprintf(reply, reply_size, "> web:off");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_prefs.web_enabled == 0) {
|
|
||||||
snprintf(reply, reply_size, "> web:off");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_web_panel.isRunning() || !_wifi_started || WiFi.status() != WL_CONNECTED) {
|
|
||||||
snprintf(reply, reply_size, "> web:down");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(reply, reply_size, "> web:up url:https://%s/ auth:%s", WiFi.localIP().toString().c_str(),
|
|
||||||
_web_panel.hasSessionToken() ? "unlocked" : "locked");
|
|
||||||
#else
|
|
||||||
(void)reply_size;
|
|
||||||
snprintf(reply, reply_size, "> web:unsupported");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) {
|
bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) {
|
||||||
uint8_t next_mask = _prefs.enabled_mask & 0x07;
|
uint8_t next_mask = _prefs.enabled_mask & 0x07;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -1117,20 +900,6 @@ bool MQTTUplink::setTxEnabled(bool enabled) {
|
|||||||
return savePrefs();
|
return savePrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MQTTUplink::setWebEnabled(bool enabled) {
|
|
||||||
#if WITH_WEB_PANEL
|
|
||||||
_prefs.web_enabled = enabled ? 1 : 0;
|
|
||||||
bool ok = savePrefs();
|
|
||||||
if (_prefs.web_enabled != 0) {
|
|
||||||
ensureWebServer();
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
#else
|
|
||||||
(void)enabled;
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::setIata(const char* iata) {
|
bool MQTTUplink::setIata(const char* iata) {
|
||||||
if (iata == nullptr || *iata == 0) {
|
if (iata == nullptr || *iata == 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1147,54 +916,6 @@ bool MQTTUplink::setIata(const char* iata) {
|
|||||||
return savePrefs();
|
return savePrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MQTTUplink::setWifiPowerSave(const char* mode) {
|
|
||||||
if (mode == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t next_mode;
|
|
||||||
if (strcmp(mode, "none") == 0) {
|
|
||||||
next_mode = 0;
|
|
||||||
} else if (strcmp(mode, "min") == 0) {
|
|
||||||
next_mode = 1;
|
|
||||||
} else if (strcmp(mode, "max") == 0) {
|
|
||||||
next_mode = 2;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_prefs.wifi_powersave = next_mode;
|
|
||||||
bool ok = savePrefs();
|
|
||||||
if (_wifi_started) {
|
|
||||||
ok = WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave)) && ok;
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* MQTTUplink::getWifiPowerSave() const {
|
|
||||||
return getPowerSaveLabel(_prefs.wifi_powersave);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::setWifiSSID(const char* ssid) {
|
|
||||||
if (ssid == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
StrHelper::strncpy(_prefs.wifi_ssid, ssid, sizeof(_prefs.wifi_ssid));
|
|
||||||
bool ok = savePrefs();
|
|
||||||
reconnectWifi();
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::setWifiPassword(const char* pwd) {
|
|
||||||
if (pwd == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
StrHelper::strncpy(_prefs.wifi_pwd, pwd, sizeof(_prefs.wifi_pwd));
|
|
||||||
bool ok = savePrefs();
|
|
||||||
reconnectWifi();
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MQTTUplink::setOwnerPublicKey(const char* owner_public_key) {
|
bool MQTTUplink::setOwnerPublicKey(const char* owner_public_key) {
|
||||||
if (owner_public_key == nullptr) {
|
if (owner_public_key == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1227,66 +948,21 @@ bool MQTTUplink::setOwnerEmail(const char* owner_email) {
|
|||||||
return savePrefs();
|
return savePrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUplink::formatWifiStatusReply(char* reply, size_t reply_size) const {
|
bool MQTTUplink::isAnyBrokerConnected() const {
|
||||||
const char* status = "disconnected";
|
for (const BrokerState& broker : _brokers) {
|
||||||
const char* state = "disconnected";
|
if (broker.spec != nullptr && broker.connected) {
|
||||||
wl_status_t wifi_status = WiFi.status();
|
return true;
|
||||||
if (_prefs.wifi_ssid[0] == 0) {
|
}
|
||||||
status = "unconfigured";
|
|
||||||
state = "unconfigured";
|
|
||||||
} else if (wifi_status == WL_CONNECTED) {
|
|
||||||
status = "connected";
|
|
||||||
state = "connected";
|
|
||||||
} else if (_wifi_started) {
|
|
||||||
status = "connecting";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (wifi_status) {
|
|
||||||
case WL_IDLE_STATUS:
|
|
||||||
state = "idle";
|
|
||||||
break;
|
|
||||||
case WL_NO_SSID_AVAIL:
|
|
||||||
state = "no_ssid";
|
|
||||||
break;
|
|
||||||
case WL_SCAN_COMPLETED:
|
|
||||||
state = "scan_completed";
|
|
||||||
break;
|
|
||||||
case WL_CONNECTED:
|
|
||||||
state = "connected";
|
|
||||||
break;
|
|
||||||
case WL_CONNECT_FAILED:
|
|
||||||
state = "connect_failed";
|
|
||||||
break;
|
|
||||||
case WL_CONNECTION_LOST:
|
|
||||||
state = "connection_lost";
|
|
||||||
break;
|
|
||||||
case WL_DISCONNECTED:
|
|
||||||
state = "disconnected";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state = "unknown";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wifi_status == WL_CONNECTED) {
|
|
||||||
const int rssi_dbm = WiFi.RSSI();
|
|
||||||
snprintf(reply, reply_size,
|
|
||||||
"> ssid:%s status:%s code:%d state:%s ip:%s rssi:%d quality:%d%% signal:%s",
|
|
||||||
_prefs.wifi_ssid, status, static_cast<int>(wifi_status), state, WiFi.localIP().toString().c_str(),
|
|
||||||
rssi_dbm, getWifiQualityPercent(rssi_dbm), getWifiQualityLabel(rssi_dbm));
|
|
||||||
} else {
|
|
||||||
snprintf(reply, reply_size, "> ssid:%s status:%s code:%d state:%s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "-",
|
|
||||||
status, static_cast<int>(wifi_status), state);
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
MQTTUplink::MQTTUplink(mesh::RTCClock&, mesh::LocalIdentity&)
|
MQTTUplink::MQTTUplink(mesh::RTCClock&, mesh::LocalIdentity&)
|
||||||
: _fs(nullptr), _rtc(nullptr), _identity(nullptr), _running(false), _wifi_started(false), _sntp_started(false),
|
: _fs(nullptr), _rtc(nullptr), _identity(nullptr), _running(false), _last_status_publish(0), _last_status{},
|
||||||
_have_time_sync(false), _last_wifi_attempt(0), _last_status_publish(0), _last_status{},
|
_node_name(nullptr), _network(nullptr) {
|
||||||
_node_name(nullptr),
|
MQTTPrefsStore::setDefaults(_prefs);
|
||||||
_web_runner(nullptr) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MQTTUplink::savePrefs() { return false; }
|
bool MQTTUplink::savePrefs() { return false; }
|
||||||
@@ -1295,25 +971,18 @@ void MQTTUplink::end() {}
|
|||||||
void MQTTUplink::loop(const MQTTStatusSnapshot&) {}
|
void MQTTUplink::loop(const MQTTStatusSnapshot&) {}
|
||||||
void MQTTUplink::publishPacket(const mesh::Packet&, bool, int, float, int, int) {}
|
void MQTTUplink::publishPacket(const mesh::Packet&, bool, int, float, int, int) {}
|
||||||
void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const { snprintf(reply, reply_size, "> unsupported"); }
|
void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const { snprintf(reply, reply_size, "> unsupported"); }
|
||||||
void MQTTUplink::formatWebStatusReply(char* reply, size_t reply_size) const { snprintf(reply, reply_size, "> unsupported"); }
|
|
||||||
bool MQTTUplink::setEndpointEnabled(uint8_t, bool) { return false; }
|
bool MQTTUplink::setEndpointEnabled(uint8_t, bool) { return false; }
|
||||||
bool MQTTUplink::isEndpointEnabled(uint8_t) const { return false; }
|
bool MQTTUplink::isEndpointEnabled(uint8_t) const { return false; }
|
||||||
bool MQTTUplink::setPacketsEnabled(bool) { return false; }
|
bool MQTTUplink::setPacketsEnabled(bool) { return false; }
|
||||||
bool MQTTUplink::setRawEnabled(bool) { return false; }
|
bool MQTTUplink::setRawEnabled(bool) { return false; }
|
||||||
bool MQTTUplink::setStatusEnabled(bool) { return false; }
|
bool MQTTUplink::setStatusEnabled(bool) { return false; }
|
||||||
bool MQTTUplink::setTxEnabled(bool) { return false; }
|
bool MQTTUplink::setTxEnabled(bool) { return false; }
|
||||||
bool MQTTUplink::setWebEnabled(bool) { return false; }
|
|
||||||
bool MQTTUplink::setIata(const char*) { return false; }
|
bool MQTTUplink::setIata(const char*) { return false; }
|
||||||
bool MQTTUplink::setWifiPowerSave(const char*) { return false; }
|
|
||||||
const char* MQTTUplink::getWifiPowerSave() const { return "unsupported"; }
|
|
||||||
bool MQTTUplink::isActive() const { return false; }
|
bool MQTTUplink::isActive() const { return false; }
|
||||||
bool MQTTUplink::setWifiSSID(const char*) { return false; }
|
|
||||||
bool MQTTUplink::setWifiPassword(const char*) { return false; }
|
|
||||||
bool MQTTUplink::setOwnerPublicKey(const char*) { return false; }
|
bool MQTTUplink::setOwnerPublicKey(const char*) { return false; }
|
||||||
bool MQTTUplink::setOwnerEmail(const char*) { return false; }
|
bool MQTTUplink::setOwnerEmail(const char*) { return false; }
|
||||||
void MQTTUplink::formatWifiStatusReply(char* reply, size_t reply_size) const { snprintf(reply, reply_size, "> unsupported"); }
|
|
||||||
void MQTTUplink::reconnectWifi() {}
|
|
||||||
bool MQTTUplink::sendStatusNow() { return false; }
|
bool MQTTUplink::sendStatusNow() { return false; }
|
||||||
|
bool MQTTUplink::isAnyBrokerConnected() const { return false; }
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,16 @@
|
|||||||
#ifdef WITH_MQTT_UPLINK
|
#ifdef WITH_MQTT_UPLINK
|
||||||
|
|
||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
#include <helpers/CommonCLI.h>
|
#include <helpers/NetworkStateProvider.h>
|
||||||
#include <helpers/web/WebPanelServer.h>
|
|
||||||
#include <target.h>
|
#include <target.h>
|
||||||
|
|
||||||
#include "JWTHelper.h"
|
#include "JWTHelper.h"
|
||||||
#include "MQTTPrefs.h"
|
#include "MQTTPrefs.h"
|
||||||
|
|
||||||
#if defined(ESP_PLATFORM)
|
#if defined(ESP_PLATFORM)
|
||||||
#include <WiFi.h>
|
|
||||||
#if !defined(WITH_WEB_PANEL)
|
|
||||||
#define WITH_WEB_PANEL 1
|
|
||||||
#endif
|
|
||||||
#include <mqtt_client.h>
|
#include <mqtt_client.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
using MQTTWebCommandRunner = WebPanelCommandRunner;
|
|
||||||
|
|
||||||
struct MQTTStatusSnapshot {
|
struct MQTTStatusSnapshot {
|
||||||
int battery_mv;
|
int battery_mv;
|
||||||
uint32_t uptime_secs;
|
uint32_t uptime_secs;
|
||||||
@@ -49,7 +42,6 @@ public:
|
|||||||
bool isActive() const;
|
bool isActive() const;
|
||||||
|
|
||||||
void formatStatusReply(char* reply, size_t reply_size) const;
|
void formatStatusReply(char* reply, size_t reply_size) const;
|
||||||
void formatWebStatusReply(char* reply, size_t reply_size) const;
|
|
||||||
|
|
||||||
bool setEndpointEnabled(uint8_t bit, bool enabled);
|
bool setEndpointEnabled(uint8_t bit, bool enabled);
|
||||||
bool isEndpointEnabled(uint8_t bit) const;
|
bool isEndpointEnabled(uint8_t bit) const;
|
||||||
@@ -61,24 +53,16 @@ public:
|
|||||||
bool isStatusEnabled() const { return _prefs.status_enabled != 0; }
|
bool isStatusEnabled() const { return _prefs.status_enabled != 0; }
|
||||||
bool setTxEnabled(bool enabled);
|
bool setTxEnabled(bool enabled);
|
||||||
bool isTxEnabled() const { return _prefs.tx_enabled != 0; }
|
bool isTxEnabled() const { return _prefs.tx_enabled != 0; }
|
||||||
bool setWebEnabled(bool enabled);
|
|
||||||
bool isWebEnabled() const { return _prefs.web_enabled != 0; }
|
|
||||||
bool setIata(const char* iata);
|
bool setIata(const char* iata);
|
||||||
const char* getIata() const { return _prefs.iata; }
|
const char* getIata() const { return _prefs.iata; }
|
||||||
void setNodeNameSource(const char* node_name) { _node_name = node_name; }
|
void setNodeNameSource(const char* node_name) { _node_name = node_name; }
|
||||||
void setWebCommandRunner(MQTTWebCommandRunner* runner);
|
|
||||||
bool setWifiSSID(const char* ssid);
|
|
||||||
bool setWifiPassword(const char* pwd);
|
|
||||||
const char* getWifiSSID() const { return _prefs.wifi_ssid; }
|
|
||||||
bool setWifiPowerSave(const char* mode);
|
|
||||||
const char* getWifiPowerSave() const;
|
|
||||||
bool setOwnerPublicKey(const char* owner_public_key);
|
bool setOwnerPublicKey(const char* owner_public_key);
|
||||||
const char* getOwnerPublicKey() const { return _prefs.owner_public_key; }
|
const char* getOwnerPublicKey() const { return _prefs.owner_public_key; }
|
||||||
bool setOwnerEmail(const char* owner_email);
|
bool setOwnerEmail(const char* owner_email);
|
||||||
const char* getOwnerEmail() const { return _prefs.owner_email; }
|
const char* getOwnerEmail() const { return _prefs.owner_email; }
|
||||||
void formatWifiStatusReply(char* reply, size_t reply_size) const;
|
|
||||||
void reconnectWifi();
|
|
||||||
bool sendStatusNow();
|
bool sendStatusNow();
|
||||||
|
bool isAnyBrokerConnected() const;
|
||||||
|
void setNetworkStateProvider(NetworkStateProvider* network) { _network = network; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#if defined(ESP_PLATFORM)
|
#if defined(ESP_PLATFORM)
|
||||||
@@ -114,16 +98,11 @@ private:
|
|||||||
mesh::LocalIdentity* _identity;
|
mesh::LocalIdentity* _identity;
|
||||||
MQTTPrefs _prefs;
|
MQTTPrefs _prefs;
|
||||||
bool _running;
|
bool _running;
|
||||||
bool _wifi_started;
|
|
||||||
bool _sntp_started;
|
|
||||||
bool _have_time_sync;
|
|
||||||
unsigned long _last_wifi_attempt;
|
|
||||||
unsigned long _last_status_publish;
|
unsigned long _last_status_publish;
|
||||||
MQTTStatusSnapshot _last_status;
|
MQTTStatusSnapshot _last_status;
|
||||||
char _device_id[65];
|
char _device_id[65];
|
||||||
const char* _node_name;
|
const char* _node_name;
|
||||||
MQTTWebCommandRunner* _web_runner;
|
NetworkStateProvider* _network;
|
||||||
WebPanelServer _web_panel;
|
|
||||||
|
|
||||||
#if defined(ESP_PLATFORM)
|
#if defined(ESP_PLATFORM)
|
||||||
static constexpr uint8_t kEastmeshBit = 0x01;
|
static constexpr uint8_t kEastmeshBit = 0x01;
|
||||||
@@ -135,10 +114,6 @@ private:
|
|||||||
BrokerState _brokers[3];
|
BrokerState _brokers[3];
|
||||||
|
|
||||||
static void handleMqttEvent(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data);
|
static void handleMqttEvent(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data);
|
||||||
void ensureWebServer();
|
|
||||||
void stopWebServer();
|
|
||||||
void ensureWifi();
|
|
||||||
void updateTimeSync();
|
|
||||||
bool hasEnabledBroker() const;
|
bool hasEnabledBroker() const;
|
||||||
static uint8_t normalizeEnabledMask(uint8_t mask);
|
static uint8_t normalizeEnabledMask(uint8_t mask);
|
||||||
void formatTopic(char* dst, size_t dst_size, const char* leaf) const;
|
void formatTopic(char* dst, size_t dst_size, const char* leaf) const;
|
||||||
@@ -155,8 +130,6 @@ private:
|
|||||||
int buildPacketJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr,
|
int buildPacketJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr,
|
||||||
int score, int duration) const;
|
int score, int duration) const;
|
||||||
int buildRawJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr) const;
|
int buildRawJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr) const;
|
||||||
static wifi_ps_type_t toEspPowerSave(uint8_t mode);
|
|
||||||
static const char* getPowerSaveLabel(uint8_t mode);
|
|
||||||
static void escapeJsonString(const char* input, char* output, size_t output_size);
|
static void escapeJsonString(const char* input, char* output, size_t output_size);
|
||||||
static void makeSafeToken(const char* input, char* output, size_t output_size);
|
static void makeSafeToken(const char* input, char* output, size_t output_size);
|
||||||
static void bytesToHexUpper(const uint8_t* src, size_t len, char* dst, size_t dst_size);
|
static void bytesToHexUpper(const uint8_t* src, size_t len, char* dst, size_t dst_size);
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -17,6 +17,20 @@ public:
|
|||||||
virtual ~WebPanelCommandRunner() = default;
|
virtual ~WebPanelCommandRunner() = default;
|
||||||
virtual void runWebCommand(const char* command, char* reply, size_t reply_size) = 0;
|
virtual void runWebCommand(const char* command, char* reply, size_t reply_size) = 0;
|
||||||
virtual const char* getWebAdminPassword() const = 0;
|
virtual const char* getWebAdminPassword() const = 0;
|
||||||
|
virtual bool isWebStatsEnabled() const { return false; }
|
||||||
|
virtual bool formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
||||||
|
if (reply != nullptr && reply_size > 0) {
|
||||||
|
reply[0] = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
virtual bool formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) {
|
||||||
|
(void)series;
|
||||||
|
if (reply != nullptr && reply_size > 0) {
|
||||||
|
reply[0] = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class WebPanelServer {
|
class WebPanelServer {
|
||||||
@@ -45,6 +59,7 @@ private:
|
|||||||
|
|
||||||
static esp_err_t handleIndex(httpd_req_t* req);
|
static esp_err_t handleIndex(httpd_req_t* req);
|
||||||
static esp_err_t handleApp(httpd_req_t* req);
|
static esp_err_t handleApp(httpd_req_t* req);
|
||||||
|
static esp_err_t handleStatsPage(httpd_req_t* req);
|
||||||
static esp_err_t handleLogin(httpd_req_t* req);
|
static esp_err_t handleLogin(httpd_req_t* req);
|
||||||
static esp_err_t handleCommand(httpd_req_t* req);
|
static esp_err_t handleCommand(httpd_req_t* req);
|
||||||
static esp_err_t handleStats(httpd_req_t* req);
|
static esp_err_t handleStats(httpd_req_t* req);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#include "WebPrefs.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void WebPrefsStore::setDefaults(WebPrefs& prefs) {
|
||||||
|
memset(&prefs, 0, sizeof(prefs));
|
||||||
|
prefs.magic = kMagic;
|
||||||
|
prefs.web_enabled = 1;
|
||||||
|
prefs.web_stats_enabled = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebPrefsStore::load(FILESYSTEM* fs, WebPrefs& prefs) {
|
||||||
|
setDefaults(prefs);
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs->exists(kFilename)) {
|
||||||
|
save(fs, prefs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
File file = fs->open(kFilename, "r");
|
||||||
|
#else
|
||||||
|
File file = fs->open(kFilename);
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebPrefs persisted{};
|
||||||
|
size_t bytes_to_read = min(static_cast<size_t>(file.size()), sizeof(persisted));
|
||||||
|
bool ok = bytes_to_read >= sizeof(persisted.magic) &&
|
||||||
|
file.read(reinterpret_cast<uint8_t*>(&persisted), bytes_to_read) == bytes_to_read;
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (!ok || persisted.magic != kMagic) {
|
||||||
|
fs->remove(kFilename);
|
||||||
|
save(fs, prefs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs = persisted;
|
||||||
|
prefs.web_enabled = prefs.web_enabled ? 1 : 0;
|
||||||
|
prefs.web_stats_enabled = prefs.web_stats_enabled ? 1 : 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebPrefsStore::save(FILESYSTEM* fs, const WebPrefs& prefs) {
|
||||||
|
if (fs == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fs->exists(kFilename) && !fs->remove(kFilename)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
File file = fs->open(kFilename, "w");
|
||||||
|
#else
|
||||||
|
File file = fs->open(kFilename, "w", true);
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok = file.write(reinterpret_cast<const uint8_t*>(&prefs), sizeof(prefs)) == sizeof(prefs);
|
||||||
|
file.close();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct WebPrefs {
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t web_enabled;
|
||||||
|
uint8_t web_stats_enabled;
|
||||||
|
uint8_t reserved[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebPrefsStore {
|
||||||
|
public:
|
||||||
|
static void setDefaults(WebPrefs& prefs);
|
||||||
|
static bool load(FILESYSTEM* fs, WebPrefs& prefs);
|
||||||
|
static bool save(FILESYSTEM* fs, const WebPrefs& prefs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t kMagic = 0x57454250;
|
||||||
|
static constexpr const char* kFilename = "/web_prefs";
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "WebService.h"
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
#include <WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WebService::WebService() : _fs(nullptr), _prefs{}, _runner(nullptr), _network(nullptr) {
|
||||||
|
WebPrefsStore::setDefaults(_prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebService::begin(FILESYSTEM* fs) {
|
||||||
|
_fs = fs;
|
||||||
|
WebPrefsStore::load(_fs, _prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebService::end() {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
_panel.stop();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebService::loop() {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
ensureWebServer();
|
||||||
|
if (_panel.isRunning() && _panel.shouldAutoLock(millis())) {
|
||||||
|
_panel.lockSession();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebService::setCommandRunner(WebPanelCommandRunner* runner) {
|
||||||
|
_runner = runner;
|
||||||
|
_panel.setCommandRunner(runner);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebService::savePrefs() {
|
||||||
|
return WebPrefsStore::save(_fs, _prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebService::setWebEnabled(bool enabled) {
|
||||||
|
_prefs.web_enabled = enabled ? 1 : 0;
|
||||||
|
bool ok = savePrefs();
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
if (_prefs.web_enabled != 0) {
|
||||||
|
ensureWebServer();
|
||||||
|
} else {
|
||||||
|
_panel.stop();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebService::setWebStatsEnabled(bool enabled) {
|
||||||
|
_prefs.web_stats_enabled = enabled ? 1 : 0;
|
||||||
|
return savePrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebService::formatWebStatusReply(char* reply, size_t reply_size) const {
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
if (_runner == nullptr || _prefs.web_enabled == 0) {
|
||||||
|
snprintf(reply, reply_size, "> web:off");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_panel.isRunning() || _network == nullptr || !_network->isWifiConnected()) {
|
||||||
|
snprintf(reply, reply_size, "> web:down");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(reply, reply_size, "> web:up url:https://%s/ auth:%s", WiFi.localIP().toString().c_str(),
|
||||||
|
_panel.hasSessionToken() ? "unlocked" : "locked");
|
||||||
|
#else
|
||||||
|
snprintf(reply, reply_size, "> web:unsupported");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
void WebService::ensureWebServer() {
|
||||||
|
if (_runner == nullptr || _prefs.web_enabled == 0 || _network == nullptr || !_network->isWifiConnected()) {
|
||||||
|
_panel.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_panel.start();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <helpers/NetworkStateProvider.h>
|
||||||
|
|
||||||
|
#include "WebPanelServer.h"
|
||||||
|
#include "WebPrefs.h"
|
||||||
|
|
||||||
|
class WebService {
|
||||||
|
public:
|
||||||
|
WebService();
|
||||||
|
|
||||||
|
void begin(FILESYSTEM* fs);
|
||||||
|
void end();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
void setCommandRunner(WebPanelCommandRunner* runner);
|
||||||
|
void setNetworkStateProvider(NetworkStateProvider* network) { _network = network; }
|
||||||
|
|
||||||
|
bool setWebEnabled(bool enabled);
|
||||||
|
bool isWebEnabled() const { return _prefs.web_enabled != 0; }
|
||||||
|
bool setWebStatsEnabled(bool enabled);
|
||||||
|
bool isWebStatsEnabled() const { return _prefs.web_stats_enabled != 0; }
|
||||||
|
|
||||||
|
void formatWebStatusReply(char* reply, size_t reply_size) const;
|
||||||
|
bool isPanelRunning() const { return _panel.isRunning(); }
|
||||||
|
bool isPanelUnlocked() const { return _panel.hasSessionToken(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||||
|
void ensureWebServer();
|
||||||
|
#endif
|
||||||
|
bool savePrefs();
|
||||||
|
|
||||||
|
FILESYSTEM* _fs;
|
||||||
|
WebPrefs _prefs;
|
||||||
|
WebPanelCommandRunner* _runner;
|
||||||
|
NetworkStateProvider* _network;
|
||||||
|
WebPanelServer _panel;
|
||||||
|
};
|
||||||
Ссылка в новой задаче
Block a user