mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 05:21:16 +00:00
feat: prepare v1.16.07 firmware release
This commit is contained in:
1 parent
129e687c02
commit
ccce0c8d75
95 files changed
+5550
-278
No files matched your search
+63
-5
@@ -4,7 +4,64 @@ This document describes the MQTT bridge implementation that allows MeshCore repe
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### Essential Commands to Get MQTT Observer Running
|
||||
### Browser setup (recommended)
|
||||
|
||||
Normal ESP32 MQTT repeater-, room-server-, and WiFi-companion builds include an
|
||||
all-in-one WebConfig portal. The same page is used by non-MQTT ESP32 builds,
|
||||
where the MQTT tab and wizard step are removed at runtime.
|
||||
|
||||
1. Flash an observer build such as `heltec_v4_repeater_observer_mqtt`.
|
||||
2. On a fresh node with no saved WiFi SSID, join the open
|
||||
`MeshCore-Setup-XXXX` access point. The captive page should open
|
||||
automatically; otherwise browse to <http://192.168.4.1/>.
|
||||
3. Complete the wizard, review the settings, and choose **Save & Reboot**. The
|
||||
MQTT bridge remains stopped while the setup AP owns WiFi and starts normally
|
||||
after the reboot.
|
||||
4. Verify the connections on the portal's status page or with
|
||||
`get mqtt.status` from the CLI.
|
||||
|
||||
The setup AP stops after 10 minutes with no connected client. WiFi companion
|
||||
builds enable the WebUI by default. Repeater and room-server builds default it
|
||||
off; use `set webui on` for a persistent start or `start webconfig` for only the
|
||||
current boot. `get webui` reports the active URL.
|
||||
|
||||
To change an already-configured repeater/room-server observer temporarily, log
|
||||
in through its CLI and run:
|
||||
|
||||
```text
|
||||
start webconfig
|
||||
```
|
||||
|
||||
Open the reported LAN URL and sign in with the node's admin password. The LAN
|
||||
portal runs until `stop webconfig` or a reboot. To force the captive setup AP,
|
||||
first stop the MQTT bridge, then start the portal in AP mode:
|
||||
|
||||
```text
|
||||
set bridge off
|
||||
start webconfig ap
|
||||
```
|
||||
|
||||
`start webconfig ap` intentionally refuses to take WiFi away from a running
|
||||
bridge. The forced AP is open, so stop it when finished and re-enable the bridge
|
||||
if you did not reboot:
|
||||
|
||||
```text
|
||||
stop webconfig
|
||||
set bridge on
|
||||
```
|
||||
|
||||
The classic 4 MB ESP32
|
||||
`LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` and
|
||||
`LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` targets omit the browser
|
||||
portal because the async web-server code does not fit while retaining two app
|
||||
slots for LoRa OTA. Configure those two builds with the CLI below.
|
||||
|
||||
MQTT WiFi companions use this same wizard instead of the former two-page setup.
|
||||
The portal remains on the companion's station IP, alongside the companion
|
||||
protocol on TCP port 5000. Because companions have no admin CLI password, their
|
||||
LAN page is intentionally unauthenticated; use a trusted WiFi network.
|
||||
|
||||
### CLI setup and fallback
|
||||
|
||||
**1. Flash the observer firmware to your device**
|
||||
|
||||
@@ -153,7 +210,7 @@ pio run -e LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt
|
||||
pio run -e LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt
|
||||
```
|
||||
|
||||
**TLora naming:** The env prefix `LilyGo_TLora_V2_1_1_6` is LilyGo’s **T-LoRa V2.1–1.6** board (SX1276); PlatformIO selects **`ttgo-lora32-v1`** (TTGO LoRa32 V1.0). **MQTT observer** envs extend a slim base **without** `sensor_base` so the image fits `min_spiffs`; **all other** `LilyGo_TLora_V2_1_1_6_*` targets still use optional I2C environmental sensors as before. The **`lilygo_tlora_c6`** variant is separate hardware (ESP32-C6).
|
||||
**TLora naming:** The env prefix `LilyGo_TLora_V2_1_1_6` is LilyGo’s **T-LoRa V2.1–1.6** board (SX1276); PlatformIO selects **`ttgo-lora32-v1`** (TTGO LoRa32 V1.0). **MQTT observer** envs extend a slim base **without** `sensor_base` so they retain dual-app OTA on the 4 MB flash; **all other** `LilyGo_TLora_V2_1_1_6_*` targets still use optional I2C environmental sensors as before. The repeater observer also keeps 256 recent-repeater entries instead of the normal ESP32 default of 2,048. The **`lilygo_tlora_c6`** variant is separate hardware (ESP32-C6).
|
||||
|
||||
**T-LoRa V2.1–1.6 MQTT observer — one WSS broker:** This hardware is **classic ESP32 without PSRAM**. Each WSS preset uses a full TLS stack and large contiguous heap allocations; **two active broker presets at once** typically fails the second connection (`mbedtls_ssl_setup` / `esp-tls` `0x8017`, low `IntMax` in `memory`). **Treat these observer builds as supporting one active cloud preset:** configure the broker you need in `mqtt1` or `mqtt2`, and set the other slot to `none` (e.g. `set mqtt2.preset none`). Use PSRAM-capable boards if you need multiple simultaneous MQTT uplinks.
|
||||
|
||||
@@ -165,8 +222,8 @@ Some MQTT observer builds use a non-default partition table to accommodate the l
|
||||
|-------------|----------------|------------|---------------|-------|
|
||||
| `LilyGo_T3S3_sx1262_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
|
||||
| `LilyGo_T3S3_sx1262_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
|
||||
| `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | TTGO LoRa32 V1.0; observer omits `sensor_base`. **One active WSS broker** recommended (no PSRAM; dual TLS usually fails on the second slot). |
|
||||
| `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | same |
|
||||
| `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` | `dual_ota_1984k.csv` | 4 MB | 1.9375 MB | 64 KB SPIFFS; no coredump partition. **One active WSS broker** recommended (no PSRAM; dual TLS usually fails on the second slot). |
|
||||
| `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | TTGO LoRa32 V1.0; observer omits `sensor_base`; one active WSS broker recommended. |
|
||||
| `Station_G2_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
|
||||
| `Station_G2_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
|
||||
| `LilyGo_TBeam_1W_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | Set in `boards/t_beam_1w.json`; required vs implicit `default.csv` |
|
||||
@@ -176,7 +233,8 @@ Some MQTT observer builds use a non-default partition table to accommodate the l
|
||||
|
||||
Flashing a **full merged image** (`*-merged.bin` at offset `0x0`) writes a new bootloader **and** partition table. If that layout **differs** from what is already on the device, **NVS is typically wiped or invalidated** — expect to lose stored configuration (admin preferences, WiFi, MQTT slots, name, etc.) and reconfigure from scratch.
|
||||
|
||||
- **`LilyGo_TLora_V2_1_1_6_*_observer_mqtt`:** These use the **same** `min_spiffs.csv` layout as other MeshCore TLora builds, so moving between repeater / room server / MQTT observer does **not** require a different partition table for normal upgrades. **If you previously installed an older TLora MQTT observer that used `huge_app.csv`,** flashing this firmware switches back to `min_spiffs` — treat that as a **partition layout change** (merged flash; NVS may be reset). **If you install MeshCore on a device that used a non-MeshCore partition map,** the first merged flash can still **wipe** settings.
|
||||
- **`LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt`:** This uses the custom `dual_ota_1984k.csv` layout. Install its merged image when coming from a standard TLora build, the room-server observer, or any older `huge_app.csv` build; expect to reconfigure after that partition change.
|
||||
- **`LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt`:** This retains the normal `min_spiffs.csv` layout. Moving from another `min_spiffs` TLora build does not itself require a partition change, but coming from the repeater observer's custom layout, `huge_app.csv`, or a non-MeshCore layout does.
|
||||
- **`Station_G2_*_observer_mqtt`** and **`LilyGo_TBeam_1W_*_observer_mqtt`**: These use `default_16MB.csv` to accomodate the larger size of the MQTT observer firmware. Installing MQTT observer firmware on these devices requires a **merged** flash the first time. The same applies if you move **from** firmware that was built with a **different** partition table—the first merged flash that installs this layout will **wipe** stored settings.
|
||||
|
||||
**How to flash the merged firmware:**
|
||||
|
||||
+8
-8
@@ -92,8 +92,8 @@ Heltec_t096_companion_radio_ble_femoff
|
||||
# ESP32 - 20 builds
|
||||
./build.sh build-firmware \
|
||||
Heltec_v3_companion_radio_ble_ps \
|
||||
heltec_v4_companion_radio_ble_ps_femon \
|
||||
heltec_v4_3_companion_radio_ble_ps_femoff \
|
||||
heltec_v4_companion_radio_ble_femon \
|
||||
heltec_v4_3_companion_radio_ble_femoff \
|
||||
Xiao_C3_companion_radio_ble_ps \
|
||||
Xiao_S3_companion_radio_ble_ps \
|
||||
Xiao_S3_WIO_companion_radio_ble_ps \
|
||||
@@ -101,15 +101,15 @@ Heltec_v2_companion_radio_ble_ps \
|
||||
LilyGo_TBeam_1W_companion_radio_ble_ps \
|
||||
Heltec_WSL3_companion_radio_ble_ps \
|
||||
Heltec_Wireless_Tracker_companion_radio_ble_ps \
|
||||
heltec_tracker_v2_companion_radio_ble_ps_femon \
|
||||
heltec_tracker_v2_companion_radio_ble_ps_femoff \
|
||||
heltec_tracker_v2_companion_radio_ble_femon \
|
||||
heltec_tracker_v2_companion_radio_ble_femoff \
|
||||
Heltec_Wireless_Paper_companion_radio_ble_ps \
|
||||
LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \
|
||||
Heltec_ct62_companion_radio_ble_ps \
|
||||
T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \
|
||||
Tbeam_SX1262_companion_radio_ble_ps \
|
||||
heltec_v4_expansionkit_tft_companion_radio_ble_ps_femon \
|
||||
heltec_v4_3_expansionkit_tft_companion_radio_ble_ps_femoff \
|
||||
heltec_v4_expansionkit_tft_companion_radio_ble_femon \
|
||||
heltec_v4_3_expansionkit_tft_companion_radio_ble_femoff \
|
||||
LilyGo_T3S3_sx1262_companion_radio_ble_ps
|
||||
|
||||
# Not working
|
||||
@@ -130,8 +130,8 @@ Xiao_C6_repeater_ \
|
||||
RAK_4631_repeater \
|
||||
Heltec_t096_repeater \
|
||||
Heltec_v3_companion_radio_ble_ps \
|
||||
heltec_v4_companion_radio_ble_ps_femon \
|
||||
heltec_v4_3_companion_radio_ble_ps_femoff \
|
||||
heltec_v4_companion_radio_ble_femon \
|
||||
heltec_v4_3_companion_radio_ble_femoff \
|
||||
Xiao_C3_companion_radio_ble_ps \
|
||||
Xiao_C6_companion_radio_ble_ \
|
||||
RAK_4631_companion_radio_ble \
|
||||
|
||||
@@ -1602,6 +1602,9 @@ collect_esp32_artifacts() {
|
||||
local env_name=$1
|
||||
local firmware_filename=$2
|
||||
|
||||
python3 scripts/check_esp32_app_size.py \
|
||||
".pio/build/${env_name}/firmware.bin" \
|
||||
".pio/build/${env_name}/partitions.bin" || return $?
|
||||
pio run -t mergebin -e "$env_name" || return $?
|
||||
copy_build_output ".pio/build/${env_name}/firmware.bin" "${OUTPUT_DIR}/${firmware_filename}.bin" || return $?
|
||||
copy_build_output ".pio/build/${env_name}/firmware-merged.bin" "${OUTPUT_DIR}/${firmware_filename}-merged.bin" || return $?
|
||||
|
||||
+70
-6
@@ -2,6 +2,10 @@
|
||||
|
||||
This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
|
||||
|
||||
The first word of a command is case-insensitive, so `set`, `Set`, and `SET`
|
||||
are equivalent, as are `get`, `Get`, and the other command verbs. The case of
|
||||
arguments such as node names, passwords, and keys is left unchanged.
|
||||
|
||||
## Navigation
|
||||
|
||||
- [Operational](#operational)
|
||||
@@ -100,6 +104,48 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
- `start ota`
|
||||
- `stop ota`
|
||||
|
||||
On an ESP32 build with WebConfig, the manual OTA uploader and WebConfig both use HTTP
|
||||
port 80 and cannot run together. Stop WebConfig before `start ota`, or stop OTA
|
||||
before `start webconfig`.
|
||||
|
||||
---
|
||||
|
||||
### Browser configuration portal (ESP32 repeater and room server)
|
||||
|
||||
**Usage:**
|
||||
|
||||
- `start webconfig`
|
||||
- `start webconfig ap`
|
||||
- `stop webconfig`
|
||||
- `set webui on`
|
||||
- `set webui off`
|
||||
- `get webui`
|
||||
|
||||
`set webui on` is the persistent master switch. It starts the portal now and
|
||||
again after future reboots; `set webui off` closes it and disables that boot
|
||||
start. `get webui` reports the saved on/off state plus whether the portal is
|
||||
inactive, joining WiFi, serving a setup AP, or serving a LAN URL. The default is
|
||||
`off` on repeater and room-server builds. On display-equipped repeaters, an
|
||||
otherwise-unused triple click toggles the same saved setting.
|
||||
|
||||
`start webconfig` is a temporary start that does not change the saved switch.
|
||||
It serves the shared WiFi, radio, flood, loop, and status page and reports its
|
||||
URL. MQTT builds also show the MQTT tab and wizard step; non-MQTT builds remove
|
||||
them entirely. Sign in with the node's admin password. If the node has no saved
|
||||
WiFi SSID, the command starts the open `MeshCore-Setup-XXXX` captive AP at
|
||||
<http://192.168.4.1/> instead.
|
||||
|
||||
`start webconfig ap` forces captive-AP mode. It will not interrupt an active
|
||||
MQTT bridge, so run `set bridge off` first. Use `stop webconfig` to close either
|
||||
mode for the current boot. (`stop webconfig` does not change a saved `webui on`.)
|
||||
LAN mode otherwise remains active until reboot; setup mode stops after 10 minutes
|
||||
with no connected client.
|
||||
|
||||
The browser portal is not compiled into the two 4 MB
|
||||
`LilyGo_TLora_V2_1_1_6_*_observer_mqtt` targets because it does not fit while
|
||||
retaining the two app slots required for LoRa OTA. Their normal CLI settings
|
||||
remain available.
|
||||
|
||||
---
|
||||
|
||||
### Erase/Factory Reset
|
||||
@@ -1221,12 +1267,25 @@ The payload names follow the [MeshCore packet-format allocation](https://docs.me
|
||||
are never affected.
|
||||
|
||||
**Behavior:** A match prevents retransmission by this repeater. The packet is
|
||||
still received and can still be logged. Rules are persistent and the table is
|
||||
empty by default. While the temporary radio is active, only rows explicitly
|
||||
marked `suspend=tempradio` are skipped. `tempradio` is a radio state, not an OTA
|
||||
mode; normal payload types can also use the temporary channel. Other rows
|
||||
remain in force. A malformed persisted table fails open (no general rules are
|
||||
applied).
|
||||
still received and can still be logged. Rules are persistent. While the
|
||||
temporary radio is active, only rows explicitly marked `suspend=tempradio` are
|
||||
skipped. `tempradio` is a radio state, not an OTA mode; normal payload types can
|
||||
also use the temporary channel. Other rows remain in force. A malformed
|
||||
persisted table fails open (no general rules are applied).
|
||||
|
||||
**Default row:** Repeater firmware seeds a new flood-filter table with
|
||||
`ota all suspend=tempradio` in slot 1. This blocks repeated LoRa OTA (`0x0C`)
|
||||
floods at every received hop unless temporary radio is actually active. The OTA
|
||||
core independently refuses OTA receive, relay, and transmit outside temporary
|
||||
radio. The row is editable and deletable; once the table is saved, deletion is
|
||||
persistent. Restore the exact seeded row with:
|
||||
|
||||
```text
|
||||
set flood.filter.1 0x0C all suspend=tempradio
|
||||
```
|
||||
|
||||
Omitting `all` is equivalent. Omit `.1` as well to reuse an identical rule or
|
||||
the first empty slot instead of replacing slot 1.
|
||||
|
||||
**Remote-admin protection:** `flood.filter` cannot block `anon_req`, `path`, or
|
||||
`response` at received hop counts `0-6`; those login-capable types become
|
||||
@@ -1253,6 +1312,7 @@ match it.
|
||||
set flood.filter grp_data 4+
|
||||
set flood.filter.2 PAYLOAD_TYPE_ADVERT 6+
|
||||
set flood.filter ota 2-4
|
||||
set flood.filter.1 0x0C all suspend=tempradio
|
||||
set flood.filter grp_data all suspend=tempradio
|
||||
set flood.filter any 12+
|
||||
get flood.filter
|
||||
@@ -2120,6 +2180,10 @@ clear recent.repeater
|
||||
**Usage:**
|
||||
- `gps sync`
|
||||
|
||||
The GPS must be enabled. When GPS power saving has put an enabled receiver to
|
||||
sleep, this command schedules a sync and wakes it; after `gps off`, it reports
|
||||
`gps is off` without scheduling work.
|
||||
|
||||
---
|
||||
|
||||
#### Set this node's location based on the GPS coordinates
|
||||
|
||||
+24
-3
@@ -765,6 +765,9 @@ After this bootloader is flashed onto the device, you can trigger an over-the-ai
|
||||
6. From your phone or computer, connect to the `MeshCore-OTA` hotspot when one was started.
|
||||
7. Open the URL reported by `start ota` and upload the non-merged bin from the flasher. When the device started `MeshCore-OTA`, the URL is <http://192.168.4.1/update>.
|
||||
|
||||
On an MQTT observer, stop WebConfig before running `start ota`; both servers use
|
||||
HTTP port 80. Likewise, stop the OTA uploader before running `start webconfig`.
|
||||
|
||||
|
||||
### 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
|
||||
**A:** Yes, developer `che aporeps` has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
|
||||
@@ -801,13 +804,31 @@ Where `&type` is:
|
||||
|
||||
### 7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?
|
||||
**A:**
|
||||
Flash a companion Wi-Fi build. If it has no saved Wi-Fi configuration, it starts an open `MeshCore-Setup` access point. Join it, browse to <http://192.168.4.1/>, and submit the network SSID and password. The device saves the credentials only after a successful connection. The page and device screen then show the station IP assigned by your router.
|
||||
Flash an ESP32 companion Wi-Fi build. Its shared WebUI is enabled by default. If
|
||||
it has no saved Wi-Fi configuration, it starts an open `MeshCore-Setup-XXXX`
|
||||
access point. Join it and browse to <http://192.168.4.1/>. The wizard configures
|
||||
WiFi, node identity, and radio settings; MQTT companion builds add an MQTT step
|
||||
and MQTT tab, while non-MQTT companions remove those controls. After saving,
|
||||
the page and device screen show the station IP assigned by your router.
|
||||
|
||||
If a saved network remains unreachable for two minutes, `MeshCore-Setup` starts as a fallback. The node retries the saved SSID and password every two minutes while the setup AP is open. If the original network returns, the node reconnects and closes `MeshCore-Setup` automatically.
|
||||
If a saved network remains unreachable for two minutes, the setup AP starts as
|
||||
a fallback. If the original network returns, the companion closes that recovery
|
||||
portal and resumes normal LAN operation.
|
||||
|
||||
The setup AP always uses `192.168.4.1`; the companion's station IP is assigned by DHCP and can be different. A real `WIFI_SSID` and `WIFI_PWD` can still be supplied at compile time as initial defaults, but they are no longer required.
|
||||
|
||||
The `Heltec_v3_companion_radio_wifi_mqtt` and `heltec_v4_companion_radio_wifi_mqtt` builds use a two-stage first-run setup. The open `MeshCore-Setup` AP serves only the SSID/password form at <http://192.168.4.1/>. After the companion joins that network, reconnect your phone or computer to the same network and open the station IP shown on the setup page or device screen. That second, station-only page asks for the MQTT preset or broker, routing identity, credentials, and publish options. It remains available at the node's station IP so MQTT settings can be changed later; the companion interface remains available on TCP port `5000`.
|
||||
On the LAN, the WebUI remains available at the companion's station IP and the
|
||||
companion protocol remains available on TCP port `5000`. Companion builds do not
|
||||
have an admin CLI password, so this configuration page trusts the local LAN; do
|
||||
not put the companion on an untrusted WiFi network.
|
||||
|
||||
ESP32 repeater and room-server builds use the same page, but keep it off by
|
||||
default. Run `set webui on` from the admin CLI, open the reported LAN URL, and
|
||||
sign in with the admin password; `get webui` reports its current address. Their
|
||||
MQTT observer variants expose the MQTT controls, while ordinary builds do not.
|
||||
The two 4 MB
|
||||
`LilyGo_TLora_V2_1_1_6_*_observer_mqtt` builds retain CLI-only configuration so
|
||||
their flash can keep two app slots for LoRa OTA.
|
||||
|
||||
### 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?
|
||||
**A:**
|
||||
|
||||
+30
-15
@@ -32,11 +32,12 @@ get flood.filter
|
||||
get flood.moderation
|
||||
```
|
||||
|
||||
The `flood.filter` and `flood.moderation` tables each have 16 persistent slots
|
||||
and start empty. A row can opt into `suspend=tempradio`; temporary radio is not
|
||||
synonymous with OTA and can carry normal packet types too. A corrupt or
|
||||
truncated table fails open, so corrupt storage does not silently enable
|
||||
blocking.
|
||||
The `flood.filter` and `flood.moderation` tables each have 16 persistent slots.
|
||||
A new `flood.filter` table starts with `ota all suspend=tempradio` in slot 1;
|
||||
`flood.moderation` starts empty. A row can opt into `suspend=tempradio`;
|
||||
temporary radio is not synonymous with OTA and can carry normal packet types
|
||||
too. A corrupt or truncated table fails open, so corrupt storage does not
|
||||
silently enable blocking.
|
||||
|
||||
## Force unscoped floods into a transport scope
|
||||
|
||||
@@ -90,8 +91,12 @@ type forwardable.
|
||||
LoRa OTA (`0x0C`) falls under `other:*`. A matching row adds the selected
|
||||
transport code, but OTA still operates normally during the temporary-radio
|
||||
window because the OTA handler accepts both unscoped and transport-scoped flood
|
||||
routes. The target region must allow flooding. The OTA core is already dormant
|
||||
outside the temporary-radio window, so no default OTA filter row is installed.
|
||||
routes. The target region must allow flooding. A new repeater also seeds
|
||||
`ota all suspend=tempradio` in flood-filter slot 1. That visible rule blocks OTA
|
||||
forwarding at every received hop outside temporary-radio operation and is
|
||||
skipped while temporary radio is active. Independently, the OTA core refuses
|
||||
OTA receive, relay, and transmit outside an actually active temporary-radio
|
||||
window, even if the seeded row is deleted or replaced.
|
||||
|
||||
Capacity is selected at build time:
|
||||
|
||||
@@ -149,9 +154,18 @@ Without a slot number, `set` reuses an identical rule, including its suspension
|
||||
setting, or selects the first empty slot. With a slot number, it replaces that
|
||||
slot. Omitting the hop expression means `all` (`0-63`).
|
||||
|
||||
No flood-filter row is created automatically. Operators may add
|
||||
`suspend=tempradio` to any row that should be skipped while the radio is on a
|
||||
temporary channel.
|
||||
On first initialization, flood-filter slot 1 is seeded with:
|
||||
|
||||
```text
|
||||
set flood.filter.1 0x0C all suspend=tempradio
|
||||
```
|
||||
|
||||
This is a normal editable row. After the table has been saved, deleting it
|
||||
remains persistent across reboot; the firmware does not recreate it. Run the
|
||||
same command to restore the exact seeded row, or omit `.1` to preserve existing
|
||||
slot assignments and use the first empty slot. Operators may add
|
||||
`suspend=tempradio` to any other row that should be skipped while the radio is
|
||||
on a temporary channel.
|
||||
|
||||
Suspension does not approve a packet or bypass the rest of the filter table. It
|
||||
skips that row, then evaluation continues with the next row and the remaining
|
||||
@@ -358,17 +372,18 @@ Use username and path rules as traffic moderation, not as an authorization
|
||||
boundary. For a strict network boundary, combine these tools with region ACLs,
|
||||
private transport/channel keys, and controlled device access.
|
||||
|
||||
## Restore the factory-seeded channel block
|
||||
## Restore the factory-seeded rows
|
||||
|
||||
The repeater's factory-seeded forwarding row can be restored through the CLI:
|
||||
The repeater's factory-seeded forwarding rows can be restored through the CLI:
|
||||
|
||||
```text
|
||||
set flood.channel.block.1 #wardriving h=4
|
||||
set flood.filter.1 0x0C all suspend=tempradio
|
||||
```
|
||||
|
||||
This explicitly replaces slot 1. Inspect the slot first if it may now contain
|
||||
another rule. To preserve existing slot assignments, omit `.1`; the command
|
||||
then updates an identical channel row or uses the first empty slot.
|
||||
Each command explicitly replaces slot 1 in its own table. Inspect the slot first
|
||||
if it may now contain another rule. To preserve existing slot assignments, omit
|
||||
`.1`; the command then reuses an identical row or uses the first empty slot.
|
||||
|
||||
## Remove the custom rules
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ set flood.retry.ignore none
|
||||
| `flood.channel.block` | Blocks selected flood `GRP_TXT`/`GRP_DATA` channels when the key validates the packet. New repeater block lists start with editable/deletable `#wardriving h=4`. Add `h=<all|1-7|default>` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] <key|#channel> [name] [h=...]`, `del flood.channel.block[.n]` | `set flood.channel.block #wardriving h=4` |
|
||||
| `flood.channel.block.hops` | Limits keyed channel-block matches to short flood paths. `all` blocks matching packets at any hop count; `1`-`7` repeats packets at that hop count or lower and blocks longer matches. This does not restrict unkeyed `GRP_DATA`; use `flood.channel.data.hops` for that. | `get flood.channel.block.hops`, `set flood.channel.block.hops <all|1-7>` | `set flood.channel.block.hops 3` |
|
||||
| `flood.channel.scope` | Assigns a transport-region scope to received unscoped floods. Exact channel keys beat `txt:*`; `login:*` covers the remote-login family, and `other:*` covers every remaining flood type except TRACE, including OTA. TRACE remains unchanged across scope boundaries. ACL permission `4` can manage the table. | `get flood.channel.scope[.n]`, `set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region>`, `del flood.channel.scope.<n>|all` | `set flood.channel.scope login:* west` |
|
||||
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and optional received hop count/range (omitted means `all`). The table starts empty; only rows marked `suspend=tempradio` are skipped during temporary-radio operation. Login-capable `anon_req`/`path`/`response` types become filterable at hop `7`; flood `txt_msg` becomes filterable at hop `5`. Standard direct traceroute, other direct routing, and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.<n>|all` | `set flood.filter grp_data 4+` |
|
||||
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and optional received hop count/range (omitted means `all`). New tables seed slot 1 with `ota all suspend=tempradio`; only rows marked `suspend=tempradio` are skipped during temporary-radio operation. Login-capable `anon_req`/`path`/`response` types become filterable at hop `7`; flood `txt_msg` becomes filterable at hop `5`. Standard direct traceroute, other direct routing, and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.<n>|all` | `set flood.filter.1 0x0C all suspend=tempradio` |
|
||||
| `flood.moderation` | Decrypts keyed `GRP_TXT` channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports `public`, `#channel`, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. | `get flood.moderation[.n]`, `set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...]`, `del flood.moderation.<n>|all` | `set flood.moderation public "Noisy User" rate=5/min hops=4` |
|
||||
| `clock.sync.mesh` | After 30 minutes of uptime, estimates UTC from a configurable consensus of fresh signed-advert or valid Public-channel sources. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `get clock.sync.status` | `set clock.sync.mesh on` |
|
||||
| `clock.sync.internet` | Adds a read-only internet/NTP estimate at the same delayed check on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. | `get clock.sync.internet`, `set clock.sync.internet <on|off>` | `set clock.sync.internet on` |
|
||||
@@ -183,7 +183,9 @@ Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to
|
||||
remote client context, so they are not useful from the local serial CLI.
|
||||
|
||||
Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6`
|
||||
hex characters, and all hops in one path must use the same width.
|
||||
hex characters, and all hops in one path must use the same width. Hex input is
|
||||
case-insensitive. Replies use uppercase hex and retain the commas, so the value
|
||||
can be copied directly into another `set outpath` or `set altpath` command.
|
||||
|
||||
```text
|
||||
get outpath
|
||||
@@ -196,6 +198,16 @@ set altpath 71CE82,BA09F0
|
||||
set altpath clear
|
||||
```
|
||||
|
||||
For example, both `set altpath 600000,0d2784,f8dada` and
|
||||
`Set altpath 600000,0d2784,f8dada` store the same path and reply with:
|
||||
|
||||
```text
|
||||
> 600000,0D2784,F8DADA
|
||||
```
|
||||
|
||||
The first CLI word is case-insensitive (`set`, `Set`, and `SET` are the same,
|
||||
as are `get`, `Get`, and the other command verbs). Argument case is preserved.
|
||||
|
||||
`set outpath direct` sets a zero-hop direct route for a client reachable without
|
||||
repeaters. `set outpath clear` forgets the override and lets normal path
|
||||
discovery fill it again. `set outpath flood` forces replies to use flood packets
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include <helpers/MQTTDefaults.h>
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
#include <helpers/WiFiSetupPortal.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_wifi.h>
|
||||
#endif
|
||||
|
||||
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
|
||||
uint8_t shift = failure_count < 5 ? failure_count : 5;
|
||||
if (failure_count < 6) failure_count++;
|
||||
@@ -1180,6 +1186,10 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_mqtt_configured = false;
|
||||
_mqtt_started = false;
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
_webconfig = nullptr;
|
||||
_wc_mqtt_dirty = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::begin(bool has_display) {
|
||||
@@ -1297,6 +1307,16 @@ void MyMesh::begin(bool has_display) {
|
||||
_mqtt_bridge->setStatsSources(this, _radio, &board, _ms);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
void* web_mqtt_prefs = nullptr;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
web_mqtt_prefs = &_mqtt_prefs;
|
||||
#endif
|
||||
_webconfig = new WebConfigServer(this, web_mqtt_prefs, false,
|
||||
self_id.pub_key, FIRMWARE_VERSION,
|
||||
"companion", board.getManufacturerName());
|
||||
#endif
|
||||
}
|
||||
|
||||
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
@@ -1389,9 +1409,8 @@ static void copyMqttString(char* dest, size_t dest_size, const char* src) {
|
||||
dest[dest_size - 1] = 0;
|
||||
}
|
||||
|
||||
void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password,
|
||||
bool allow_setup_page) {
|
||||
if (!_mqtt_started && !_mqtt_setup.isActive()) {
|
||||
void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password) {
|
||||
if (!_mqtt_started) {
|
||||
if (strcmp(_mqtt_prefs.wifi_ssid, wifi_ssid ? wifi_ssid : "") != 0) {
|
||||
copyMqttString(_mqtt_prefs.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid), wifi_ssid);
|
||||
}
|
||||
@@ -1400,25 +1419,11 @@ void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password,
|
||||
}
|
||||
}
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED || !allow_setup_page) {
|
||||
_mqtt_setup.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mqtt_setup.loop()) {
|
||||
if (_mqtt_started && _mqtt_bridge) {
|
||||
_mqtt_bridge->end();
|
||||
}
|
||||
_mqtt_started = false;
|
||||
_mqtt_configured = true;
|
||||
Serial.println("MQTT companion: applying settings saved from the web page");
|
||||
}
|
||||
|
||||
if (!_mqtt_setup.isActive()) {
|
||||
_mqtt_setup.begin(&_mqtt_prefs);
|
||||
}
|
||||
|
||||
if (_mqtt_configured && !_mqtt_started && _mqtt_bridge) {
|
||||
if (WiFi.status() == WL_CONNECTED && _mqtt_configured && !_mqtt_started && _mqtt_bridge
|
||||
#ifdef WITH_WEBCONFIG
|
||||
&& !isWebConfigSetupActive()
|
||||
#endif
|
||||
) {
|
||||
_mqtt_started = true; // begin is one-shot; avoid retrying partial allocations
|
||||
_mqtt_bridge->begin();
|
||||
if (_mqtt_bridge->isRunning()) {
|
||||
@@ -1428,9 +1433,429 @@ void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MyMesh::stopMQTTSetupPage() {
|
||||
_mqtt_setup.stop();
|
||||
#ifdef WITH_WEBCONFIG
|
||||
static bool wcParseBool(const char* value, bool& out) {
|
||||
if (strcmp(value, "on") == 0) { out = true; return true; }
|
||||
if (strcmp(value, "off") == 0) { out = false; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool wcParseLong(const char* value, long min_value, long max_value, long& out) {
|
||||
if (!value || !value[0]) return false;
|
||||
char* end = nullptr;
|
||||
long parsed = strtol(value, &end, 10);
|
||||
if (!end || *end != 0 || parsed < min_value || parsed > max_value) return false;
|
||||
out = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wcParseDouble(const char* value, double min_value, double max_value, double& out) {
|
||||
if (!value || !value[0]) return false;
|
||||
char* end = nullptr;
|
||||
double parsed = strtod(value, &end);
|
||||
if (!end || *end != 0 || !isfinite(parsed) || parsed < min_value || parsed > max_value) return false;
|
||||
out = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wcValidNtpHost(const char* value) {
|
||||
if (!value || !value[0] || strlen(value) > 63 || value[0] == '.') return false;
|
||||
size_t len = strlen(value);
|
||||
if (value[len - 1] == '.') return false;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = value[i];
|
||||
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '-') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wcValidHexKey(const char* value) {
|
||||
if (!value || strlen(value) != 64) return false;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (!isxdigit(static_cast<unsigned char>(value[i]))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wcCopyValue(char* dest, size_t dest_size, const char* value) {
|
||||
if (!dest || dest_size == 0 || !value || strlen(value) >= dest_size) return false;
|
||||
strncpy(dest, value, dest_size - 1);
|
||||
dest[dest_size - 1] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
|
||||
memset(&s, 0, sizeof(s));
|
||||
wcCopyValue(s.name, sizeof(s.name), _prefs.node_name);
|
||||
s.lat = sensors.node_lat;
|
||||
s.lon = sensors.node_lon;
|
||||
s.freq = _prefs.freq;
|
||||
s.bw = _prefs.bw;
|
||||
s.sf = _prefs.sf;
|
||||
s.cr = _prefs.cr;
|
||||
s.tx_power = _prefs.tx_power_dbm;
|
||||
s.airtime_factor = _prefs.airtime_factor;
|
||||
s.rx_delay = _prefs.rx_delay_base;
|
||||
s.rx_gain = _prefs.rx_boosted_gain;
|
||||
s.repeat = _prefs.client_repeat != 0;
|
||||
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
|
||||
| WebConfigServer::CAP_RX_DELAY | WebConfigServer::CAP_RX_GAIN
|
||||
| WebConfigServer::CAP_REPEAT;
|
||||
}
|
||||
|
||||
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
if (!_webconfig) {
|
||||
strcpy(reply, "Err: WebUI unavailable (not enough memory)");
|
||||
return false;
|
||||
}
|
||||
if (_webconfig->isRunning() || _webconfig->isStopping()) {
|
||||
if (force_ap && !_webconfig->isStopping()
|
||||
&& _webconfig->mode() == WebConfigServer::MODE_LAN) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_mqtt_started && _mqtt_bridge) {
|
||||
_mqtt_bridge->end();
|
||||
_mqtt_started = false;
|
||||
}
|
||||
#endif
|
||||
return _webconfig->startSetupMode(reply);
|
||||
}
|
||||
strcpy(reply, _webconfig->isStopping() ? "Err: WebUI still stopping"
|
||||
: "Err: WebUI already running");
|
||||
return false;
|
||||
}
|
||||
if (force_ap) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_mqtt_started && _mqtt_bridge) {
|
||||
_mqtt_bridge->end();
|
||||
_mqtt_started = false;
|
||||
}
|
||||
#endif
|
||||
return _webconfig->startSetupMode(reply);
|
||||
}
|
||||
return _webconfig->startAutoMode(reply);
|
||||
}
|
||||
|
||||
void MyMesh::stopWebConfig() {
|
||||
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
|
||||
}
|
||||
|
||||
void MyMesh::serviceWebConfig() {
|
||||
if (_webconfig) _webconfig->tick(millis());
|
||||
}
|
||||
|
||||
bool MyMesh::isWebConfigSetupActive() const {
|
||||
return _webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP
|
||||
&& !_webconfig->isStopping();
|
||||
}
|
||||
|
||||
void MyMesh::rebootNow() {
|
||||
board.reboot();
|
||||
}
|
||||
|
||||
void MyMesh::onConfigBatchStart() {
|
||||
_wc_mqtt_dirty = false;
|
||||
}
|
||||
|
||||
void MyMesh::onConfigBatchEnd() {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_wc_mqtt_dirty) {
|
||||
CompanionMqttSetupPortal::saveStoredConfig(_mqtt_prefs);
|
||||
if (_mqtt_started && _mqtt_bridge) _mqtt_bridge->end();
|
||||
_mqtt_started = false;
|
||||
MQTTPrefs verified;
|
||||
_mqtt_configured = CompanionMqttSetupPortal::loadStoredConfig(verified);
|
||||
if (_mqtt_configured) _mqtt_prefs = verified;
|
||||
}
|
||||
#endif
|
||||
_wc_mqtt_dirty = false;
|
||||
}
|
||||
|
||||
void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
reply[0] = 0;
|
||||
if (!cmd || strncmp(cmd, "set ", 4) != 0) {
|
||||
strcpy(reply, "Error: unsupported command");
|
||||
return;
|
||||
}
|
||||
char* key = cmd + 4;
|
||||
char* split = strchr(key, ' ');
|
||||
if (!split) {
|
||||
strcpy(reply, "Error: missing value");
|
||||
return;
|
||||
}
|
||||
*split = 0;
|
||||
const char* value = split + 1;
|
||||
|
||||
if (strcmp(key, "name") == 0) {
|
||||
if (!value[0] || !wcCopyValue(_prefs.node_name, sizeof(_prefs.node_name), value)) {
|
||||
strcpy(reply, "Error: name must be 1-31 characters");
|
||||
} else {
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "lat") == 0 || strcmp(key, "lon") == 0) {
|
||||
double parsed;
|
||||
const bool latitude = key[1] == 'a';
|
||||
if (!wcParseDouble(value, latitude ? -90.0 : -180.0,
|
||||
latitude ? 90.0 : 180.0, parsed)) {
|
||||
strcpy(reply, latitude ? "Error: latitude must be -90 to 90"
|
||||
: "Error: longitude must be -180 to 180");
|
||||
} else {
|
||||
if (latitude) sensors.node_lat = parsed; else sensors.node_lon = parsed;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "radio") == 0) {
|
||||
float freq, bw;
|
||||
int sf, cr;
|
||||
char extra;
|
||||
if (sscanf(value, "%f,%f,%d,%d%c", &freq, &bw, &sf, &cr, &extra) != 4
|
||||
|| !isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f
|
||||
|| bw < 7.0f || bw > 500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8) {
|
||||
strcpy(reply, "Error: radio must be freq,bw,sf,cr");
|
||||
} else {
|
||||
_prefs.freq = freq;
|
||||
_prefs.bw = bw;
|
||||
_prefs.sf = static_cast<uint8_t>(sf);
|
||||
_prefs.cr = static_cast<uint8_t>(cr);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK - reboot required");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "tx") == 0) {
|
||||
long parsed;
|
||||
if (!wcParseLong(value, -9, MAX_LORA_TX_POWER, parsed)) {
|
||||
snprintf(reply, 160, "Error: TX power must be -9 to %d", MAX_LORA_TX_POWER);
|
||||
} else {
|
||||
_prefs.tx_power_dbm = static_cast<int8_t>(parsed);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "af") == 0 || strcmp(key, "rxdelay") == 0) {
|
||||
double parsed;
|
||||
const bool is_af = strcmp(key, "af") == 0;
|
||||
if (!wcParseDouble(value, 0.0, is_af ? 9.0 : 20.0, parsed)) {
|
||||
strcpy(reply, is_af ? "Error: airtime factor must be 0-9"
|
||||
: "Error: RX delay must be 0-20");
|
||||
} else {
|
||||
if (is_af) _prefs.airtime_factor = parsed; else _prefs.rx_delay_base = parsed;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "radio.rxgain") == 0 || strcmp(key, "repeat") == 0) {
|
||||
bool enabled;
|
||||
if (!wcParseBool(value, enabled)) {
|
||||
strcpy(reply, "Error: must be on or off");
|
||||
} else {
|
||||
if (strcmp(key, "radio.rxgain") == 0) {
|
||||
_prefs.rx_boosted_gain = enabled;
|
||||
radio_driver.setRxBoostedGainMode(enabled);
|
||||
} else {
|
||||
_prefs.client_repeat = enabled;
|
||||
}
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool changed = false;
|
||||
if (strcmp(key, "mqtt.origin") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin), value);
|
||||
} else if (strcmp(key, "mqtt.iata") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata), value);
|
||||
if (changed) {
|
||||
for (char* p = _mqtt_prefs.mqtt_iata; *p; p++) *p = toupper(static_cast<unsigned char>(*p));
|
||||
}
|
||||
} else if (strcmp(key, "mqtt.status") == 0 || strcmp(key, "mqtt.packets") == 0
|
||||
|| strcmp(key, "mqtt.raw") == 0 || strcmp(key, "mqtt.rx") == 0
|
||||
|| strcmp(key, "snmp") == 0) {
|
||||
bool enabled;
|
||||
if (!wcParseBool(value, enabled)) {
|
||||
strcpy(reply, "Error: must be on or off");
|
||||
return;
|
||||
}
|
||||
if (strcmp(key, "mqtt.status") == 0) _mqtt_prefs.mqtt_status_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.packets") == 0) _mqtt_prefs.mqtt_packets_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.raw") == 0) _mqtt_prefs.mqtt_raw_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.rx") == 0) _mqtt_prefs.mqtt_rx_enabled = enabled;
|
||||
else _mqtt_prefs.snmp_enabled = enabled;
|
||||
changed = true;
|
||||
} else if (strcmp(key, "mqtt.tx") == 0) {
|
||||
if (strcmp(value, "off") == 0) _mqtt_prefs.mqtt_tx_enabled = 0;
|
||||
else if (strcmp(value, "on") == 0) _mqtt_prefs.mqtt_tx_enabled = 1;
|
||||
else if (strcmp(value, "advert") == 0) _mqtt_prefs.mqtt_tx_enabled = 2;
|
||||
else {
|
||||
strcpy(reply, "Error: MQTT TX must be off, on, or advert");
|
||||
return;
|
||||
}
|
||||
changed = true;
|
||||
} else if (strcmp(key, "mqtt.interval") == 0) {
|
||||
long minutes;
|
||||
if (!wcParseLong(value, 1, 60, minutes)) {
|
||||
strcpy(reply, "Error: interval must be 1-60 minutes");
|
||||
return;
|
||||
}
|
||||
_mqtt_prefs.mqtt_status_interval = static_cast<uint32_t>(minutes) * 60000UL;
|
||||
changed = true;
|
||||
} else if (strcmp(key, "mqtt.ntp") == 0) {
|
||||
if (strcmp(value, "none") == 0) {
|
||||
_mqtt_prefs.mqtt_ntp_server[0] = 0;
|
||||
changed = true;
|
||||
} else if (wcValidNtpHost(value)) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_ntp_server,
|
||||
sizeof(_mqtt_prefs.mqtt_ntp_server), value);
|
||||
}
|
||||
} else if (strcmp(key, "mqtt.owner") == 0) {
|
||||
if (!value[0]) {
|
||||
_mqtt_prefs.mqtt_owner_public_key[0] = 0;
|
||||
changed = true;
|
||||
} else if (wcValidHexKey(value)) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_owner_public_key,
|
||||
sizeof(_mqtt_prefs.mqtt_owner_public_key), value);
|
||||
}
|
||||
} else if (strcmp(key, "mqtt.email") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_email, sizeof(_mqtt_prefs.mqtt_email), value);
|
||||
} else if (strcmp(key, "timezone") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.timezone_string, sizeof(_mqtt_prefs.timezone_string), value);
|
||||
} else if (strcmp(key, "timezone.offset") == 0) {
|
||||
long offset;
|
||||
if (!wcParseLong(value, -12, 14, offset)) {
|
||||
strcpy(reply, "Error: timezone offset must be -12 to 14");
|
||||
return;
|
||||
}
|
||||
_mqtt_prefs.timezone_offset = static_cast<int8_t>(offset);
|
||||
changed = true;
|
||||
} else if (strcmp(key, "snmp.community") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.snmp_community,
|
||||
sizeof(_mqtt_prefs.snmp_community), value);
|
||||
} else if (strncmp(key, "mqtt", 4) == 0 && key[4] >= '1'
|
||||
&& key[4] <= ('0' + MAX_MQTT_SLOTS) && key[5] == '.') {
|
||||
int slot = key[4] - '1';
|
||||
const char* field = key + 6;
|
||||
if (strcmp(field, "preset") == 0) {
|
||||
const bool valid = findMQTTPreset(value) != nullptr
|
||||
|| strcmp(value, MQTT_PRESET_CUSTOM) == 0
|
||||
|| strcmp(value, MQTT_PRESET_NONE) == 0;
|
||||
if (!valid) {
|
||||
strcpy(reply, "Error: unknown MQTT preset");
|
||||
return;
|
||||
}
|
||||
if (findMQTTPreset(value)) {
|
||||
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
|
||||
if (i != slot && strcmp(_mqtt_prefs.mqtt_slot_preset[i], value) == 0) {
|
||||
snprintf(reply, 160, "Error: preset already assigned to slot %d", i + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_preset[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_preset[slot]), value);
|
||||
} else if (strcmp(field, "server") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_host[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_host[slot]), value);
|
||||
} else if (strcmp(field, "port") == 0) {
|
||||
long port;
|
||||
if (!wcParseLong(value, 1, 65535, port)) {
|
||||
strcpy(reply, "Error: port must be 1-65535");
|
||||
return;
|
||||
}
|
||||
_mqtt_prefs.mqtt_slot_port[slot] = static_cast<uint16_t>(port);
|
||||
changed = true;
|
||||
} else if (strcmp(field, "username") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_username[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_username[slot]), value);
|
||||
} else if (strcmp(field, "password") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_password[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_password[slot]), value);
|
||||
} else if (strcmp(field, "token") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_token[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_token[slot]), value);
|
||||
} else if (strcmp(field, "topic") == 0) {
|
||||
if (strcmp(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_CUSTOM) != 0) {
|
||||
strcpy(reply, "Error: topic only applies to custom slots");
|
||||
return;
|
||||
}
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_topic[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_topic[slot]), value);
|
||||
} else if (strcmp(field, "audience") == 0) {
|
||||
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_audience[slot],
|
||||
sizeof(_mqtt_prefs.mqtt_slot_audience[slot]), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
_wc_mqtt_dirty = true;
|
||||
strcpy(reply, "OK");
|
||||
} else if (reply[0] == 0) {
|
||||
strcpy(reply, "Error: invalid or unsupported value");
|
||||
}
|
||||
#else
|
||||
strcpy(reply, "Error: unsupported setting for this companion");
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
|
||||
char ip[20] = "";
|
||||
int wifi_rssi = 0;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
|
||||
}
|
||||
int pos = snprintf(buf, buf_size,
|
||||
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
|
||||
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
|
||||
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
|
||||
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
|
||||
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
|
||||
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
|
||||
"\"tx_queue\":%d,\"wifi_rssi\":%d,\"ip\":\"%s\",\"mqtt_queue\":%d,\"slots\":[",
|
||||
(unsigned long)(millis() / 1000), (unsigned)board.getBattMilliVolts(),
|
||||
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
|
||||
(unsigned long)ESP.getMaxAllocHeap(),
|
||||
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
|
||||
radio_driver.getLastSNR(),
|
||||
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
|
||||
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
|
||||
(unsigned long)radio_driver.getPacketsRecvErrors(),
|
||||
(unsigned long)getNumSentFlood(), (unsigned long)getNumSentDirect(),
|
||||
(unsigned long)getNumRecvFlood(), (unsigned long)getNumRecvDirect(),
|
||||
(int)_mgr->getOutboundCount(0xFFFFFFFF), wifi_rssi, ip,
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_mqtt_bridge ? _mqtt_bridge->getQueueSize() : 0);
|
||||
#else
|
||||
0);
|
||||
#endif
|
||||
if (pos < 0 || pos >= static_cast<int>(buf_size) - 3) return;
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool first = true;
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
MQTTBridge::SlotStatusSnapshot status;
|
||||
if (!MQTTBridge::getSlotStatusSnapshot(i, &status)) continue;
|
||||
int written = snprintf(buf + pos, buf_size - pos,
|
||||
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\"}",
|
||||
first ? "" : ",", i + 1, status.name, status.state);
|
||||
if (written < 0 || written >= static_cast<int>(buf_size - pos)) break;
|
||||
pos += written;
|
||||
first = false;
|
||||
}
|
||||
#endif
|
||||
snprintf(buf + pos, buf_size - pos, "]}");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
#include "DataStore.h"
|
||||
#include "NodePrefs.h"
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) && !defined(WEBCONFIG_DISABLED)
|
||||
#include <helpers/esp32/WebConfigServer.h>
|
||||
#endif
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
||||
#include <helpers/CompanionMqttSetupPortal.h>
|
||||
#include <helpers/MQTTPrefs.h>
|
||||
@@ -90,7 +94,11 @@ struct AdvertPath {
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
};
|
||||
|
||||
class MyMesh : public BaseChatMesh, public DataStoreHost {
|
||||
class MyMesh : public BaseChatMesh, public DataStoreHost
|
||||
#ifdef WITH_WEBCONFIG
|
||||
, public WebConfigServer::Callbacks
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
|
||||
|
||||
@@ -102,12 +110,24 @@ public:
|
||||
uint32_t getBLEPin();
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
||||
void serviceMQTT(const char* wifi_ssid, const char* wifi_password,
|
||||
bool allow_setup_page);
|
||||
void stopMQTTSetupPage();
|
||||
void serviceMQTT(const char* wifi_ssid, const char* wifi_password);
|
||||
bool isMQTTConfigured() const { return _mqtt_configured; }
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
bool startWebConfig(bool force_ap, char* reply);
|
||||
void stopWebConfig();
|
||||
void serviceWebConfig();
|
||||
bool isWebConfigSetupActive() const;
|
||||
|
||||
void getNodeSnapshot(WebConfigServer::NodeSnapshot& snapshot) override;
|
||||
void execCommand(char* cmd, char* reply) override;
|
||||
void rebootNow() override;
|
||||
void onConfigBatchStart() override;
|
||||
void onConfigBatchEnd() override;
|
||||
void buildStatsJson(char* buf, size_t buf_size) override;
|
||||
#endif
|
||||
|
||||
void loop();
|
||||
void handleCmdFrame(size_t len);
|
||||
bool advert();
|
||||
@@ -237,9 +257,12 @@ private:
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
||||
MQTTPrefs _mqtt_prefs;
|
||||
MQTTBridge* _mqtt_bridge;
|
||||
CompanionMqttSetupPortal _mqtt_setup;
|
||||
bool _mqtt_configured;
|
||||
bool _mqtt_started;
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
WebConfigServer* _webconfig;
|
||||
bool _wc_mqtt_dirty;
|
||||
#endif
|
||||
uint32_t pending_login;
|
||||
uint32_t pending_status;
|
||||
|
||||
@@ -128,7 +128,7 @@ void halt() {
|
||||
static char configured_wifi_ssid[32];
|
||||
static char configured_wifi_password[64];
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(DISPLAY_CLASS)
|
||||
#if defined(WITH_WEBCONFIG) && defined(DISPLAY_CLASS)
|
||||
static DisplayDriver* companion_setup_display = nullptr;
|
||||
static unsigned long companion_setup_display_refresh = 0;
|
||||
|
||||
@@ -142,9 +142,23 @@ void halt() {
|
||||
companion_setup_display->setTextSize(1);
|
||||
companion_setup_display->setColor(DisplayDriver::LIGHT);
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
char setup_ssid[33] = {0};
|
||||
char setup_ip[16] = {0};
|
||||
if (WebConfigServer::getSetupInfo(setup_ssid, sizeof(setup_ssid),
|
||||
setup_ip, sizeof(setup_ip))) {
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 0, "MQTT setup");
|
||||
companion_setup_display->width() / 2, 0, "WebUI setup");
|
||||
companion_setup_display->setCursor(0, 14);
|
||||
companion_setup_display->print("Join open WiFi:");
|
||||
companion_setup_display->drawTextEllipsized(
|
||||
0, 25, companion_setup_display->width(), setup_ssid);
|
||||
companion_setup_display->setCursor(0, 39);
|
||||
companion_setup_display->print("Open in browser:");
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 51, setup_ip);
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 0, "WebUI");
|
||||
companion_setup_display->setCursor(0, 14);
|
||||
companion_setup_display->print("Join WiFi:");
|
||||
companion_setup_display->drawTextEllipsized(
|
||||
@@ -154,18 +168,6 @@ void halt() {
|
||||
const String ip = WiFi.localIP().toString();
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 51, ip.c_str());
|
||||
} else if (wifiSetupPortal().isActive()) {
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 0, "WiFi setup");
|
||||
companion_setup_display->setCursor(0, 14);
|
||||
companion_setup_display->print("Join open WiFi:");
|
||||
companion_setup_display->drawTextEllipsized(
|
||||
0, 25, companion_setup_display->width(), COMPANION_WIFI_SETUP_AP);
|
||||
companion_setup_display->setCursor(0, 39);
|
||||
companion_setup_display->print("Open in browser:");
|
||||
const String ip = wifiSetupPortal().apIP().toString();
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 51, ip.c_str());
|
||||
} else {
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 8, "WiFi connecting");
|
||||
@@ -286,7 +288,7 @@ void setup() {
|
||||
DisplayDriver* disp = NULL;
|
||||
if (display.begin()) {
|
||||
disp = &display;
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
|
||||
companion_setup_display = disp;
|
||||
#endif
|
||||
disp->startFrame();
|
||||
@@ -415,12 +417,25 @@ void setup() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
wifi_disconnected_since = millis() ? millis() : 1;
|
||||
WiFi.begin(configured_wifi_ssid, configured_wifi_password);
|
||||
} else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
|
||||
}
|
||||
#ifndef WITH_WEBCONFIG
|
||||
else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
|
||||
WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal");
|
||||
}
|
||||
// Disable WiFi modem power-save: its periodic modem/light-sleep stalls the SX1262 SPI+DIO servicing,
|
||||
// which makes the LoRa radio go deaf (no TX/RX) while WiFi is associated. Required for LoRa+WiFi to
|
||||
// coexist on this ESP32 — the small extra idle current is well worth a working radio.
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
// WiFi companions expose the shared WebUI by default. With no stored
|
||||
// credentials it opens the captive setup AP; otherwise it waits for the
|
||||
// station connection and serves the same page on the LAN.
|
||||
if (WebConfigServer::loadEnabled(true)) {
|
||||
char web_reply[160];
|
||||
the_mesh.startWebConfig(!have_wifi, web_reply);
|
||||
WIFI_DEBUG_PRINTLN("%s", web_reply);
|
||||
}
|
||||
#endif
|
||||
// Disable WiFi modem power-save after starting WebConfig: that startup may
|
||||
// restore a saved ESP-IDF WiFi power policy. Companion radios must keep
|
||||
// modem sleep disabled because its pauses can stall SX1262 SPI/DIO service.
|
||||
WiFi.setSleep(false);
|
||||
serial_interface.begin(TCP_PORT);
|
||||
#ifdef ENABLE_OTA
|
||||
@@ -491,11 +506,15 @@ void loop() {
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
|
||||
if (the_mesh.isMQTTConfigured()) {
|
||||
ui_task.loop();
|
||||
} else {
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
|
||||
if (the_mesh.isWebConfigSetupActive()
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|| !the_mesh.isMQTTConfigured()
|
||||
#endif
|
||||
) {
|
||||
renderCompanionSetupDisplay();
|
||||
} else {
|
||||
ui_task.loop();
|
||||
}
|
||||
#else
|
||||
ui_task.loop();
|
||||
@@ -521,23 +540,51 @@ void loop() {
|
||||
}
|
||||
|
||||
#if defined(ESP32) && defined(WIFI_SSID)
|
||||
#ifdef WITH_WEBCONFIG
|
||||
the_mesh.serviceWebConfig();
|
||||
#endif
|
||||
#ifdef ENABLE_OTA
|
||||
ota_seeder_loop(); // accept/drop a motatool `serve --tcp` connection on the dedicated seeder port
|
||||
ota_console_loop(); // service the OTA text console (port 5002)
|
||||
#endif
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
wifi_disconnected_since = 0;
|
||||
if (wifi_setup_recovery_mode && !wifiSetupPortal().isActive()) {
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (wifi_setup_recovery_mode && the_mesh.isWebConfigSetupActive()) {
|
||||
the_mesh.stopWebConfig();
|
||||
wifi_setup_recovery_mode = false;
|
||||
}
|
||||
#endif
|
||||
if (wifi_setup_recovery_mode
|
||||
#ifdef WITH_WEBCONFIG
|
||||
&& !the_mesh.isWebConfigSetupActive()
|
||||
#else
|
||||
&& !wifiSetupPortal().isActive()
|
||||
#endif
|
||||
) {
|
||||
wifi_setup_recovery_mode = false;
|
||||
}
|
||||
} else if (configured_wifi_ssid[0]) {
|
||||
unsigned long now = millis();
|
||||
if (wifi_disconnected_since == 0) wifi_disconnected_since = now ? now : 1;
|
||||
if (!wifi_setup_recovery_mode && !wifiSetupPortal().isActive()
|
||||
&& now - wifi_disconnected_since >= WIFI_SETUP_FALLBACK_MS) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
the_mesh.stopMQTTSetupPage();
|
||||
if (!wifi_setup_recovery_mode
|
||||
#ifdef WITH_WEBCONFIG
|
||||
&& !the_mesh.isWebConfigSetupActive()
|
||||
#else
|
||||
&& !wifiSetupPortal().isActive()
|
||||
#endif
|
||||
&& now - wifi_disconnected_since >= WIFI_SETUP_FALLBACK_MS) {
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::loadEnabled(true)) {
|
||||
char web_reply[160];
|
||||
if (the_mesh.startWebConfig(true, web_reply)) {
|
||||
wifi_setup_recovery_mode = true;
|
||||
WIFI_DEBUG_PRINTLN("WiFi unavailable for two minutes; %s", web_reply);
|
||||
} else {
|
||||
wifi_disconnected_since = now;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) {
|
||||
wifiSetupPortal().configureRecovery(
|
||||
configured_wifi_ssid, configured_wifi_password, WIFI_SETUP_FALLBACK_MS);
|
||||
@@ -547,10 +594,17 @@ void loop() {
|
||||
// Avoid retrying portal creation on every pass through loop().
|
||||
wifi_disconnected_since = now;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// Safely attempt to reconnect every 10 seconds if flagged
|
||||
if (!wifiSetupPortal().isActive() && wifi_needs_reconnect
|
||||
if (
|
||||
#ifdef WITH_WEBCONFIG
|
||||
!the_mesh.isWebConfigSetupActive()
|
||||
#else
|
||||
!wifiSetupPortal().isActive()
|
||||
#endif
|
||||
&& wifi_needs_reconnect
|
||||
&& (millis() - last_wifi_reconnect_attempt > 10000)) {
|
||||
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
|
||||
WiFi.disconnect();
|
||||
@@ -558,8 +612,7 @@ void loop() {
|
||||
last_wifi_reconnect_attempt = millis();
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
the_mesh.serviceMQTT(configured_wifi_ssid, configured_wifi_password,
|
||||
!wifiSetupPortal().isActive());
|
||||
the_mesh.serviceMQTT(configured_wifi_ssid, configured_wifi_password);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "MyMesh.h"
|
||||
#include <algorithm>
|
||||
#include <stdlib.h> // for qsort()
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/ClockSyncUtils.h>
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#ifdef WITH_WEBCONFIG
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
@@ -2644,6 +2648,23 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
_alerter.setBridge(mqtt_bridge);
|
||||
#endif
|
||||
|
||||
#if defined(WITH_WEBCONFIG) && !defined(WEBCONFIG_NO_AUTO_AP)
|
||||
bool start_webui = WebConfigServer::loadEnabled(false);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Preserve the MQTT observer's first-boot setup experience even though the
|
||||
// persistent WebUI master switch defaults off on infrastructure roles.
|
||||
start_webui = start_webui || _cli.getObserverPrefs()->wifi_ssid[0] == 0;
|
||||
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
|
||||
}
|
||||
#endif
|
||||
if (start_webui) {
|
||||
char wc_reply[160];
|
||||
startWebConfig(false, wc_reply);
|
||||
Serial.println(wc_reply);
|
||||
}
|
||||
#endif
|
||||
|
||||
saved_radio_apply_pending = !applySavedRadioParams();
|
||||
if (!saved_radio_apply_pending) {
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
@@ -3990,9 +4011,27 @@ static uint8_t floodPacketFilterProtectedHops(const mesh::Packet* packet) {
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::seedDefaultFloodPacketFilters() {
|
||||
auto& entry = flood_packet_filters[0];
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
entry.active = true;
|
||||
entry.payload_type = PAYLOAD_TYPE_OTA;
|
||||
entry.min_hops = 0;
|
||||
entry.max_hops = FLOOD_PACKET_FILTER_MAX_HOPS;
|
||||
entry.suspend_on_temp_radio = true;
|
||||
}
|
||||
|
||||
void MyMesh::loadFloodPacketFilters() {
|
||||
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_PACKET_FILTER_FILE)) return;
|
||||
if (_fs == NULL) {
|
||||
seedDefaultFloodPacketFilters();
|
||||
return;
|
||||
}
|
||||
if (!_fs->exists(FLOOD_PACKET_FILTER_FILE)) {
|
||||
seedDefaultFloodPacketFilters();
|
||||
saveFloodPacketFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
File file = openFloodChannelBlockRead(_fs, FLOOD_PACKET_FILTER_FILE);
|
||||
if (!file) return;
|
||||
@@ -5995,6 +6034,210 @@ void MyMesh::clearStats() {
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
|
||||
memset(&s, 0, sizeof(s));
|
||||
StrHelper::strncpy(s.name, _prefs.node_name, sizeof(s.name));
|
||||
StrHelper::strncpy(s.admin_password, _prefs.password, sizeof(s.admin_password));
|
||||
s.lat = _prefs.node_lat;
|
||||
s.lon = _prefs.node_lon;
|
||||
s.freq = _prefs.freq;
|
||||
s.bw = _prefs.bw;
|
||||
s.sf = _prefs.sf;
|
||||
s.cr = _prefs.cr;
|
||||
s.tx_power = _prefs.tx_power_dbm;
|
||||
s.airtime_factor = _prefs.airtime_factor;
|
||||
s.rx_delay = _prefs.rx_delay_base;
|
||||
s.tx_delay = _prefs.tx_delay_factor;
|
||||
s.cad = _prefs.cad_enabled;
|
||||
s.rx_gain = _prefs.rx_boosted_gain;
|
||||
s.repeat = !_prefs.disable_fwd;
|
||||
s.advert_interval = _prefs.advert_interval * 2;
|
||||
s.flood_advert_interval = _prefs.flood_advert_interval;
|
||||
s.flood_max = _prefs.flood_max;
|
||||
s.flood_max_advert = _prefs.flood_max_advert;
|
||||
s.flood_max_unscoped = _prefs.flood_max_unscoped;
|
||||
s.loop_detect = _prefs.loop_detect;
|
||||
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
|
||||
| WebConfigServer::CAP_DELAYS | WebConfigServer::CAP_CAD
|
||||
| WebConfigServer::CAP_RX_GAIN | WebConfigServer::CAP_REPEAT
|
||||
| WebConfigServer::CAP_ADVERT | WebConfigServer::CAP_FLOOD
|
||||
| WebConfigServer::CAP_LOOP | WebConfigServer::CAP_WIFI_POWER_SAVE;
|
||||
}
|
||||
|
||||
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
if (_cli.getBoard()->isOTAUpdateRunning()) {
|
||||
strcpy(reply, "Err: OTA server is running - 'stop ota' first");
|
||||
return true;
|
||||
}
|
||||
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
|
||||
strcpy(reply, _webconfig->isStopping() ? "Err: webconfig still stopping, retry shortly"
|
||||
: "Err: webconfig already running");
|
||||
return true;
|
||||
}
|
||||
if (!_webconfig) {
|
||||
void* mqtt_prefs = nullptr;
|
||||
bool owns_wifi = true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
mqtt_prefs = _cli.getObserverPrefs();
|
||||
owns_wifi = false;
|
||||
#endif
|
||||
_webconfig = new WebConfigServer(this, mqtt_prefs, owns_wifi,
|
||||
self_id.pub_key, getFirmwareVer(), getRole(),
|
||||
_cli.getBoard()->getManufacturerName());
|
||||
if (!_webconfig) {
|
||||
strcpy(reply, "Err: not enough memory for webconfig");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (force_ap) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) {
|
||||
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
_webconfig->startSetupMode(reply);
|
||||
} else {
|
||||
_webconfig->startAutoMode(reply);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::stopWebConfig(char* reply) {
|
||||
if (!_webconfig || !_webconfig->isRunning()) {
|
||||
strcpy(reply, "Err: webconfig not running");
|
||||
return true;
|
||||
}
|
||||
_webconfig->requestStop();
|
||||
strcpy(reply, "OK - webconfig stopping");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::setWebUIEnabled(bool enabled, char* reply) {
|
||||
if (!WebConfigServer::saveEnabled(enabled)) {
|
||||
strcpy(reply, "Error: failed to save webui setting");
|
||||
return true;
|
||||
}
|
||||
if (enabled) {
|
||||
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
|
||||
strcpy(reply, "OK - webui on (already active)");
|
||||
} else {
|
||||
startWebConfig(false, reply);
|
||||
if (strncmp(reply, "WebConfig", 9) == 0) {
|
||||
char tmp[160];
|
||||
StrHelper::strncpy(tmp, reply, sizeof(tmp));
|
||||
snprintf(reply, 160, "OK - webui on; %s", tmp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
|
||||
strcpy(reply, "OK - webui off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::getWebUIStatus(char* reply) const {
|
||||
const bool enabled = WebConfigServer::loadEnabled(false);
|
||||
if (!_webconfig || (!_webconfig->isRunning() && !_webconfig->isStopping())) {
|
||||
snprintf(reply, 160, "> %s, inactive", enabled ? "on" : "off");
|
||||
} else if (_webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
char ssid[33], ip[16];
|
||||
WebConfigServer::getSetupInfo(ssid, sizeof(ssid), ip, sizeof(ip));
|
||||
snprintf(reply, 160, "> %s, setup AP %s http://%s/", enabled ? "on" : "off", ssid, ip);
|
||||
} else if (_webconfig->mode() == WebConfigServer::MODE_CONNECTING) {
|
||||
snprintf(reply, 160, "> %s, connecting to WiFi", enabled ? "on" : "off");
|
||||
} else {
|
||||
snprintf(reply, 160, "> %s, http://%s/", enabled ? "on" : "off",
|
||||
WiFi.localIP().toString().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::onConfigBatchEnd() {
|
||||
_wc_batch_active = false;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_wc_restart_pending) {
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
restartBridge();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t mask = _wc_slot_restart_mask;
|
||||
_wc_slot_restart_mask = 0;
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (mask & (1U << i)) restartBridgeSlot(i);
|
||||
}
|
||||
#else
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
|
||||
char ip[20] = "";
|
||||
int wifi_rssi = 0;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
|
||||
}
|
||||
int pos = snprintf(buf, buf_size,
|
||||
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
|
||||
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
|
||||
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
|
||||
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
|
||||
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
|
||||
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
|
||||
"\"tx_queue\":%d,\"wifi_rssi\":%d,\"ip\":\"%s\",\"mqtt_queue\":%d,\"slots\":[",
|
||||
(unsigned long)(uptime_millis / 1000), (unsigned)board.getBattMilliVolts(),
|
||||
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
|
||||
(unsigned long)ESP.getMaxAllocHeap(),
|
||||
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
|
||||
radio_driver.getLastSNR(),
|
||||
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
|
||||
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
|
||||
(unsigned long)radio_driver.getPacketsRecvErrors(),
|
||||
(unsigned long)getNumSentFlood(), (unsigned long)getNumSentDirect(),
|
||||
(unsigned long)getNumRecvFlood(), (unsigned long)getNumRecvDirect(),
|
||||
(int)_mgr->getOutboundCount(0xFFFFFFFF), wifi_rssi, ip,
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
mqtt_bridge ? mqtt_bridge->getQueueSize() : 0);
|
||||
#else
|
||||
0);
|
||||
#endif
|
||||
if (pos < 0 || pos >= static_cast<int>(buf_size) - 3) return;
|
||||
|
||||
bool first = true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
MQTTBridge::SlotStatusSnapshot status;
|
||||
if (!MQTTBridge::getSlotStatusSnapshot(i, &status)) continue;
|
||||
int written;
|
||||
if (status.has_publish_counts) {
|
||||
written = snprintf(buf + pos, buf_size - pos,
|
||||
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\",\"ok\":%lu,\"err\":%lu}",
|
||||
first ? "" : ",", i + 1, status.name, status.state,
|
||||
status.publish_ok, status.publish_err);
|
||||
} else {
|
||||
written = snprintf(buf + pos, buf_size - pos,
|
||||
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\"}",
|
||||
first ? "" : ",", i + 1, status.name, status.state);
|
||||
}
|
||||
if (written < 0 || written >= static_cast<int>(buf_size - pos)) break;
|
||||
pos += written;
|
||||
first = false;
|
||||
}
|
||||
#else
|
||||
(void)first;
|
||||
#endif
|
||||
snprintf(buf + pos, buf_size - pos, "]}");
|
||||
}
|
||||
#endif
|
||||
|
||||
static char* trimSpaces(char* s) {
|
||||
while (*s == ' ') s++;
|
||||
char* end = s + strlen(s);
|
||||
@@ -6090,10 +6333,27 @@ static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, si
|
||||
|
||||
uint8_t hash_size = (path_len >> 6) + 1;
|
||||
uint8_t hop_count = path_len & 63;
|
||||
uint8_t byte_len = hop_count * hash_size;
|
||||
char hex[(MAX_PATH_SIZE * 2) + 1];
|
||||
mesh::Utils::toHex(hex, path, byte_len);
|
||||
snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex);
|
||||
size_t path_text_len = (size_t)hop_count * hash_size * 2 + (hop_count - 1);
|
||||
|
||||
// Every path that fits in the 160-byte CLI command buffer also fits this
|
||||
// shorter reply form. Retain a compact, complete fallback for a path learned
|
||||
// from an unusually long over-the-air route rather than truncating it.
|
||||
if (path_text_len + 3 > out_len) {
|
||||
out[0] = '>';
|
||||
out[1] = ' ';
|
||||
mesh::Utils::toHex(out + 2, path, (size_t)hop_count * hash_size);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pos = 0;
|
||||
out[pos++] = '>';
|
||||
out[pos++] = ' ';
|
||||
for (uint8_t hop = 0; hop < hop_count; hop++) {
|
||||
mesh::Utils::toHex(out + pos, path + ((size_t)hop * hash_size), hash_size);
|
||||
pos += hash_size * 2;
|
||||
if (hop + 1 < hop_count) out[pos++] = ',';
|
||||
}
|
||||
out[pos] = 0;
|
||||
}
|
||||
|
||||
static bool commandFamilyMatches(const char* command, const char* family) {
|
||||
@@ -6227,6 +6487,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
command += 3;
|
||||
}
|
||||
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
if (sender && !sender->isAdmin()) {
|
||||
bool allowed = (sender->isRegionMgr() && isRegionMgrAllowed(command))
|
||||
|| (sender->isFilterMgr() && isFilterMgrAllowed(command));
|
||||
@@ -6600,6 +6862,21 @@ void MyMesh::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::takeButtonToggleRequest()) {
|
||||
char wc_reply[160];
|
||||
setWebUIEnabled(!WebConfigServer::loadEnabled(false), wc_reply);
|
||||
Serial.println(wc_reply);
|
||||
}
|
||||
if (_webconfig) {
|
||||
_webconfig->tick(millis());
|
||||
if (!_webconfig->isRunning() && !_webconfig->isStopping()) {
|
||||
delete _webconfig;
|
||||
_webconfig = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// is pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs);
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
#include "helpers/esp32/WebConfigServer.h"
|
||||
#endif
|
||||
|
||||
#ifdef WITH_SNMP
|
||||
#include "helpers/SNMPAgent.h"
|
||||
#endif
|
||||
@@ -168,7 +172,11 @@ struct NeighbourInfo {
|
||||
#define RADIO_APPLY_RETRY_INTERVAL_MILLIS 1000UL
|
||||
#define SCHEDULED_RADIO_CLOCK_CHECKPOINT_SECS 60UL
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#ifdef WITH_WEBCONFIG
|
||||
, public WebConfigServer::Callbacks
|
||||
#endif
|
||||
{
|
||||
struct ScheduledRadioSetting {
|
||||
bool active;
|
||||
bool temporary;
|
||||
@@ -341,6 +349,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
AlertReporter _alerter;
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
WebConfigServer* _webconfig = nullptr;
|
||||
bool _wc_batch_active = false;
|
||||
bool _wc_restart_pending = false;
|
||||
uint8_t _wc_slot_restart_mask = 0;
|
||||
#endif
|
||||
|
||||
bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const;
|
||||
int8_t getDirectRetryMinSNRX4() const;
|
||||
@@ -406,6 +420,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
bool shouldBlockFloodChannelForward(const mesh::Packet* packet) const;
|
||||
void loadFloodPacketFilters();
|
||||
bool saveFloodPacketFilters();
|
||||
void seedDefaultFloodPacketFilters();
|
||||
bool shouldBlockFloodPacketForward(const mesh::Packet* packet) const;
|
||||
void formatFloodPacketFilters(const char* args, char* reply) const;
|
||||
void formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len) const;
|
||||
@@ -669,6 +684,12 @@ public:
|
||||
void restartBridge() override {
|
||||
AbstractBridge* active_bridge = activeBridge();
|
||||
if (!active_bridge || !active_bridge->isRunning()) return;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_wc_batch_active) {
|
||||
_wc_restart_pending = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
active_bridge->end();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Set device metadata before restarting bridge (same as in begin())
|
||||
@@ -687,6 +708,12 @@ public:
|
||||
void restartBridgeSlot(int slot) override {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_wc_batch_active && slot >= 0 && slot < MAX_MQTT_SLOTS) {
|
||||
_wc_slot_restart_mask |= static_cast<uint8_t>(1U << slot);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
mqtt_bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
|
||||
#else
|
||||
(void)slot;
|
||||
@@ -741,6 +768,26 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
bool startWebConfig(bool force_ap, char* reply) override;
|
||||
bool stopWebConfig(char* reply) override;
|
||||
bool setWebUIEnabled(bool enabled, char* reply) override;
|
||||
bool getWebUIStatus(char* reply) const override;
|
||||
bool isWebConfigActive() const override {
|
||||
return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping());
|
||||
}
|
||||
void getNodeSnapshot(WebConfigServer::NodeSnapshot& snapshot) override;
|
||||
void execCommand(char* cmd, char* reply) override { handleCommand(0, cmd, reply); }
|
||||
void rebootNow() override { _cli.getBoard()->reboot(); }
|
||||
void onConfigBatchStart() override {
|
||||
_wc_batch_active = true;
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
}
|
||||
void onConfigBatchEnd() override;
|
||||
void buildStatsJson(char* buf, size_t buf_size) override;
|
||||
#endif
|
||||
|
||||
// To check if there is pending work
|
||||
bool hasPendingWork() const;
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <WiFi.h>
|
||||
#include <helpers/esp32/WebConfigServer.h>
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
@@ -97,6 +98,38 @@ void UITask::renderCurrScreen() {
|
||||
_display->setCursor((_display->width() - poffWidth) / 2, 48);
|
||||
_display->drawTextCentered(_display->width()/2, 48, poweroff_string);
|
||||
} else {
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::isRebootPending()) {
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->setCursor(0, 14);
|
||||
_display->print("Config saved!");
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 30);
|
||||
_display->print("Rebooting...");
|
||||
return;
|
||||
}
|
||||
char wc_ssid[33], wc_ip[16];
|
||||
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->setCursor(0, 0);
|
||||
_display->print("WebUI WiFi Setup");
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 14);
|
||||
_display->print("Join WiFi:");
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
_display->setCursor(6, 24);
|
||||
_display->print(wc_ssid);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 40);
|
||||
_display->print("Then browse to:");
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
_display->setCursor(6, 50);
|
||||
_display->print(wc_ip);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
_display->setCursor(0, 0);
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
@@ -140,6 +173,21 @@ void UITask::loop() {
|
||||
_display->turnOn();
|
||||
Serial.println("Powering Off");
|
||||
_powering_off_at = millis() + POWEROFF_DELAY;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
// Single click wakes the display and long press powers off. Triple-click
|
||||
// was unused in the repeater UI, so use it for the persistent WebUI toggle.
|
||||
WebConfigServer::requestToggleFromButton();
|
||||
_display->turnOn();
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::getSetupInfo(nullptr, 0, nullptr, 0)) {
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <Mesh.h>
|
||||
|
||||
#include "MyMesh.h"
|
||||
#if defined(ESP32) && MAX_RECENT_REPEATERS > 0
|
||||
#include <new>
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
@@ -15,8 +18,20 @@
|
||||
|
||||
StdRNG fast_rng;
|
||||
#if MAX_RECENT_REPEATERS > 0
|
||||
#if defined(ESP32)
|
||||
// Classic ESP32 has a much smaller link-time DRAM window than its runtime heap
|
||||
// map. Allocate this large, fixed-capacity history at startup so enabling WiFi
|
||||
// features (including WebConfig) does not overflow that static DRAM window.
|
||||
// Global constructors run before setup(), while the heap is still
|
||||
// unfragmented; a failed allocation safely falls back to no history table.
|
||||
SimpleMeshTables::RecentRepeaterInfo* recent_repeater_storage =
|
||||
new (std::nothrow) SimpleMeshTables::RecentRepeaterInfo[MAX_RECENT_REPEATERS];
|
||||
SimpleMeshTables tables(recent_repeater_storage,
|
||||
recent_repeater_storage ? MAX_RECENT_REPEATERS : 0);
|
||||
#else
|
||||
SimpleMeshTables::RecentRepeaterInfo recent_repeater_storage[MAX_RECENT_REPEATERS];
|
||||
SimpleMeshTables tables(recent_repeater_storage, MAX_RECENT_REPEATERS);
|
||||
#endif
|
||||
#else
|
||||
SimpleMeshTables tables;
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#ifdef WITH_WEBCONFIG
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
|
||||
uint8_t shift = failure_count < 5 ? failure_count : 5;
|
||||
@@ -883,6 +887,21 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
_alerter.begin(&_prefs, _cli.getObserverPrefs(), this, this);
|
||||
_alerter.setBridge(bridge);
|
||||
#endif
|
||||
|
||||
#if defined(WITH_WEBCONFIG) && !defined(WEBCONFIG_NO_AUTO_AP)
|
||||
bool start_webui = WebConfigServer::loadEnabled(false);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
start_webui = start_webui || _cli.getObserverPrefs()->wifi_ssid[0] == 0;
|
||||
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
|
||||
if (bridge && bridge->isRunning()) bridge->end();
|
||||
}
|
||||
#endif
|
||||
if (start_webui) {
|
||||
char wc_reply[160];
|
||||
startWebConfig(false, wc_reply);
|
||||
Serial.println(wc_reply);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::applySavedRadioParams() {
|
||||
@@ -1059,6 +1078,209 @@ void MyMesh::clearStats() {
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
|
||||
memset(&s, 0, sizeof(s));
|
||||
StrHelper::strncpy(s.name, _prefs.node_name, sizeof(s.name));
|
||||
StrHelper::strncpy(s.admin_password, _prefs.password, sizeof(s.admin_password));
|
||||
s.lat = _prefs.node_lat;
|
||||
s.lon = _prefs.node_lon;
|
||||
s.freq = _prefs.freq;
|
||||
s.bw = _prefs.bw;
|
||||
s.sf = _prefs.sf;
|
||||
s.cr = _prefs.cr;
|
||||
s.tx_power = _prefs.tx_power_dbm;
|
||||
s.airtime_factor = _prefs.airtime_factor;
|
||||
s.rx_delay = _prefs.rx_delay_base;
|
||||
s.tx_delay = _prefs.tx_delay_factor;
|
||||
s.cad = _prefs.cad_enabled;
|
||||
s.rx_gain = _prefs.rx_boosted_gain;
|
||||
s.repeat = !_prefs.disable_fwd;
|
||||
s.advert_interval = _prefs.advert_interval * 2;
|
||||
s.flood_advert_interval = _prefs.flood_advert_interval;
|
||||
s.flood_max = _prefs.flood_max;
|
||||
s.flood_max_advert = _prefs.flood_max_advert;
|
||||
s.flood_max_unscoped = _prefs.flood_max_unscoped;
|
||||
s.loop_detect = _prefs.loop_detect;
|
||||
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
|
||||
| WebConfigServer::CAP_DELAYS | WebConfigServer::CAP_CAD
|
||||
| WebConfigServer::CAP_RX_GAIN | WebConfigServer::CAP_REPEAT
|
||||
| WebConfigServer::CAP_ADVERT | WebConfigServer::CAP_FLOOD
|
||||
| WebConfigServer::CAP_LOOP | WebConfigServer::CAP_WIFI_POWER_SAVE;
|
||||
}
|
||||
|
||||
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
if (_cli.getBoard()->isOTAUpdateRunning()) {
|
||||
strcpy(reply, "Err: OTA server is running - 'stop ota' first");
|
||||
return true;
|
||||
}
|
||||
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
|
||||
strcpy(reply, _webconfig->isStopping() ? "Err: webconfig still stopping, retry shortly"
|
||||
: "Err: webconfig already running");
|
||||
return true;
|
||||
}
|
||||
if (!_webconfig) {
|
||||
void* mqtt_prefs = nullptr;
|
||||
bool owns_wifi = true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
mqtt_prefs = _cli.getObserverPrefs();
|
||||
owns_wifi = false;
|
||||
#endif
|
||||
_webconfig = new WebConfigServer(this, mqtt_prefs, owns_wifi,
|
||||
self_id.pub_key, getFirmwareVer(), getRole(),
|
||||
_cli.getBoard()->getManufacturerName());
|
||||
if (!_webconfig) {
|
||||
strcpy(reply, "Err: not enough memory for webconfig");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (force_ap) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (bridge && bridge->isRunning()) {
|
||||
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
_webconfig->startSetupMode(reply);
|
||||
} else {
|
||||
_webconfig->startAutoMode(reply);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::setWebUIEnabled(bool enabled, char* reply) {
|
||||
if (!WebConfigServer::saveEnabled(enabled)) {
|
||||
strcpy(reply, "Error: failed to save webui setting");
|
||||
return true;
|
||||
}
|
||||
if (enabled) {
|
||||
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
|
||||
strcpy(reply, "OK - webui on (already active)");
|
||||
} else {
|
||||
startWebConfig(false, reply);
|
||||
if (strncmp(reply, "WebConfig", 9) == 0) {
|
||||
char tmp[160];
|
||||
StrHelper::strncpy(tmp, reply, sizeof(tmp));
|
||||
snprintf(reply, 160, "OK - webui on; %s", tmp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
|
||||
strcpy(reply, "OK - webui off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::getWebUIStatus(char* reply) const {
|
||||
const bool enabled = WebConfigServer::loadEnabled(false);
|
||||
if (!_webconfig || (!_webconfig->isRunning() && !_webconfig->isStopping())) {
|
||||
snprintf(reply, 160, "> %s, inactive", enabled ? "on" : "off");
|
||||
} else if (_webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
char ssid[33], ip[16];
|
||||
WebConfigServer::getSetupInfo(ssid, sizeof(ssid), ip, sizeof(ip));
|
||||
snprintf(reply, 160, "> %s, setup AP %s http://%s/", enabled ? "on" : "off", ssid, ip);
|
||||
} else if (_webconfig->mode() == WebConfigServer::MODE_CONNECTING) {
|
||||
snprintf(reply, 160, "> %s, connecting to WiFi", enabled ? "on" : "off");
|
||||
} else {
|
||||
snprintf(reply, 160, "> %s, http://%s/", enabled ? "on" : "off",
|
||||
WiFi.localIP().toString().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::stopWebConfig(char* reply) {
|
||||
if (!_webconfig || !_webconfig->isRunning()) {
|
||||
strcpy(reply, "Err: webconfig not running");
|
||||
return true;
|
||||
}
|
||||
_webconfig->requestStop();
|
||||
strcpy(reply, "OK - webconfig stopping");
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::onConfigBatchEnd() {
|
||||
_wc_batch_active = false;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_wc_restart_pending) {
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
restartBridge();
|
||||
return;
|
||||
}
|
||||
const uint8_t mask = _wc_slot_restart_mask;
|
||||
_wc_slot_restart_mask = 0;
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (mask & (1U << i)) restartBridgeSlot(i);
|
||||
}
|
||||
#else
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
|
||||
char ip[20] = "";
|
||||
int wifi_rssi = 0;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
|
||||
}
|
||||
int pos = snprintf(buf, buf_size,
|
||||
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
|
||||
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
|
||||
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
|
||||
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
|
||||
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
|
||||
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
|
||||
"\"tx_queue\":%d,\"wifi_rssi\":%d,\"ip\":\"%s\",\"mqtt_queue\":%d,\"slots\":[",
|
||||
(unsigned long)(uptime_millis / 1000), (unsigned)board.getBattMilliVolts(),
|
||||
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
|
||||
(unsigned long)ESP.getMaxAllocHeap(),
|
||||
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
|
||||
radio_driver.getLastSNR(),
|
||||
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
|
||||
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
|
||||
(unsigned long)radio_driver.getPacketsRecvErrors(),
|
||||
(unsigned long)getNumSentFlood(), (unsigned long)getNumSentDirect(),
|
||||
(unsigned long)getNumRecvFlood(), (unsigned long)getNumRecvDirect(),
|
||||
(int)_mgr->getOutboundCount(0xFFFFFFFF), wifi_rssi, ip,
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bridge ? bridge->getQueueSize() : 0);
|
||||
#else
|
||||
0);
|
||||
#endif
|
||||
if (pos < 0 || pos >= static_cast<int>(buf_size) - 3) return;
|
||||
|
||||
bool first = true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
MQTTBridge::SlotStatusSnapshot status;
|
||||
if (!MQTTBridge::getSlotStatusSnapshot(i, &status)) continue;
|
||||
int written;
|
||||
if (status.has_publish_counts) {
|
||||
written = snprintf(buf + pos, buf_size - pos,
|
||||
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\",\"ok\":%lu,\"err\":%lu}",
|
||||
first ? "" : ",", i + 1, status.name, status.state,
|
||||
status.publish_ok, status.publish_err);
|
||||
} else {
|
||||
written = snprintf(buf + pos, buf_size - pos,
|
||||
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\"}",
|
||||
first ? "" : ",", i + 1, status.name, status.state);
|
||||
}
|
||||
if (written < 0 || written >= static_cast<int>(buf_size - pos)) break;
|
||||
pos += written;
|
||||
first = false;
|
||||
}
|
||||
#else
|
||||
(void)first;
|
||||
#endif
|
||||
snprintf(buf + pos, buf_size - pos, "]}");
|
||||
}
|
||||
#endif
|
||||
|
||||
void MyMesh::formatStatsReply(char *reply) {
|
||||
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
|
||||
}
|
||||
@@ -1125,6 +1347,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
command += 3;
|
||||
}
|
||||
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
// handle ACL related commands
|
||||
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
||||
char* hex = &command[8];
|
||||
@@ -1303,6 +1527,16 @@ void MyMesh::loop() {
|
||||
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
|
||||
}
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_webconfig) {
|
||||
_webconfig->tick(millis());
|
||||
if (!_webconfig->isRunning() && !_webconfig->isStopping()) {
|
||||
delete _webconfig;
|
||||
_webconfig = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// is pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs, MyMesh::saveFilter);
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
#include "helpers/esp32/WebConfigServer.h"
|
||||
#endif
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
@@ -94,7 +99,11 @@ struct PostInfo {
|
||||
char text[MAX_POST_TEXT_LEN+1];
|
||||
};
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#ifdef WITH_WEBCONFIG
|
||||
, public WebConfigServer::Callbacks
|
||||
#endif
|
||||
{
|
||||
FILESYSTEM* _fs;
|
||||
uint32_t last_millis;
|
||||
uint64_t uptime_millis;
|
||||
@@ -134,6 +143,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
AlertReporter _alerter;
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
WebConfigServer* _webconfig = nullptr;
|
||||
bool _wc_batch_active = false;
|
||||
bool _wc_restart_pending = false;
|
||||
uint8_t _wc_slot_restart_mask = 0;
|
||||
#endif
|
||||
|
||||
void addPost(ClientInfo* client, const char* postData);
|
||||
bool applySavedRadioParams();
|
||||
@@ -317,6 +332,12 @@ public:
|
||||
|
||||
void restartBridge() override {
|
||||
if (!bridge || !bridge->isRunning()) return;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_wc_batch_active) {
|
||||
_wc_restart_pending = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
bridge->end();
|
||||
char device_id[65];
|
||||
mesh::LocalIdentity self_id = getSelfId();
|
||||
@@ -334,6 +355,12 @@ public:
|
||||
void restartBridgeSlot(int slot) override {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (!bridge || !bridge->isRunning()) return;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_wc_batch_active && slot >= 0 && slot < MAX_MQTT_SLOTS) {
|
||||
_wc_slot_restart_mask |= static_cast<uint8_t>(1U << slot);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
|
||||
#else
|
||||
(void)slot;
|
||||
@@ -359,6 +386,26 @@ public:
|
||||
return bridge->ntpDiag(reply, reply_size, verbose);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
bool startWebConfig(bool force_ap, char* reply) override;
|
||||
bool stopWebConfig(char* reply) override;
|
||||
bool setWebUIEnabled(bool enabled, char* reply) override;
|
||||
bool getWebUIStatus(char* reply) const override;
|
||||
bool isWebConfigActive() const override {
|
||||
return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping());
|
||||
}
|
||||
void getNodeSnapshot(WebConfigServer::NodeSnapshot& snapshot) override;
|
||||
void execCommand(char* cmd, char* reply) override { handleCommand(0, cmd, reply); }
|
||||
void rebootNow() override { _cli.getBoard()->reboot(); }
|
||||
void onConfigBatchStart() override {
|
||||
_wc_batch_active = true;
|
||||
_wc_restart_pending = false;
|
||||
_wc_slot_restart_mask = 0;
|
||||
}
|
||||
void onConfigBatchEnd() override;
|
||||
void buildStatsJson(char* buf, size_t buf_size) override;
|
||||
#endif
|
||||
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
||||
|
||||
// To check if there is pending work
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <WiFi.h>
|
||||
#include <helpers/esp32/WebConfigServer.h>
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
@@ -78,6 +79,38 @@ void UITask::renderCurrScreen() {
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::isRebootPending()) {
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->setCursor(0, 14);
|
||||
_display->print("Config saved!");
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 30);
|
||||
_display->print("Rebooting...");
|
||||
return;
|
||||
}
|
||||
char wc_ssid[33], wc_ip[16];
|
||||
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->setCursor(0, 0);
|
||||
_display->print("WebUI WiFi Setup");
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 14);
|
||||
_display->print("Join WiFi:");
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
_display->setCursor(6, 24);
|
||||
_display->print(wc_ssid);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, 40);
|
||||
_display->print("Then browse to:");
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
_display->setCursor(6, 50);
|
||||
_display->print(wc_ip);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// node name
|
||||
_display->setCursor(0, 0);
|
||||
_display->setTextSize(1);
|
||||
@@ -127,6 +160,13 @@ void UITask::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (WebConfigServer::getSetupInfo(nullptr, 0, nullptr, 0)) {
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_display->isOn()) {
|
||||
if (millis() >= _next_refresh) {
|
||||
_display->startFrame();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SensorMesh.h"
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
|
||||
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
|
||||
uint8_t shift = failure_count < 5 ? failure_count : 5;
|
||||
@@ -503,6 +504,8 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
||||
command += 3;
|
||||
}
|
||||
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
// first, see if this is a custom-handled CLI command (ie. in main.cpp)
|
||||
if (handleCustomCommand(sender_timestamp, command, reply)) {
|
||||
return; // command has been handled
|
||||
|
||||
+7
-1
@@ -66,7 +66,9 @@ monitor_filters = esp32_exception_decoder
|
||||
; (board_build.partitions) with two app slots + otadata (boards without one fetch-refuse cleanly).
|
||||
; pio_endf appends the EndF self-identity trailer. (nRF52 single-slot OTA is enabled per-board, not here —
|
||||
; it needs a custom bootloader; RP2040/STM32 have no A/B path yet.)
|
||||
extra_scripts = merge-bin.py
|
||||
extra_scripts =
|
||||
pre:scripts/generate_webconfig_html.py
|
||||
merge-bin.py
|
||||
post:tools/mota/pio_endf.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D ESP32_PLATFORM
|
||||
@@ -74,7 +76,11 @@ build_flags = ${arduino_base.build_flags}
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW)
|
||||
; -D ESP32_CPU_FREQ=80 ; change it to your need
|
||||
lib_deps = ${arduino_base.lib_deps}
|
||||
ESP32Async/ESPAsyncWebServer @ 3.10.3
|
||||
bblanchon/ArduinoJson
|
||||
build_src_filter = ${arduino_base.build_src_filter}
|
||||
+<helpers/esp32/WebConfigServer.cpp>
|
||||
+<helpers/ota/*.cpp>
|
||||
+<helpers/ota/detools/detools.c>
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when an ESP32 app image (including EndF) exceeds its app partition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ENTRY_SIZE = 32
|
||||
PARTITION_MAGIC = 0x50AA
|
||||
TYPE_APP = 0x00
|
||||
SUBTYPE_OTA_0 = 0x10
|
||||
|
||||
|
||||
def app_partition_size(table: bytes) -> tuple[str, int]:
|
||||
first_app: tuple[str, int] | None = None
|
||||
for offset in range(0, len(table) - ENTRY_SIZE + 1, ENTRY_SIZE):
|
||||
entry = table[offset : offset + ENTRY_SIZE]
|
||||
magic, part_type, subtype, _address, size = struct.unpack_from("<HBBII", entry)
|
||||
if magic != PARTITION_MAGIC:
|
||||
continue
|
||||
if part_type != TYPE_APP:
|
||||
continue
|
||||
label = entry[12:28].split(b"\0", 1)[0].decode("ascii", "replace") or "app"
|
||||
if first_app is None:
|
||||
first_app = (label, size)
|
||||
if subtype == SUBTYPE_OTA_0:
|
||||
return label, size
|
||||
if first_app is not None:
|
||||
return first_app
|
||||
raise ValueError("partition table has no app partition")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print(f"usage: {Path(sys.argv[0]).name} FIRMWARE.BIN PARTITIONS.BIN", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
firmware_path = Path(sys.argv[1])
|
||||
partition_path = Path(sys.argv[2])
|
||||
try:
|
||||
image_size = firmware_path.stat().st_size
|
||||
label, limit = app_partition_size(partition_path.read_bytes())
|
||||
except (OSError, ValueError) as error:
|
||||
print(f"ESP32 app-size check failed: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if image_size > limit:
|
||||
print(
|
||||
f"ESP32 app image is {image_size} bytes, exceeding {label} "
|
||||
f"({limit} bytes) by {image_size - limit} bytes",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"ESP32 app image fits {label}: {image_size}/{limit} bytes "
|
||||
f"({limit - image_size} bytes free)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Pre-build script: gzip webui/index.html into a PROGMEM C header so the
|
||||
# webconfig portal can serve the page straight from flash with
|
||||
# Content-Encoding: gzip. The generated header is tracked so ESP32 targets
|
||||
# that replace extra_scripts still compile from a clean checkout; this script
|
||||
# refreshes it whenever the source page (or this script) is newer.
|
||||
#
|
||||
# Output: src/helpers/esp32/WebConfigHtml.h
|
||||
# WEBCONFIG_HTML_GZ[] - gzipped page (PROGMEM)
|
||||
# WEBCONFIG_HTML_GZ_LEN - byte length
|
||||
# WEBCONFIG_HTML_ETAG - quoted strong ETag (sha256 prefix of the gz body)
|
||||
|
||||
import gzip
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# PlatformIO/SCons injects Import(). Keep the generator directly runnable as
|
||||
# well so maintainers can refresh and verify the tracked header without first
|
||||
# starting a firmware build.
|
||||
try:
|
||||
Import("env") # noqa: F821
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
SOURCE = os.path.join("webui", "index.html")
|
||||
OUTPUT = os.path.join("src", "helpers", "esp32", "WebConfigHtml.h")
|
||||
# __file__ is not defined inside PIO/SCons-executed extra_scripts
|
||||
SCRIPT = os.path.join("scripts", "generate_webconfig_html.py")
|
||||
|
||||
|
||||
def status(msg):
|
||||
sys.stderr.write("WebConfig HTML: %s\n" % msg)
|
||||
|
||||
|
||||
def needs_rebuild():
|
||||
if not os.path.isfile(OUTPUT):
|
||||
return True
|
||||
out_mtime = os.path.getmtime(OUTPUT)
|
||||
if os.path.getmtime(SOURCE) > out_mtime:
|
||||
return True
|
||||
if os.path.isfile(SCRIPT) and os.path.getmtime(SCRIPT) > out_mtime:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isfile(SOURCE):
|
||||
status("ERROR: %s not found" % SOURCE)
|
||||
sys.exit(2)
|
||||
|
||||
if not needs_rebuild():
|
||||
return
|
||||
|
||||
with open(SOURCE, "rb") as f:
|
||||
raw = f.read()
|
||||
|
||||
# mtime=0 keeps the gzip output (and therefore the ETag) deterministic
|
||||
gz = gzip.compress(raw, compresslevel=9, mtime=0)
|
||||
etag = hashlib.sha256(gz).hexdigest()[:16]
|
||||
|
||||
lines = []
|
||||
lines.append("// Auto-generated by scripts/generate_webconfig_html.py from %s" % SOURCE.replace(os.sep, "/"))
|
||||
lines.append("// DO NOT EDIT - edit webui/index.html instead.")
|
||||
lines.append("#pragma once")
|
||||
lines.append("#include <stdint.h>")
|
||||
lines.append("#include <pgmspace.h>")
|
||||
lines.append("")
|
||||
lines.append("const uint32_t WEBCONFIG_HTML_GZ_LEN = %d;" % len(gz))
|
||||
lines.append('const char WEBCONFIG_HTML_ETAG[] = "\\"%s\\"";' % etag)
|
||||
lines.append("const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {")
|
||||
for i in range(0, len(gz), 16):
|
||||
chunk = gz[i:i + 16]
|
||||
lines.append(" " + "".join("0x%02x," % b for b in chunk))
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
|
||||
with open(OUTPUT, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
status("%s -> %s (%d bytes raw, %d bytes gzipped)" % (SOURCE, OUTPUT, len(raw), len(gz)))
|
||||
|
||||
|
||||
# This script is attached only to esp32_base. Generate unconditionally here:
|
||||
# CPPDEFINES is not fully resolved when PlatformIO loads pre-scripts, so trying
|
||||
# to detect the final role at this point can leave a stale embedded page.
|
||||
# Non-WebConfig ESP32 builds do not compile the generated data into firmware.
|
||||
main()
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
|
||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||
virtual bool stopOTAUpdate(char reply[]) { return false; } // not supported
|
||||
virtual bool isOTAUpdateRunning() const { return false; }
|
||||
// Pull-based OTA: fetch the firmware build for this variant from a baked-in manifest and flash it.
|
||||
// current_ver is the running firmware version string (used to skip if already up to date); when
|
||||
// dry_run is true the build is only reported, not flashed. Observer (ESP32+WiFi) builds only.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
namespace mesh {
|
||||
namespace cli {
|
||||
|
||||
// Mobile keyboards commonly capitalize the first word entered into a CLI
|
||||
// field. Command verbs are identifiers, so normalize only that first token and
|
||||
// leave every argument (names, passwords, keys, and other values) untouched.
|
||||
inline void normalizeCommandVerb(char* command) {
|
||||
if (command == nullptr) return;
|
||||
|
||||
while (*command == ' ' || *command == '\t') command++;
|
||||
while (*command != 0 && *command != ' ' && *command != '\t') {
|
||||
if (*command >= 'A' && *command <= 'Z') {
|
||||
*command = static_cast<char>(*command - 'A' + 'a');
|
||||
}
|
||||
command++;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace mesh
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "CommonCLI.h"
|
||||
#include "CLICommandUtils.h"
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "AdvertDataHelpers.h"
|
||||
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
|
||||
@@ -1751,6 +1752,8 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
}
|
||||
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
|
||||
// live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverCommand(sender_timestamp, command, reply)) return;
|
||||
@@ -1787,6 +1790,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else {
|
||||
strcpy(reply, "ERR: clock cannot go backwards");
|
||||
}
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
} else if (memcmp(command, "start webconfig", 15) == 0
|
||||
&& (command[15] == 0 || command[15] == ' ')) {
|
||||
const bool force_ap = command[15] == ' ' && strcmp(&command[16], "ap") == 0;
|
||||
if (command[15] == ' ' && !force_ap) {
|
||||
strcpy(reply, "ERR: usage start webconfig [ap]");
|
||||
} else if (!_callbacks->startWebConfig(force_ap, reply)) {
|
||||
strcpy(reply, "ERR: webconfig not supported on this build");
|
||||
}
|
||||
} else if (strcmp(command, "stop webconfig") == 0) {
|
||||
if (!_callbacks->stopWebConfig(reply)) {
|
||||
strcpy(reply, "ERR: webconfig not supported on this build");
|
||||
}
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
} else if (memcmp(command, "memory", 6) == 0) {
|
||||
sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d, IntFree: %d, IntMax: %d, PSRAM: %d/%d",
|
||||
@@ -1799,6 +1816,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
#endif
|
||||
} else if (memcmp(command, "start ota", 9) == 0 && (command[9] == 0 || command[9] == ' ')) {
|
||||
// Manual OTA: bring up the board's browser server for a hand-uploaded binary.
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
if (_callbacks->isWebConfigActive()) {
|
||||
strcpy(reply, "ERR: stop webconfig first");
|
||||
} else
|
||||
#endif
|
||||
if (!_board->startOTAUpdate(_prefs->node_name, reply)) {
|
||||
strcpy(reply, "Error");
|
||||
}
|
||||
@@ -1977,7 +1999,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
}
|
||||
} else if (memcmp(command, "gps sync", 8) == 0) {
|
||||
LocationProvider * l = _sensors->getLocationProvider();
|
||||
if (l != NULL) {
|
||||
if (!_prefs->gps_enabled) {
|
||||
strcpy(reply, "gps is off");
|
||||
} else if (l != NULL) {
|
||||
l->syncTime();
|
||||
strcpy(reply, "scheduled");
|
||||
} else {
|
||||
@@ -2157,6 +2181,22 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
|
||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
if (memcmp(config, "webui ", 6) == 0) {
|
||||
const char* value = &config[6];
|
||||
bool enabled;
|
||||
if (strcmp(value, "on") == 0) enabled = true;
|
||||
else if (strcmp(value, "off") == 0) enabled = false;
|
||||
else {
|
||||
strcpy(reply, "Error: usage set webui on|off");
|
||||
return;
|
||||
}
|
||||
if (!_callbacks->setWebUIEnabled(enabled, reply)) {
|
||||
strcpy(reply, "Error: webui not supported on this build");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
|
||||
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
||||
@@ -2967,6 +3007,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
|
||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
if (strcmp(config, "webui") == 0) {
|
||||
if (!_callbacks->getWebUIStatus(reply)) {
|
||||
strcpy(reply, "Error: webui not supported on this build");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
|
||||
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
|
||||
|
||||
@@ -466,6 +466,32 @@ public:
|
||||
return false;
|
||||
};
|
||||
|
||||
// Browser-based configuration portal. ESP32 infrastructure roles override
|
||||
// these; force_ap=true asks for the captive SoftAP even when WiFi is set.
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
virtual bool startWebConfig(bool force_ap, char* reply) {
|
||||
(void)force_ap;
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool stopWebConfig(char* reply) {
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool isWebConfigActive() const {
|
||||
return false;
|
||||
};
|
||||
virtual bool setWebUIEnabled(bool enabled, char* reply) {
|
||||
(void)enabled;
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool getWebUIStatus(char* reply) const {
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
#endif
|
||||
|
||||
virtual int getQueueSize() {
|
||||
return 0; // no op by default
|
||||
};
|
||||
|
||||
@@ -385,12 +385,14 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
|
||||
} else if (memcmp(subcmd, "server ", 7) == 0) {
|
||||
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[slot], &subcmd[7], sizeof(_mqtt_prefs.mqtt_slot_host[slot]));
|
||||
savePrefs();
|
||||
_callbacks->restartBridgeSlot(slot);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(subcmd, "port ", 5) == 0) {
|
||||
int port = atoi(&subcmd[5]);
|
||||
if (port > 0 && port <= 65535) {
|
||||
_mqtt_prefs.mqtt_slot_port[slot] = port;
|
||||
savePrefs();
|
||||
_callbacks->restartBridgeSlot(slot);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error: port must be between 1 and 65535");
|
||||
@@ -457,6 +459,10 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
|
||||
savePrefs();
|
||||
_callbacks->restartBridgeSlot(slot);
|
||||
strcpy(reply, "OK");
|
||||
} else if (strcmp(config, "mqtt.owner") == 0 || strcmp(config, "mqtt.owner ") == 0) {
|
||||
_mqtt_prefs.mqtt_owner_public_key[0] = '\0';
|
||||
savePrefs();
|
||||
strcpy(reply, "OK - owner cleared");
|
||||
} else if (memcmp(config, "mqtt.owner ", 11) == 0) {
|
||||
const char* owner_key = &config[11];
|
||||
int key_len = strlen(owner_key);
|
||||
|
||||
@@ -177,6 +177,7 @@ public:
|
||||
|
||||
bool startOTAUpdate(const char* id, char reply[]) override;
|
||||
bool stopOTAUpdate(char reply[]) override;
|
||||
bool isOTAUpdateRunning() const override { return ota_server != nullptr; }
|
||||
bool otaFromManifest(const char* current_ver, bool dry_run, char reply[]) override;
|
||||
// Heavy body (TLS + JSON / HTTPUpdate). Runs in a dedicated large-stack task
|
||||
// spawned by otaFromManifest() — public only so that task entry point can call
|
||||
|
||||
@@ -260,6 +260,36 @@ void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPref
|
||||
snprintf(buf + pos, bufsize - pos, ", q:%d", q);
|
||||
}
|
||||
|
||||
bool MQTTBridge::getSlotStatusSnapshot(int slot_index, SlotStatusSnapshot* out) {
|
||||
if (!out || slot_index < 0 || slot_index >= RUNTIME_MQTT_SLOTS) return false;
|
||||
if (!s_mqtt_bridge_instance || !s_mqtt_bridge_instance->_initialized) return false;
|
||||
|
||||
MQTTBridge* bridge = s_mqtt_bridge_instance;
|
||||
const MQTTSlot& slot = bridge->_slots[slot_index];
|
||||
if (!slot.enabled && slot.preset) {
|
||||
out->name = slot.preset->name;
|
||||
out->state = "inactive";
|
||||
} else if (!slot.enabled) {
|
||||
return false;
|
||||
} else {
|
||||
out->name = slot.preset ? slot.preset->name : MQTT_PRESET_CUSTOM;
|
||||
if (!bridge->isSlotReady(slot_index)) {
|
||||
out->state = "wait";
|
||||
} else if (slot.connected) {
|
||||
out->state = "ok";
|
||||
} else if (slot.circuit_breaker_tripped) {
|
||||
out->state = "fail";
|
||||
} else {
|
||||
out->state = "disc";
|
||||
}
|
||||
}
|
||||
|
||||
out->has_publish_counts = false;
|
||||
out->publish_ok = 0;
|
||||
out->publish_err = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t MQTTBridge::getLastWifiDisconnectReason() { return s_wifi_disconnect_reason; }
|
||||
unsigned long MQTTBridge::getLastWifiDisconnectTime() { return s_wifi_disconnect_time; }
|
||||
|
||||
@@ -2097,7 +2127,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
} else if (ps_pref == 2) {
|
||||
ps_mode = WIFI_PS_MAX_MODEM;
|
||||
} else {
|
||||
ps_mode = WIFI_PS_NONE; // default: no power save; eliminates DTIM wake latency on mains-powered bridges
|
||||
ps_mode = WIFI_PS_MIN_MODEM;
|
||||
}
|
||||
esp_wifi_set_ps(ps_mode);
|
||||
#ifdef MQTT_WIFI_TX_POWER
|
||||
|
||||
@@ -502,6 +502,17 @@ public:
|
||||
/** True after this bridge has successfully set the RTC from NTP this boot. */
|
||||
bool hasNtpTime() const { return _ntp_synced; }
|
||||
static void formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs);
|
||||
// Structured status used by the browser configuration page. Publish
|
||||
// counters are optional because not every supported PsychicMqttClient
|
||||
// version exposes them.
|
||||
struct SlotStatusSnapshot {
|
||||
const char* name;
|
||||
const char* state;
|
||||
bool has_publish_counts;
|
||||
unsigned long publish_ok;
|
||||
unsigned long publish_err;
|
||||
};
|
||||
static bool getSlotStatusSnapshot(int slot_index, SlotStatusSnapshot* out);
|
||||
/** True when WiFi is set and at least one MQTT slot can run (preset + custom host if needed). */
|
||||
static bool isConfigValid(const MQTTPrefs* obs);
|
||||
static void formatSlotDiagReply(char* buf, size_t bufsize, int slot_index);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
|
||||
// Shared browser configuration portal for ESP32 infrastructure and WiFi
|
||||
// companion builds. The UI removes MQTT controls when no bridge is present.
|
||||
//
|
||||
// Two modes:
|
||||
// - SETUP: open SoftAP + captive portal, raised automatically on first boot
|
||||
// when no WiFi is configured (wifi_ssid empty), or manually via
|
||||
// `start webconfig ap`. Save -> reboot; auto-stops after an idle timeout.
|
||||
// - LAN: bound to an existing or portal-owned STA connection. Infrastructure
|
||||
// roles require the node admin password; companions use their trusted LAN.
|
||||
//
|
||||
// Concurrency model: AsyncWebServer handlers run on the async_tcp task and
|
||||
// must never touch the CLI, prefs persistence, or the radio. Config writes
|
||||
// are marshaled into a single-slot command batch that tick() - called from
|
||||
// MyMesh::loop() on the Arduino loop task - drains through the existing CLI
|
||||
// `set` handlers. Prefs-struct reads and batch state are guarded by _mux.
|
||||
//
|
||||
// The master switch and standalone WiFi credentials use independent NVS
|
||||
// namespaces, so role-specific prefs-file layouts remain untouched.
|
||||
|
||||
#if defined(ESP_PLATFORM) && !defined(WEBCONFIG_DISABLED)
|
||||
|
||||
#define WITH_WEBCONFIG 1
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
class AsyncWebServer;
|
||||
class AsyncWebServerRequest;
|
||||
class DNSServer;
|
||||
|
||||
#ifndef WEBCONFIG_AP_IDLE_TIMEOUT_MS
|
||||
#define WEBCONFIG_AP_IDLE_TIMEOUT_MS (10UL * 60UL * 1000UL)
|
||||
#endif
|
||||
#ifndef WEBCONFIG_SESSION_TTL_MS
|
||||
#define WEBCONFIG_SESSION_TTL_MS (20UL * 60UL * 1000UL)
|
||||
#endif
|
||||
|
||||
class WebConfigServer {
|
||||
public:
|
||||
enum Mode : uint8_t { MODE_OFF = 0, MODE_SETUP, MODE_CONNECTING, MODE_LAN };
|
||||
|
||||
enum Capability : uint32_t {
|
||||
CAP_LOCATION = 1UL << 0,
|
||||
CAP_AIRTIME = 1UL << 1,
|
||||
CAP_RX_DELAY = 1UL << 2,
|
||||
CAP_CAD = 1UL << 3,
|
||||
CAP_RX_GAIN = 1UL << 4,
|
||||
CAP_REPEAT = 1UL << 5,
|
||||
CAP_ADVERT = 1UL << 6,
|
||||
CAP_FLOOD = 1UL << 7,
|
||||
CAP_LOOP = 1UL << 8,
|
||||
CAP_TX_DELAY = 1UL << 9,
|
||||
CAP_WIFI_POWER_SAVE = 1UL << 10,
|
||||
CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
|
||||
};
|
||||
|
||||
// Role-neutral snapshot. Infrastructure and companion builds have different
|
||||
// persisted NodePrefs layouts, so the portal never casts either one or
|
||||
// assumes their member offsets match.
|
||||
struct NodeSnapshot {
|
||||
char name[32];
|
||||
char admin_password[32]; // empty = trusted LAN/no login prompt
|
||||
double lat;
|
||||
double lon;
|
||||
float freq;
|
||||
float bw;
|
||||
float airtime_factor;
|
||||
float rx_delay;
|
||||
float tx_delay;
|
||||
int8_t tx_power;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t cad;
|
||||
uint8_t rx_gain;
|
||||
uint8_t repeat;
|
||||
uint16_t advert_interval;
|
||||
uint8_t flood_advert_interval;
|
||||
uint8_t flood_max;
|
||||
uint8_t flood_max_advert;
|
||||
uint8_t flood_max_unscoped;
|
||||
uint8_t loop_detect;
|
||||
uint32_t capabilities;
|
||||
};
|
||||
|
||||
class Callbacks {
|
||||
public:
|
||||
virtual void getNodeSnapshot(NodeSnapshot& snapshot) = 0;
|
||||
// Run one CLI command. Called from tick() only (loop task); reply is 160 bytes.
|
||||
virtual void execCommand(char* cmd, char* reply) = 0;
|
||||
virtual void rebootNow() = 0;
|
||||
// Bracket a config batch so bridge restarts triggered by individual
|
||||
// `set` handlers can be coalesced into one.
|
||||
virtual void onConfigBatchStart() {}
|
||||
virtual void onConfigBatchEnd() {}
|
||||
// Fill buf with the stats JSON snapshot. Called from tick() (loop task).
|
||||
virtual void buildStatsJson(char* buf, size_t buf_size) = 0;
|
||||
// Teardown finished (server + DNS freed, WiFi mode restored).
|
||||
virtual void onWebConfigStopped() {}
|
||||
};
|
||||
|
||||
// mqtt_prefs is an MQTTPrefs* on WITH_MQTT_BRIDGE builds and NULL otherwise.
|
||||
// owns_wifi is true when the portal itself brought up STA/AP (normal
|
||||
// repeater/room-server builds), false when the companion/MQTT runtime owns it.
|
||||
WebConfigServer(Callbacks* callbacks, void* mqtt_prefs, bool owns_wifi,
|
||||
const uint8_t* pub_key, const char* fw_ver,
|
||||
const char* role, const char* board_name);
|
||||
~WebConfigServer();
|
||||
|
||||
// For the device display: true while a setup-mode portal is active.
|
||||
// Fills the AP SSID and portal IP; either buffer may be NULL to just poll.
|
||||
// Call from the loop task only (same task that changes the mode).
|
||||
static bool getSetupInfo(char* ssid, size_t ssid_len, char* ip, size_t ip_len);
|
||||
|
||||
// For the device display: true once a config save completed and the node is
|
||||
// about to reboot — ground truth for the user even if the browser lost its
|
||||
// connection before the confirmation arrived. Loop task only.
|
||||
static bool isRebootPending();
|
||||
|
||||
// Persistent master switch and standalone WiFi credentials use separate NVS
|
||||
// namespaces. This avoids changing NodePrefs or MQTTPrefs file layouts.
|
||||
static bool loadEnabled(bool default_value = false);
|
||||
static bool saveEnabled(bool enabled);
|
||||
static bool loadStandaloneWiFi(char* ssid, size_t ssid_len,
|
||||
char* password, size_t password_len,
|
||||
uint8_t* power_save = NULL);
|
||||
static bool saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
uint8_t power_save);
|
||||
|
||||
// UI tasks use an otherwise-unused multi-click gesture without reaching into
|
||||
// MyMesh directly. The mesh loop consumes the request and performs the NVS /
|
||||
// WiFi transition on its own task.
|
||||
static void requestToggleFromButton();
|
||||
static bool takeButtonToggleRequest();
|
||||
|
||||
bool startSetupMode(char reply[]); // open SoftAP + DNS captive portal
|
||||
bool startLanMode(char reply[]); // bind to existing STA connection
|
||||
bool startAutoMode(char reply[]); // use saved WiFi, or setup AP when absent
|
||||
void requestStop(); // stop listening now, free after grace period
|
||||
void tick(uint32_t now); // call every loop iteration
|
||||
|
||||
Mode mode() const { return _mode; }
|
||||
bool isRunning() const { return _mode != MODE_OFF; }
|
||||
bool isStopping() const { return _stopping; }
|
||||
|
||||
private:
|
||||
static const int MAX_BATCH = 24;
|
||||
static const size_t MAX_BODY = 4096;
|
||||
enum BatchState : uint8_t { BATCH_IDLE = 0, BATCH_PENDING, BATCH_DONE };
|
||||
struct BatchEntry {
|
||||
char key[24]; // allowlisted `set` key (echoed back to the UI)
|
||||
char cmd[160]; // full CLI command (may contain secrets - never echoed)
|
||||
char reply[160];
|
||||
};
|
||||
|
||||
Callbacks* _cb;
|
||||
void* _mqtt_prefs;
|
||||
bool _owns_wifi;
|
||||
const uint8_t* _pub_key;
|
||||
const char* _fw_ver;
|
||||
const char* _role;
|
||||
const char* _board_name;
|
||||
|
||||
AsyncWebServer* _server = NULL;
|
||||
DNSServer* _dns = NULL;
|
||||
SemaphoreHandle_t _mux;
|
||||
Mode _mode = MODE_OFF;
|
||||
bool _stopping = false;
|
||||
bool _was_setup_ap = false;
|
||||
uint32_t _connect_deadline = 0;
|
||||
char _wifi_ssid[32] = {0};
|
||||
char _wifi_password[64] = {0};
|
||||
uint8_t _wifi_power_save = 1;
|
||||
char _ap_ssid[33] = {0};
|
||||
|
||||
// Most-recently-created instance, for the display's getSetupInfo() poll.
|
||||
static WebConfigServer* _active;
|
||||
|
||||
// Command batch: filled by async_tcp under _mux, drained by tick().
|
||||
volatile BatchState _batch_state = BATCH_IDLE;
|
||||
uint8_t _batch_count = 0;
|
||||
uint8_t _batch_next = 0; // drain progress (one command per tick)
|
||||
uint32_t _batch_last_cmd = 0;
|
||||
bool _batch_reboot = false;
|
||||
bool _batch_reboot_armed = false;
|
||||
bool _standalone_wifi_dirty = false;
|
||||
BatchEntry _batch[MAX_BATCH];
|
||||
|
||||
// LAN-mode session (single slot; new login evicts the old session)
|
||||
char _session_token[33] = {0};
|
||||
uint32_t _session_last_seen = 0;
|
||||
uint8_t _login_fails = 0;
|
||||
uint32_t _login_lock_until = 0;
|
||||
|
||||
// Once set (via /api/portal/exit), OS captive probes get native "success"
|
||||
// replies so the phone's sign-in sheet can be dismissed without dropping the
|
||||
// WiFi, letting the user continue in their real browser. async_tcp-task only.
|
||||
volatile bool _captive_release = false;
|
||||
|
||||
// Save-path diagnostics: 1 Hz serial trace for 60 s after each config POST
|
||||
// (AP station count, heap, batch state) to pinpoint client drops on hardware.
|
||||
volatile uint32_t _diag_until = 0;
|
||||
uint32_t _diag_last = 0;
|
||||
|
||||
volatile uint32_t _last_activity = 0;
|
||||
uint32_t _reboot_at = 0; // 0 = none scheduled
|
||||
uint32_t _delete_at = 0; // deferred teardown deadline
|
||||
volatile uint32_t _stats_wanted_until = 0;
|
||||
uint32_t _stats_built_at = 0;
|
||||
char _stats_json[1024] = {0};
|
||||
|
||||
void createServer();
|
||||
void registerRoutes();
|
||||
void drainBatch(uint32_t now);
|
||||
void finalizeTeardown();
|
||||
bool checkAuth(AsyncWebServerRequest* req);
|
||||
static void collectBody(AsyncWebServerRequest* req, uint8_t* data, size_t len,
|
||||
size_t index, size_t total);
|
||||
|
||||
void handleRoot(AsyncWebServerRequest* req);
|
||||
void handleStatus(AsyncWebServerRequest* req);
|
||||
void handleLogin(AsyncWebServerRequest* req);
|
||||
void handleLogout(AsyncWebServerRequest* req);
|
||||
void handleConfigGet(AsyncWebServerRequest* req);
|
||||
void handleConfigPost(AsyncWebServerRequest* req);
|
||||
void handleConfigResult(AsyncWebServerRequest* req);
|
||||
void handleStats(AsyncWebServerRequest* req);
|
||||
void handleScan(AsyncWebServerRequest* req);
|
||||
void handlePresets(AsyncWebServerRequest* req);
|
||||
void handleReboot(AsyncWebServerRequest* req);
|
||||
void handlePortalExit(AsyncWebServerRequest* req);
|
||||
void handleNotFound(AsyncWebServerRequest* req);
|
||||
};
|
||||
|
||||
#endif // ESP_PLATFORM && !WEBCONFIG_DISABLED
|
||||
@@ -94,6 +94,9 @@ void SerialEthernetInterface::enable() {
|
||||
|
||||
void SerialEthernetInterface::disable() {
|
||||
_isEnabled = false;
|
||||
deviceConnected = false;
|
||||
if (client) client.stop();
|
||||
clearBuffers();
|
||||
}
|
||||
|
||||
size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
@@ -153,9 +156,8 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
ETHERNET_DEBUG_PRINTLN("Closing previous client");
|
||||
client.stop();
|
||||
}
|
||||
_state = RECV_STATE_IDLE;
|
||||
_frame_len = 0;
|
||||
_rx_len = 0;
|
||||
// Queued replies and partial input belong to the previous TCP session.
|
||||
clearBuffers();
|
||||
client = newClient;
|
||||
ETHERNET_DEBUG_PRINTLN("Switched to new client");
|
||||
}
|
||||
@@ -172,9 +174,13 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
deviceConnected = true;
|
||||
}
|
||||
} else {
|
||||
if (deviceConnected) {
|
||||
bool wasConnected = deviceConnected;
|
||||
if (wasConnected || send_queue_len > 0 || _state != RECV_STATE_IDLE) {
|
||||
deviceConnected = false;
|
||||
ETHERNET_DEBUG_PRINTLN("Disconnected");
|
||||
clearBuffers();
|
||||
if (wasConnected) {
|
||||
ETHERNET_DEBUG_PRINTLN("Disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand.
|
||||
// 514 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// 545 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT
|
||||
// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.
|
||||
|
||||
@@ -59,6 +59,22 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x9e40157c, "Heltec_E290_repeater_bridge_espnow" },
|
||||
{ 0xb12e6e01, "Heltec_E290_room_server" },
|
||||
{ 0xf6f835ec, "Heltec_mesh_solar_repeater" },
|
||||
{ 0x7fab017f, "heltec_rc32_companion_radio_ble" },
|
||||
{ 0xf310a4d1, "heltec_rc32_companion_radio_usb" },
|
||||
{ 0xdd5953ec, "heltec_rc32_companion_radio_wifi" },
|
||||
{ 0x97574bc2, "heltec_rc32_kiss_modem" },
|
||||
{ 0x2d918a3c, "heltec_rc32_repeater" },
|
||||
{ 0xd1755faa, "heltec_rc32_repeater_bridge_espnow" },
|
||||
{ 0x8a6ceaf0, "heltec_rc32_room_server" },
|
||||
{ 0x57f69746, "heltec_rc32_sensor" },
|
||||
{ 0xbc05d6a7, "heltec_rc32_terminal_chat" },
|
||||
{ 0x5bad80f1, "heltec_rc32_without_display_companion_radio_ble" },
|
||||
{ 0x112d34c4, "heltec_rc32_without_display_companion_radio_usb" },
|
||||
{ 0xf10a26df, "heltec_rc32_without_display_companion_radio_wifi" },
|
||||
{ 0xc5409ced, "heltec_rc32_without_display_repeater" },
|
||||
{ 0xf9022545, "heltec_rc32_without_display_repeater_bridge_espnow" },
|
||||
{ 0x2023036d, "heltec_rc32_without_display_room_server" },
|
||||
{ 0x4abd24af, "heltec_rc32_without_display_sensor" },
|
||||
{ 0x35d42b35, "Heltec_t096_companion_radio_ble_femoff" },
|
||||
{ 0x9f736ca7, "Heltec_t096_companion_radio_ble_femon" },
|
||||
{ 0x3fbb1498, "Heltec_t096_companion_radio_usb_femoff" },
|
||||
@@ -91,8 +107,6 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x85a8c944, "Heltec_tower_v2_repeater" },
|
||||
{ 0xb16009a0, "heltec_tracker_v2_companion_radio_ble_femoff" },
|
||||
{ 0xdc780e80, "heltec_tracker_v2_companion_radio_ble_femon" },
|
||||
{ 0x9b1ab06d, "heltec_tracker_v2_companion_radio_ble_ps_femoff" },
|
||||
{ 0x9cedf289, "heltec_tracker_v2_companion_radio_ble_ps_femon" },
|
||||
{ 0x4aa63180, "heltec_tracker_v2_companion_radio_usb_femoff" },
|
||||
{ 0x6fddcbf1, "heltec_tracker_v2_companion_radio_usb_femon" },
|
||||
{ 0xf4fb5484, "heltec_tracker_v2_companion_radio_wifi_femoff" },
|
||||
@@ -126,24 +140,38 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x21519537, "Heltec_v3_sensor" },
|
||||
{ 0x382bb181, "Heltec_v3_terminal_chat" },
|
||||
{ 0x5a2980e6, "heltec_v4_3_companion_radio_ble_femoff" },
|
||||
{ 0x90581c83, "heltec_v4_3_companion_radio_ble_ps_femoff" },
|
||||
{ 0x356df17c, "heltec_v4_3_companion_radio_usb_femoff" },
|
||||
{ 0x99a084fc, "heltec_v4_3_companion_radio_wifi_femoff" },
|
||||
{ 0x64c9dfc9, "heltec_v4_3_companion_radio_wifi_mqtt_femoff" },
|
||||
{ 0xe65a7dfd, "heltec_v4_3_expansionkit_tft_companion_radio_ble_ps_femoff" },
|
||||
{ 0x2a2ffc92, "heltec_v4_3_expansionkit_tft_companion_radio_ble_femoff" },
|
||||
{ 0x2c13b8e9, "heltec_v4_3_tft_companion_radio_ble_femoff" },
|
||||
{ 0xb016979a, "heltec_v4_3_tft_companion_radio_usb_femoff" },
|
||||
{ 0xb49b6b55, "heltec_v4_3_tft_companion_radio_wifi_femoff" },
|
||||
{ 0x86fb85f0, "heltec_v4_companion_radio_ble_femon" },
|
||||
{ 0xd33b0986, "heltec_v4_companion_radio_ble_ps_femon" },
|
||||
{ 0xa8f5940f, "heltec_v4_companion_radio_usb_femon" },
|
||||
{ 0x86ac3b1b, "heltec_v4_companion_radio_wifi_femon" },
|
||||
{ 0x7af848da, "heltec_v4_companion_radio_wifi_mqtt_femon" },
|
||||
{ 0xd522a14f, "heltec_v4_expansionkit_repeater" },
|
||||
{ 0x76657d76, "heltec_v4_expansionkit_repeater_observer_mqtt" },
|
||||
{ 0x21ee03bd, "heltec_v4_expansionkit_room_server_observer_mqtt" },
|
||||
{ 0x2224a925, "heltec_v4_expansionkit_tft_companion_radio_ble_ps_femon" },
|
||||
{ 0x5977d941, "heltec_v4_expansionkit_tft_companion_radio_ble_femon" },
|
||||
{ 0xf75feb3e, "heltec_v4_kiss_modem" },
|
||||
{ 0xbc9cdc70, "heltec_v4_r8_companion_radio_ble" },
|
||||
{ 0x36c78d86, "heltec_v4_r8_companion_radio_usb" },
|
||||
{ 0xdf722326, "heltec_v4_r8_companion_radio_wifi" },
|
||||
{ 0xfa901c87, "heltec_v4_r8_kiss_modem" },
|
||||
{ 0xeebe3469, "heltec_v4_r8_repeater" },
|
||||
{ 0x5729ad07, "heltec_v4_r8_room_server" },
|
||||
{ 0xb4cb035a, "heltec_v4_r8_sensor" },
|
||||
{ 0xbb429dde, "heltec_v4_r8_terminal_chat" },
|
||||
{ 0x883e3edc, "heltec_v4_r8_tft_companion_radio_ble" },
|
||||
{ 0xa0965a6b, "heltec_v4_r8_tft_companion_radio_usb" },
|
||||
{ 0x59ea25fa, "heltec_v4_r8_tft_companion_radio_wifi" },
|
||||
{ 0x1758bdb9, "heltec_v4_r8_tft_kiss_modem" },
|
||||
{ 0xa2fb4192, "heltec_v4_r8_tft_repeater" },
|
||||
{ 0x7964461c, "heltec_v4_r8_tft_room_server" },
|
||||
{ 0x5fc52bcb, "heltec_v4_r8_tft_sensor" },
|
||||
{ 0x0dacb683, "heltec_v4_r8_tft_terminal_chat" },
|
||||
{ 0xe792a051, "heltec_v4_repeater" },
|
||||
{ 0x2d5ea842, "heltec_v4_repeater_bridge_espnow" },
|
||||
{ 0xab74db9e, "heltec_v4_repeater_observer_mqtt" },
|
||||
@@ -353,12 +381,15 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x6925aaf0, "RAK_3112_terminal_chat" },
|
||||
{ 0x94c5555a, "RAK_3401_repeater" },
|
||||
{ 0x9f4d58ac, "RAK_4631_companion_radio_ble" },
|
||||
{ 0x7a55dec3, "RAK_4631_companion_radio_ethernet" },
|
||||
{ 0x48d3b5d4, "RAK_4631_companion_radio_usb" },
|
||||
{ 0x14e6965d, "RAK_4631_kiss_modem" },
|
||||
{ 0x22ea8497, "RAK_4631_repeater_bridge_rs232_serial1" },
|
||||
{ 0x21bae522, "RAK_4631_repeater_bridge_rs232_serial2" },
|
||||
{ 0x25c20350, "RAK_4631_repeater_ethernet" },
|
||||
{ 0xb876506d, "RAK_4631_repeater_no_external_sensors_lora_ota" },
|
||||
{ 0x626d80ed, "RAK_4631_room_server" },
|
||||
{ 0x6f9c3984, "RAK_4631_room_server_ethernet" },
|
||||
{ 0x8b1d6850, "RAK_4631_sensor" },
|
||||
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
|
||||
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
|
||||
|
||||
@@ -191,7 +191,18 @@ class RAK12500LocationProvider : public LocationProvider {
|
||||
int _sats = 0;
|
||||
long _epoch = 0;
|
||||
bool _fix = false;
|
||||
mesh::RTCClock* _clock = NULL;
|
||||
unsigned long _next_time_check = 0;
|
||||
unsigned long _last_time_sync = 0;
|
||||
uint8_t _valid_time_samples = 0;
|
||||
static const unsigned long TIME_SYNC_INTERVAL = 1800000;
|
||||
public:
|
||||
void setRTCClock(mesh::RTCClock* clock) { _clock = clock; }
|
||||
mesh::RTCClock* getRTCClock() override { return _clock; }
|
||||
void syncTime() override {
|
||||
_valid_time_samples = 0;
|
||||
LocationProvider::syncTime();
|
||||
}
|
||||
long getLatitude() override { return _lat; }
|
||||
long getLongitude() override { return _lng; }
|
||||
long getAltitude() override { return _alt; }
|
||||
@@ -212,7 +223,32 @@ public:
|
||||
} else {
|
||||
_fix = false;
|
||||
}
|
||||
bool date_valid = ublox_GNSS.getDateValid(2);
|
||||
bool time_valid = ublox_GNSS.getTimeValid(2);
|
||||
_epoch = ublox_GNSS.getUnixEpoch(2);
|
||||
|
||||
unsigned long now = millis();
|
||||
if ((long)(now - _next_time_check) >= 0) {
|
||||
_next_time_check = now + 1000;
|
||||
|
||||
if (_fix && _sats >= 5 && date_valid && time_valid && _epoch > 0) {
|
||||
if (_valid_time_samples < 0xFF) _valid_time_samples++;
|
||||
} else {
|
||||
_valid_time_samples = 0;
|
||||
}
|
||||
|
||||
if (!_time_sync_needed && _clock != NULL
|
||||
&& (unsigned long)(now - _last_time_sync) > TIME_SYNC_INTERVAL) {
|
||||
_time_sync_needed = true;
|
||||
}
|
||||
if (_time_sync_needed && _clock != NULL && _valid_time_samples > 2) {
|
||||
_clock->setCurrentTime((uint32_t)_epoch);
|
||||
markTimeSyncApplied();
|
||||
_time_sync_needed = false;
|
||||
_last_time_sync = now;
|
||||
_last_valid_time_sync = _clock->getCurrentTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
bool isEnabled() override { return true; }
|
||||
};
|
||||
@@ -899,6 +935,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
gps_active = true;
|
||||
gps_detected = true;
|
||||
|
||||
RAK12500_provider.setRTCClock(_location->getRTCClock());
|
||||
_location = &RAK12500_provider;
|
||||
return true;
|
||||
} else if (Serial1.available()) {
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
virtual void setNextGPSOn(unsigned long _millis) { _next_gps_on = _millis; }
|
||||
virtual unsigned long getNextGPSOn() { return _next_gps_on; }
|
||||
virtual unsigned long getLastValidTimeSync() { return _last_valid_time_sync; }
|
||||
virtual mesh::RTCClock* getRTCClock() { return NULL; }
|
||||
virtual long getLatitude() = 0;
|
||||
virtual long getLongitude() = 0;
|
||||
virtual long getAltitude() = 0;
|
||||
|
||||
@@ -110,6 +110,7 @@ public :
|
||||
}
|
||||
|
||||
void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }
|
||||
mesh::RTCClock* getRTCClock() override { return _clock; }
|
||||
long getLatitude() override { return nmea.getLatitude(); }
|
||||
long getLongitude() override { return nmea.getLongitude(); }
|
||||
long getAltitude() override {
|
||||
|
||||
@@ -405,6 +405,10 @@ static void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
|
||||
static TFT_eSprite *sprite = NULL;
|
||||
|
||||
static bool spriteReady() {
|
||||
return sprite != NULL && sprite->getPointer() != NULL;
|
||||
}
|
||||
|
||||
bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
return true;
|
||||
/*
|
||||
@@ -422,18 +426,22 @@ bool ST7735Display::begin() {
|
||||
if (!sprite) {
|
||||
// alloc offscreen canvas
|
||||
sprite = new TFT_eSprite(&lcd);
|
||||
if (sprite) {
|
||||
if (sprite->createSprite(160, 80)) {
|
||||
sprite->fillScreen(ST77XX_BLACK);
|
||||
sprite->setTextColor(curr_color = ST77XX_WHITE);
|
||||
} else {
|
||||
Serial.printf("ST7735Display: failed to alloc canvas pixels");
|
||||
}
|
||||
} else {
|
||||
Serial.printf("ST7735Display: failed to alloc canvas");
|
||||
if (!sprite) {
|
||||
Serial.println("ST7735Display: failed to alloc canvas");
|
||||
return false;
|
||||
}
|
||||
if (!sprite->createSprite(160, 80)) {
|
||||
Serial.println("ST7735Display: failed to alloc canvas pixels");
|
||||
delete sprite;
|
||||
sprite = NULL;
|
||||
return false;
|
||||
}
|
||||
sprite->fillScreen(ST77XX_BLACK);
|
||||
sprite->setTextColor(curr_color = ST77XX_WHITE);
|
||||
}
|
||||
|
||||
if (!spriteReady()) return false;
|
||||
|
||||
if (!_isOn) {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
|
||||
@@ -470,6 +478,8 @@ bool ST7735Display::begin() {
|
||||
}
|
||||
|
||||
void ST7735Display::_resetAndInit() {
|
||||
if (!spriteReady()) return;
|
||||
|
||||
// Pulse Reset low for 10ms
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
delay(2);
|
||||
@@ -528,10 +538,12 @@ void ST7735Display::turnOff() {
|
||||
}
|
||||
|
||||
void ST7735Display::clear() {
|
||||
if (!spriteReady()) return;
|
||||
sprite->fillScreen(ST77XX_BLACK);
|
||||
}
|
||||
|
||||
void ST7735Display::startFrame(Color bkg) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->fillScreen(ST77XX_BLACK);
|
||||
sprite->setTextColor(curr_color = ST77XX_WHITE);
|
||||
sprite->setFreeFont();
|
||||
@@ -540,6 +552,7 @@ void ST7735Display::startFrame(Color bkg) {
|
||||
}
|
||||
|
||||
void ST7735Display::setTextSize(int sz) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->setTextSize(sz);
|
||||
}
|
||||
|
||||
@@ -570,34 +583,41 @@ void ST7735Display::setColor(Color c) {
|
||||
curr_color = ST77XX_WHITE;
|
||||
break;
|
||||
}
|
||||
sprite->setTextColor(curr_color);
|
||||
if (spriteReady()) sprite->setTextColor(curr_color);
|
||||
}
|
||||
|
||||
void ST7735Display::setCursor(int x, int y) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->setCursor(x*SCALE_X, y*SCALE_Y);
|
||||
}
|
||||
|
||||
void ST7735Display::print(const char* str) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->print(str);
|
||||
}
|
||||
|
||||
void ST7735Display::fillRect(int x, int y, int w, int h) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color);
|
||||
}
|
||||
|
||||
void ST7735Display::drawRect(int x, int y, int w, int h) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color);
|
||||
}
|
||||
|
||||
void ST7735Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, curr_color);
|
||||
}
|
||||
|
||||
uint16_t ST7735Display::getTextWidth(const char* str) {
|
||||
if (!spriteReady()) return 0;
|
||||
return sprite->textWidth(str) / SCALE_X;
|
||||
}
|
||||
|
||||
void ST7735Display::endFrame() {
|
||||
if (!spriteReady()) return;
|
||||
// blit the canvas buffer to LCD
|
||||
set_CS(LOW);
|
||||
_spi->beginTransaction(_spiSettings);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
|
||||
TEST(CLICommandUtils, NormalizesCapitalizedSetVerb) {
|
||||
char command[] = "Set altpath 600000,0d2784,F8DADA";
|
||||
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
EXPECT_STREQ("set altpath 600000,0d2784,F8DADA", command);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, NormalizesAnyCommandVerbCase) {
|
||||
char get_command[] = "GET outpath";
|
||||
char clear_command[] = "cLeAr recent.repeater";
|
||||
|
||||
mesh::cli::normalizeCommandVerb(get_command);
|
||||
mesh::cli::normalizeCommandVerb(clear_command);
|
||||
|
||||
EXPECT_STREQ("get outpath", get_command);
|
||||
EXPECT_STREQ("clear recent.repeater", clear_command);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, PreservesCaseSensitiveArguments) {
|
||||
char command[] = "SET name RidgeNode-A";
|
||||
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
EXPECT_STREQ("set name RidgeNode-A", command);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, HandlesLeadingWhitespaceAndSingleWordCommands) {
|
||||
char spaced_command[] = " Get outpath";
|
||||
char single_command[] = "PowerSaving";
|
||||
|
||||
mesh::cli::normalizeCommandVerb(spaced_command);
|
||||
mesh::cli::normalizeCommandVerb(single_command);
|
||||
|
||||
EXPECT_STREQ(" get outpath", spaced_command);
|
||||
EXPECT_STREQ("powersaving", single_command);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Two 1.5 MiB OTA slots with 896 KiB SPIFFS on a 4 MiB ESP32.
|
||||
# Use for targets whose aligned app image no longer fits Arduino's 1.25 MiB default slots.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x180000,
|
||||
app1, app, ota_1, 0x190000,0x180000,
|
||||
spiffs, data, spiffs, 0x310000,0xE0000,
|
||||
coredump, data, coredump,0x3F0000,0x10000,
|
||||
|
@@ -1,6 +1,9 @@
|
||||
[Ebyte_EoRa-S3]
|
||||
extends = esp32_base
|
||||
board = ebyte_eora-s3
|
||||
; Keep dual OTA slots and SPIFFS while avoiding the board definition's
|
||||
; undersized 1.25 MiB default app partition.
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-I variants/ebyte_eora_s3
|
||||
|
||||
@@ -36,7 +36,14 @@ lib_deps =
|
||||
extends = GAT562_30S_Mesh_Kit
|
||||
build_flags =
|
||||
${GAT562_30S_Mesh_Kit.build_flags}
|
||||
; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The
|
||||
; room-server and companion targets retain the complete optional sensor set.
|
||||
${nrf52_ota_sensor_trim.build_flags}
|
||||
-UENV_INCLUDE_MLX90614
|
||||
-UENV_INCLUDE_BMP085
|
||||
-UENV_INCLUDE_INA219
|
||||
-UENV_INCLUDE_INA226
|
||||
-UENV_INCLUDE_INA260
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"GAT562 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
|
||||
@@ -29,6 +29,7 @@ lib_deps =
|
||||
|
||||
[env:Generic_E22_sx1262_repeater]
|
||||
extends = Generic_E22
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
build_flags =
|
||||
@@ -75,6 +76,7 @@ lib_deps =
|
||||
|
||||
[env:Generic_E22_sx1262_repeater_bridge_espnow]
|
||||
extends = Generic_E22
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
@@ -109,6 +111,7 @@ build_flags =
|
||||
|
||||
[env:Generic_E22_sx1268_repeater]
|
||||
extends = Generic_E22
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
build_flags =
|
||||
@@ -155,6 +158,7 @@ lib_deps =
|
||||
|
||||
[env:Generic_E22_sx1268_repeater_bridge_espnow]
|
||||
extends = Generic_E22
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
|
||||
@@ -36,6 +36,9 @@ lib_deps =
|
||||
|
||||
[env:Generic_ESPNOW_repeatr]
|
||||
extends = Generic_ESPNOW
|
||||
; WebUI plus the EndF trailer no longer fits the board's 1.25 MiB app slots.
|
||||
; Keep dual OTA and SPIFFS using the larger established layout.
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags =
|
||||
${Generic_ESPNOW.build_flags}
|
||||
-D ADVERT_NAME='"ESPNOW Repeater"'
|
||||
@@ -67,6 +70,7 @@ lib_deps =
|
||||
|
||||
[env:Generic_ESPNOW_room_svr]
|
||||
extends = Generic_ESPNOW
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags =
|
||||
${Generic_ESPNOW.build_flags}
|
||||
-D ADVERT_NAME='"Generic ESPNow Room"'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Heltec_ct62]
|
||||
extends = esp32_base
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = variants/dual_ota_1536k.csv
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-I variants/heltec_ct62
|
||||
@@ -42,6 +43,7 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and dual-slot LoRa OTA
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_ct62.build_src_filter}
|
||||
@@ -82,6 +84,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and dual-slot LoRa OTA
|
||||
; -D BRIDGE_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
@@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
SolarSensorManager sensors = SolarSensorManager(nmea);
|
||||
SolarExternalWatchdog external_watchdog;
|
||||
|
||||
|
||||
@@ -67,6 +67,16 @@ build_src_filter = ${Heltec_t096.build_src_filter}
|
||||
|
||||
build_flags =
|
||||
${Heltec_t096.build_flags}
|
||||
; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The
|
||||
; sensor and companion targets retain the complete optional sensor set.
|
||||
${nrf52_ota_sensor_trim.build_flags}
|
||||
-UENV_INCLUDE_BMP085
|
||||
-UENV_INCLUDE_MLX90614
|
||||
-UENV_INCLUDE_INA226
|
||||
-UENV_INCLUDE_INA260
|
||||
-UENV_INCLUDE_INA219
|
||||
-UENV_INCLUDE_LPS22HB
|
||||
-UENV_INCLUDE_BMP280
|
||||
-D ADVERT_NAME='"Heltec_t096 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -79,6 +89,15 @@ build_flags =
|
||||
extends = Heltec_t096
|
||||
build_flags =
|
||||
${Heltec_t096.build_flags}
|
||||
; The RS232 bridge shares the repeater's strict in-place LoRa OTA budget.
|
||||
${nrf52_ota_sensor_trim.build_flags}
|
||||
-UENV_INCLUDE_BMP085
|
||||
-UENV_INCLUDE_MLX90614
|
||||
-UENV_INCLUDE_INA226
|
||||
-UENV_INCLUDE_INA260
|
||||
-UENV_INCLUDE_INA219
|
||||
-UENV_INCLUDE_LPS22HB
|
||||
-UENV_INCLUDE_BMP280
|
||||
-D ADVERT_NAME='"RS232 Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter}
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
${sensor_base.lib_deps}
|
||||
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
|
||||
bodmer/TFT_eSPI @ ^2.4.31
|
||||
debug_tool = jlink
|
||||
upload_protocol = nrfutil
|
||||
|
||||
@@ -36,6 +36,16 @@ build_src_filter = ${Heltec_t1.build_src_filter}
|
||||
|
||||
build_flags =
|
||||
${Heltec_t1.build_flags}
|
||||
; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The
|
||||
; room-server and companion targets retain the complete optional sensor set.
|
||||
${nrf52_ota_sensor_trim.build_flags}
|
||||
-UENV_INCLUDE_BMP085
|
||||
-UENV_INCLUDE_MLX90614
|
||||
-UENV_INCLUDE_INA226
|
||||
-UENV_INCLUDE_INA260
|
||||
-UENV_INCLUDE_INA219
|
||||
-UENV_INCLUDE_LPS22HB
|
||||
-UENV_INCLUDE_BMP280
|
||||
-D ADVERT_NAME='"Heltec_t1 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -89,6 +89,10 @@ const char* HeltecTowerV2Board::getManufacturerName() const {
|
||||
}
|
||||
|
||||
void HeltecTowerV2Board::powerOff() {
|
||||
shutdownPeripherals();
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
initiateShutdown(SHUTDOWN_REASON_USER);
|
||||
#else
|
||||
pinMode(PIN_GPS_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE);
|
||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||
@@ -100,4 +104,5 @@ void HeltecTowerV2Board::powerOff() {
|
||||
digitalWrite(PIN_BAT_CTL, LOW);
|
||||
variant_shutdown();
|
||||
sd_power_system_off();
|
||||
#endif
|
||||
}
|
||||
@@ -189,17 +189,6 @@ build_flags =
|
||||
${env:heltec_tracker_v2_companion_radio_ble_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_tracker_v2_companion_radio_ble_ps_femon]
|
||||
extends = env:heltec_tracker_v2_companion_radio_ble_femon
|
||||
build_flags =
|
||||
${env:heltec_tracker_v2_companion_radio_ble_femon.build_flags}
|
||||
|
||||
[env:heltec_tracker_v2_companion_radio_ble_ps_femoff]
|
||||
extends = env:heltec_tracker_v2_companion_radio_ble_ps_femon
|
||||
build_flags =
|
||||
${env:heltec_tracker_v2_companion_radio_ble_ps_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_tracker_v2_companion_radio_wifi_femon]
|
||||
extends = Heltec_tracker_v2
|
||||
build_flags =
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, NULL, GPS_RESET, GPS_EN, &board.periph_power);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -43,4 +43,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -50,37 +50,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
|
||||
// Clear stale wakeup sources to avoid ghost wakeup
|
||||
// This is required when Power Management and automatic lightsleep are enabled
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
||||
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
|
||||
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
|
||||
|
||||
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
|
||||
|
||||
if (pin_wake_btn < 0) {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
|
||||
} else {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
|
||||
}
|
||||
|
||||
if (secs > 0) {
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000);
|
||||
}
|
||||
|
||||
// Finally set ESP32 into sleep
|
||||
esp_deep_sleep_start(); // CPU halts here and never returns!
|
||||
}
|
||||
|
||||
void powerOff() override {
|
||||
enterDeepSleep(0);
|
||||
}
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
analogReadResolution(10);
|
||||
digitalWrite(PIN_ADC_CTRL, adc_active_state);
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -43,4 +43,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -472,17 +472,6 @@ build_flags =
|
||||
${env:heltec_v4_companion_radio_ble_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_v4_companion_radio_ble_ps_femon]
|
||||
extends = env:heltec_v4_companion_radio_ble_femon
|
||||
build_flags =
|
||||
${env:heltec_v4_companion_radio_ble_femon.build_flags}
|
||||
|
||||
[env:heltec_v4_3_companion_radio_ble_ps_femoff]
|
||||
extends = env:heltec_v4_companion_radio_ble_ps_femon
|
||||
build_flags =
|
||||
${env:heltec_v4_companion_radio_ble_ps_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_v4_companion_radio_wifi_femon]
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
@@ -697,17 +686,17 @@ build_flags =
|
||||
${env:heltec_v4_tft_companion_radio_ble_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_v4_expansionkit_tft_companion_radio_ble_ps_femon]
|
||||
[env:heltec_v4_expansionkit_tft_companion_radio_ble_femon]
|
||||
extends = env:heltec_v4_tft_companion_radio_ble_femon
|
||||
build_flags =
|
||||
${env:heltec_v4_tft_companion_radio_ble_femon.build_flags}
|
||||
-D ENV_PIN_SDA=4
|
||||
-D ENV_PIN_SCL=3
|
||||
|
||||
[env:heltec_v4_3_expansionkit_tft_companion_radio_ble_ps_femoff]
|
||||
extends = env:heltec_v4_expansionkit_tft_companion_radio_ble_ps_femon
|
||||
[env:heltec_v4_3_expansionkit_tft_companion_radio_ble_femoff]
|
||||
extends = env:heltec_v4_expansionkit_tft_companion_radio_ble_femon
|
||||
build_flags =
|
||||
${env:heltec_v4_expansionkit_tft_companion_radio_ble_ps_femon.build_flags}
|
||||
${env:heltec_v4_expansionkit_tft_companion_radio_ble_femon.build_flags}
|
||||
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
|
||||
|
||||
[env:heltec_v4_tft_companion_radio_wifi_femon]
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -43,4 +43,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -39,30 +39,12 @@ void HeltecV4R8Board::onAfterTransmit(void) {
|
||||
loRaFEMControl.setRxModeEnable();
|
||||
}
|
||||
|
||||
void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
|
||||
|
||||
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
|
||||
loRaFEMControl.setRxModeEnableWhenMCUSleep();
|
||||
|
||||
if (pin_wake_btn < 0) {
|
||||
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||
} else {
|
||||
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||
}
|
||||
|
||||
if (secs > 0) {
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000);
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
void HeltecV4R8Board::powerOff() {
|
||||
enterDeepSleep(0);
|
||||
loRaFEMControl.setSleepModeEnable();
|
||||
digitalWrite(P_LORA_PA_POWER, LOW);
|
||||
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER);
|
||||
periph_power.release(); // Drop the permanent antenna-boost/VEXT claim from begin().
|
||||
ESP32Board::powerOff();
|
||||
}
|
||||
|
||||
uint16_t HeltecV4R8Board::getBattMilliVolts() {
|
||||
|
||||
@@ -23,7 +23,6 @@ public:
|
||||
void begin();
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
|
||||
void powerOff() override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
|
||||
@@ -12,7 +12,7 @@ VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -34,4 +34,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ lib_deps =
|
||||
extends = LilyGo_T3S3_sx1262
|
||||
build_flags =
|
||||
${LilyGo_T3S3_sx1262.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"T3S3-1262 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
@@ -81,6 +82,7 @@ lib_deps =
|
||||
extends = LilyGo_T3S3_sx1262
|
||||
build_flags =
|
||||
${LilyGo_T3S3_sx1262.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
|
||||
@@ -35,6 +35,7 @@ lib_deps =
|
||||
extends = LilyGo_T3S3_sx1276
|
||||
build_flags =
|
||||
${LilyGo_T3S3_sx1276.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"T3S3-1276 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
@@ -78,6 +79,7 @@ lib_deps =
|
||||
extends = LilyGo_T3S3_sx1276
|
||||
build_flags =
|
||||
${LilyGo_T3S3_sx1276.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
|
||||
@@ -19,7 +19,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -47,4 +47,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -38,4 +38,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -42,4 +42,4 @@ bool radio_init() {
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -35,4 +35,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ lib_deps =
|
||||
|
||||
[env:LilyGo_Tlora_C6_companion_radio_ble_]
|
||||
extends = tlora_c6
|
||||
; BLE plus the shared WebUI exceeds a dual-OTA slot. Companion releases
|
||||
; explicitly disable LoRa OTA, so keep SPIFFS and use the 3 MB app layout.
|
||||
board_build.partitions = huge_app.csv
|
||||
build_flags = ${tlora_c6.build_flags}
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
|
||||
@@ -52,6 +52,10 @@ lib_deps =
|
||||
; === LILYGO T-LoRa V2.1-1.6 with SX1276 environments ===
|
||||
[env:LilyGo_TLora_V2_1_1_6_repeater]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
; The shared WebUI needs the largest dual-OTA layout that still leaves a 64 KB
|
||||
; SPIFFS identity store. Keep both OTA slots and use the smaller built-in
|
||||
; upload handler instead of ElegantOTA on this ultra-tight 4 MB target.
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
@@ -62,6 +66,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D FLOOD_CHANNEL_SCOPE_SLOTS=4 ; ultra-tight classic ESP32: three wildcards plus one exact channel
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
; -D CORE_DEBUG_LEVEL=3
|
||||
@@ -102,6 +107,8 @@ lib_deps =
|
||||
|
||||
[env:LilyGo_TLora_V2_1_1_6_companion_radio_ble]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
; Keep both OTA slots while giving the BLE stack enough room for the shared companion code.
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_flags =
|
||||
${LilyGo_TLora_V2_1_1_6.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
@@ -126,6 +133,8 @@ extends = env:LilyGo_TLora_V2_1_1_6_companion_radio_ble
|
||||
|
||||
[env:LilyGo_TLora_V2_1_1_6_room_server]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
; Preserve full WebUI/ElegantOTA and LoRa OTA with the target's larger dual-slot layout.
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_room_server>
|
||||
@@ -162,7 +171,9 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D MAX_RECENT_REPEATERS=256 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS
|
||||
-D WITH_MQTT_BRIDGE=1
|
||||
-D WEBCONFIG_DISABLED=1 ; async portal exceeds this 4 MB target's dual-OTA app slot; use CLI setup
|
||||
-D FLOOD_CHANNEL_SCOPE_SLOTS=4 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS
|
||||
-D OTA_MANIFEST_BASE='"https://observer.gessaman.com/v"'
|
||||
-D MAX_MQTT_BROKERS=3
|
||||
@@ -210,6 +221,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D ROOM_PASSWORD='"hello"'
|
||||
-D WITH_MQTT_BRIDGE=1
|
||||
-D WEBCONFIG_DISABLED=1 ; async portal exceeds this 4 MB target's dual-OTA app slot; use CLI setup
|
||||
-D OTA_MANIFEST_BASE='"https://observer.gessaman.com/v"'
|
||||
-D MAX_MQTT_BROKERS=3
|
||||
-D MQTT_MAX_PACKET_SIZE=1024
|
||||
@@ -237,6 +249,7 @@ lib_deps =
|
||||
|
||||
[env:LilyGo_TLora_V2_1_1_6_companion_radio_wifi]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_flags =
|
||||
${LilyGo_TLora_V2_1_1_6.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
@@ -260,6 +273,7 @@ lib_deps =
|
||||
;
|
||||
[env:LilyGo_TLora_V2_1_1_6_repeater_bridge_rs232]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
@@ -284,6 +298,7 @@ lib_deps =
|
||||
|
||||
[env:LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow]
|
||||
extends = LilyGo_TLora_V2_1_1_6
|
||||
board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv
|
||||
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
@@ -72,11 +72,15 @@ lib_deps =
|
||||
|
||||
[env:M5Stack_Unit_C6L_companion_radio_ble]
|
||||
extends = M5Stack_Unit_C6L
|
||||
; BLE and the shared WebUI do not fit in the base target's 1.875 MB OTA slot.
|
||||
; Companion profiles are not LoRa-OTA builds, so use the board's existing
|
||||
; single-app 3 MB layout and retain a 896 KB SPIFFS data store.
|
||||
board_build.partitions = huge_app.csv
|
||||
build_flags = ${M5Stack_Unit_C6L.build_flags}
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
; -D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
@@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
MASensorManager sensors = MASensorManager(nmea);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -17,9 +17,10 @@ extern EnvironmentSensorManager sensors;
|
||||
EnvironmentSensorManager sensors;
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
NullDisplayDriver display;
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
NullDisplayDriver display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
#ifndef LORA_CR
|
||||
#define LORA_CR 5
|
||||
@@ -80,4 +81,4 @@ bool radio_init() {
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,15 @@
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "NullDisplayDriver.h"
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern NullDisplayDriver display;
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "NullDisplayDriver.h"
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern NullDisplayDriver display;
|
||||
extern MomentaryButton user_btn;
|
||||
#endif
|
||||
|
||||
extern MinewsemiME25LS01Board board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void powerOff() override {
|
||||
shutdownPeripherals();
|
||||
initiateShutdown(SHUTDOWN_REASON_USER);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
R1NeoBoard board;
|
||||
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000);
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
NanoG2UltraSensorManager sensors = NanoG2UltraSensorManager(nmea);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -59,10 +59,11 @@ build_flags =
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
build_src_filter = ${nibble_screen_connect_base.build_src_filter}
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and the original dual-OTA layout
|
||||
build_src_filter = ${nibble_screen_connect_base.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
@@ -59,6 +59,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and the original dual-OTA layout
|
||||
build_src_filter = ${nibble_zero_connect_base.build_src_filter}
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
@@ -155,4 +156,3 @@ lib_deps =
|
||||
${nibble_zero_connect_base.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -34,4 +34,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -43,4 +43,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -41,4 +41,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ lib_deps =
|
||||
extends = rak4631
|
||||
build_flags =
|
||||
${rak4631.build_flags}
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1 ; Ethernet image exceeds the safe nRF52 in-place OTA ceiling
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"RAK4631 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
@@ -192,6 +194,8 @@ build_src_filter = ${rak4631.build_src_filter}
|
||||
extends = rak4631
|
||||
build_flags =
|
||||
${rak4631.build_flags}
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1 ; Ethernet image exceeds the safe nRF52 in-place OTA ceiling
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"RAK4631 Room Server"'
|
||||
-D ADVERT_LAT=0.0
|
||||
|
||||
@@ -26,7 +26,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
|
||||
@@ -22,7 +22,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -37,4 +37,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public:
|
||||
#endif
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
shutdownPeripherals();
|
||||
initiateShutdown(SHUTDOWN_REASON_USER);
|
||||
#else
|
||||
NRF52Board::powerOff();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Tenstar_esp32_C3]
|
||||
extends = esp32_base
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-I variants/tenstar_c3
|
||||
|
||||
@@ -45,6 +45,7 @@ build_src_filter = ${ThinkNode_M2.build_src_filter}
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
build_flags =
|
||||
${ThinkNode_M2.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and the original dual-OTA layout
|
||||
-D ADVERT_NAME='"Thinknode M2 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -84,6 +85,7 @@ build_src_filter = ${ThinkNode_M2.build_src_filter}
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
build_flags =
|
||||
${ThinkNode_M2.build_flags}
|
||||
-D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI, browser OTA, and the original dual-OTA layout
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#ifdef ENV_INCLUDE_GPS
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager();
|
||||
@@ -81,4 +81,4 @@ bool radio_init() {
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
[ThinkNode_M5]
|
||||
extends = esp32_base
|
||||
board = ESP32-S3-WROOM-1-N4
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags = ${esp32_base.build_flags}
|
||||
-I variants/thinknode_m5
|
||||
-I src/helpers/sensors
|
||||
|
||||
@@ -4,14 +4,6 @@ void ThinkNodeM7Board::begin() {
|
||||
ESP32Board::begin();
|
||||
}
|
||||
|
||||
void ThinkNodeM7Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
void ThinkNodeM7Board::powerOff() {
|
||||
enterDeepSleep(0);
|
||||
}
|
||||
|
||||
const char* ThinkNodeM7Board::getManufacturerName() const {
|
||||
return "Elecrow ThinkNode M7";
|
||||
}
|
||||
@@ -11,8 +11,6 @@ class ThinkNodeM7Board : public ESP32Board {
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
|
||||
void powerOff() override;
|
||||
const char* getManufacturerName() const override;
|
||||
void onBeforeTransmit() override {
|
||||
digitalWrite(P_LORA_TX_LED, LOW);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
[Xiao_esp32_C3]
|
||||
extends = esp32_base
|
||||
board = seeed_xiao_esp32c3
|
||||
; Preserve two OTA app slots while allowing the WebUI-enabled firmware to
|
||||
; grow beyond the board definition's 1.25 MiB default app partition.
|
||||
board_build.partitions = min_spiffs.csv
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-I variants/xiao_c3
|
||||
|
||||
@@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
@@ -39,4 +39,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ lib_deps =
|
||||
|
||||
[env:Xiao_C6_companion_radio_ble_]
|
||||
extends = Xiao_C6
|
||||
; Companions have no OTA transport; keep a large app and SPIFFS-backed identity/contact storage.
|
||||
board_build.partitions = huge_app.csv
|
||||
build_flags = ${Xiao_C6.build_flags}
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
@@ -72,7 +74,6 @@ lib_deps =
|
||||
; Meshimi variant
|
||||
[Meshimi]
|
||||
extends = Xiao_C6
|
||||
board_build.partitions = max_app_4MB.csv
|
||||
build_flags =
|
||||
${Xiao_C6.build_flags}
|
||||
-D P_LORA_TX_LED=15
|
||||
@@ -108,6 +109,8 @@ lib_deps =
|
||||
|
||||
[env:Meshimi_companion_radio_ble_]
|
||||
extends = Meshimi
|
||||
; The repeater inherits dual OTA; only the no-OTA companion needs the single large app layout.
|
||||
board_build.partitions = huge_app.csv
|
||||
build_flags = ${Meshimi.build_flags}
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
|
||||
@@ -34,6 +34,7 @@ build_src_filter = ${rak4631_hw.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/sensors>
|
||||
+<../variants/xiao_nrf52>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/ui/NullDisplayDriver.cpp>
|
||||
debug_tool = jlink
|
||||
upload_protocol = nrfutil
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
|
||||
#endif
|
||||
|
||||
XiaoNrf52Board board;
|
||||
@@ -32,4 +33,4 @@ bool radio_init() {
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/NullDisplayDriver.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
extern DISPLAY_CLASS display;
|
||||
extern MomentaryButton user_btn;
|
||||
#endif
|
||||
|
||||
extern XiaoNrf52Board board;
|
||||
@@ -28,4 +30,3 @@ extern EnvironmentSensorManager sensors;
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
+1029
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user