diff --git a/MQTT_IMPLEMENTATION.md b/MQTT_IMPLEMENTATION.md index 5ba66247..6f3d4de3 100644 --- a/MQTT_IMPLEMENTATION.md +++ b/MQTT_IMPLEMENTATION.md @@ -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 . +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:** diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 9eb516c1..42a897c8 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -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 \ diff --git a/build.sh b/build.sh index 2348918d..fa3009bd 100755 --- a/build.sh +++ b/build.sh @@ -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 $? diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 08140f1e..667b13e5 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -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 + 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 diff --git a/docs/faq.md b/docs/faq.md index ee8ade55..9dd15310 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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 . +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 , 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 . 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 . 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:** diff --git a/docs/flood_filtering.md b/docs/flood_filtering.md index 09814366..c6fdb880 100644 --- a/docs/flood_filtering.md +++ b/docs/flood_filtering.md @@ -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 diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 151e9802..16f1ce85 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -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=` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] [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 ` | `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] `, `del flood.channel.scope.|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] [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.|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] [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.|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] [path=...]`, `del flood.moderation.|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 `, `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 ` | `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 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e57f2fae..f4030d58 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -7,6 +7,12 @@ #include #endif +#ifdef WITH_WEBCONFIG +#include +#include +#include +#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(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(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(sf); + _prefs.cr = static_cast(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(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(*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(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(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(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(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(buf_size - pos)) break; + pos += written; + first = false; + } +#endif + snprintf(buf + pos, buf_size - pos, "]}"); } #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3da32813..d4f8d00b 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -26,6 +26,10 @@ #include "DataStore.h" #include "NodePrefs.h" +#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) && !defined(WEBCONFIG_DISABLED) +#include +#endif + #if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID) #include #include @@ -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; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8e5b4f50..c85e37f2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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 } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9a6403f9..4d3aa53e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,8 +1,12 @@ #include "MyMesh.h" #include #include // for qsort() +#include #include #include +#ifdef WITH_WEBCONFIG +#include +#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(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(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); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 2e453fc6..66c195f4 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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(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; diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 977be98f..8e79c784 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -7,8 +7,9 @@ #define USER_BTN_PRESSED LOW #endif -#ifdef WITH_MQTT_BRIDGE +#ifdef ESP_PLATFORM #include +#include #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 diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 5a70343e..960167ed 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -2,6 +2,9 @@ #include #include "MyMesh.h" +#if defined(ESP32) && MAX_RECENT_REPEATERS > 0 + #include +#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 diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index e16b5429..22549664 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1,5 +1,9 @@ #include "MyMesh.h" +#include #include +#ifdef WITH_WEBCONFIG +#include +#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(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(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); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 884c7bdf..d959fdb4 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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(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 diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 47d00afa..91982458 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -6,8 +6,9 @@ #define USER_BTN_PRESSED LOW #endif -#ifdef WITH_MQTT_BRIDGE +#ifdef ESP_PLATFORM #include +#include #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(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 60e719fc..dde26c4a 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,4 +1,5 @@ #include "SensorMesh.h" +#include 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 diff --git a/platformio.ini b/platformio.ini index 4e48eff9..f4bbf843 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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} + + + + diff --git a/scripts/check_esp32_app_size.py b/scripts/check_esp32_app_size.py new file mode 100644 index 00000000..b545ea7d --- /dev/null +++ b/scripts/check_esp32_app_size.py @@ -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(" 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()) diff --git a/scripts/generate_webconfig_html.py b/scripts/generate_webconfig_html.py new file mode 100644 index 00000000..139ae549 --- /dev/null +++ b/scripts/generate_webconfig_html.py @@ -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 ") + lines.append("#include ") + 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() diff --git a/src/MeshCore.h b/src/MeshCore.h index eba1741a..bf3c1fde 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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. diff --git a/src/helpers/CLICommandUtils.h b/src/helpers/CLICommandUtils.h new file mode 100644 index 00000000..fe337867 --- /dev/null +++ b/src/helpers/CLICommandUtils.h @@ -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(*command - 'A' + 'a'); + } + command++; + } +} + +} // namespace cli +} // namespace mesh diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9fbac89a..465d9b72 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,5 +1,6 @@ #include #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()) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 1a5a6d0b..2772976e 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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 }; diff --git a/src/helpers/CommonCLI_Observer.cpp b/src/helpers/CommonCLI_Observer.cpp index 27f22fdf..1a694c66 100644 --- a/src/helpers/CommonCLI_Observer.cpp +++ b/src/helpers/CommonCLI_Observer.cpp @@ -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); diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index c620c96a..09e45417 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -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 diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index f125362d..e41962a9 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -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 diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 2645ae5e..24bdb1fe 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -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); diff --git a/src/helpers/esp32/WebConfigHtml.h b/src/helpers/esp32/WebConfigHtml.h new file mode 100644 index 00000000..839b391b --- /dev/null +++ b/src/helpers/esp32/WebConfigHtml.h @@ -0,0 +1,1113 @@ +// Auto-generated by scripts/generate_webconfig_html.py from webui/index.html +// DO NOT EDIT - edit webui/index.html instead. +#pragma once +#include +#include + +const uint32_t WEBCONFIG_HTML_GZ_LEN = 17638; +const char WEBCONFIG_HTML_ETAG[] = "\"bb7880fb3d9a27bd\""; +const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = { + 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0x03,0xd5,0x7d,0x5b,0x7b,0xdb,0x46, + 0x96,0xe0,0xbb,0x7f,0x05,0x0c,0x67,0x24,0x20,0x02,0x21,0x92,0xba,0x58,0x26,0x45, + 0x69,0x14,0xd9,0xee,0xb8,0xdb,0xb7,0xb1,0x94,0x4e,0x66,0xd4,0x6e,0x07,0x24,0x40, + 0x11,0x11,0x08,0x20,0x00,0x28,0x4a,0xa6,0xf5,0x7d,0xfd,0xb8,0xfb,0xbc,0xfb,0x38, + 0xf3,0xb2,0x0f,0xf3,0x17,0xf6,0x7d,0x7f,0x4a,0xff,0x92,0x3d,0x97,0x2a,0xa0,0x70, + 0x21,0x45,0x25,0xee,0x6f,0x76,0x3b,0x9d,0x08,0x04,0xea,0x7a,0xea,0xd4,0xb9,0xd5, + 0x39,0xa7,0x0e,0x1f,0x3f,0x7f,0x77,0x7a,0xfe,0xaf,0xef,0x5f,0x68,0x93,0x6c,0x1a, + 0x1c,0x3d,0x3a,0xc4,0x3f,0x5a,0xe0,0x84,0x97,0x03,0xdd,0x0b,0x75,0x7c,0xe1,0x39, + 0x2e,0xfc,0x99,0x7a,0x99,0xa3,0x8d,0x26,0x4e,0x92,0x7a,0xd9,0x40,0x9f,0x65,0xe3, + 0xd6,0x81,0x2e,0x5f,0x87,0xce,0xd4,0x1b,0xe8,0xd7,0xbe,0x37,0x8f,0xa3,0x24,0xd3, + 0xb5,0x51,0x14,0x66,0x5e,0x08,0xc5,0xe6,0xbe,0x9b,0x4d,0x06,0xae,0x77,0xed,0x8f, + 0xbc,0x16,0xfd,0xb0,0x34,0x3f,0xf4,0x33,0xdf,0x09,0x5a,0xe9,0xc8,0x09,0xbc,0x41, + 0xc7,0xd2,0xa6,0xce,0x8d,0x3f,0x9d,0x4d,0x8b,0x17,0xb2,0xa1,0xd6,0xd8,0xcf,0x06, + 0xa3,0xe8,0xda,0x4b,0xb0,0xa7,0xcc,0xcf,0x02,0xef,0xe8,0x8d,0x97,0x4e,0x4e,0xa3, + 0xc4,0xd3,0x4e,0xa3,0x70,0xec,0x5f,0x1e,0x6e,0xf3,0xeb,0x47,0x87,0x69,0x76,0x8b, + 0x7f,0x7b,0x49,0x14,0x65,0x8b,0x47,0x9a,0xd6,0x6a,0x0d,0x2f,0x7b,0x4f,0xc6,0xbb, + 0xe3,0xfd,0xf1,0x41,0x1f,0x7e,0x8d,0x9c,0xc4,0x85,0xdf,0xe3,0x31,0xfe,0xf0,0xc3, + 0xab,0xde,0x93,0x8e,0xd3,0xdd,0xd9,0x69,0xe3,0xcf,0xe9,0x2c,0xeb,0x3d,0xd9,0xdf, + 0x7f,0xba,0x73,0xe0,0xe0,0xcf,0xc0,0x0f,0xbd,0xde,0x13,0xaf,0xeb,0x3d,0xf5,0xbc, + 0x3e,0x35,0xe5,0x8c,0x46,0xbd,0x27,0xdd,0xe1,0x53,0xd7,0x7b,0xd6,0xe7,0x9f,0xdc, + 0x84,0x68,0x2e,0xc2,0xd6,0xdc,0x67,0xee,0x1e,0xfd,0xf2,0x92,0xa4,0xf7,0xc4,0xdd, + 0xdf,0xdd,0xdb,0xdd,0xc3,0x9f,0x73,0x27,0x09,0x7b,0x4f,0x46,0x07,0x07,0x4f,0x3b, + 0x0e,0xb7,0x36,0x9a,0xf8,0x31,0xb4,0xef,0x8d,0xbb,0xe3,0xa7,0x3c,0x1a,0x1e,0xab, + 0x1c,0x9b,0x18,0xc0,0xe8,0xc0,0xed,0xba,0x1e,0xbe,0x4a,0x27,0x8e,0x1b,0xcd,0x7b, + 0x6d,0xad,0x13,0xdf,0x68,0x3b,0xf0,0x6f,0x72,0x39,0x74,0x8c,0xce,0xbe,0xd5,0x3d, + 0xb0,0x76,0xf7,0x2c,0xbb,0x7d,0x60,0xf6,0x1f,0xdd,0x3d,0xfa,0xe7,0xa9,0xe7,0xfa, + 0x8e,0x66,0xc4,0x89,0x37,0xf6,0x92,0xb4,0x35,0x8a,0x82,0x28,0x01,0xb0,0x4e,0xbc, + 0xa9,0xd7,0x73,0x9d,0xe4,0xca,0x5c,0x54,0xc0,0xd3,0x69,0x77,0xf6,0x3a,0xa3,0x02, + 0x3c,0x00,0x92,0x4e,0x77,0x98,0x43,0xc8,0xdb,0xf7,0x86,0xe3,0x6e,0x0e,0xa1,0x83, + 0xe1,0xb3,0x03,0x67,0x58,0x40,0xa8,0xeb,0xec,0xec,0xee,0x76,0x15,0x08,0xed,0x3a, + 0xcf,0x76,0xc7,0x04,0x51,0x9e,0x62,0x77,0xa7,0xeb,0xee,0x38,0xca,0x14,0x3b,0xbb, + 0xd0,0x43,0xb7,0x34,0xcb,0x1d,0x67,0x77,0x7f,0x6f,0x7f,0xf9,0x2c,0xdb,0x16,0xfe, + 0x63,0xef,0xe2,0x0c,0xef,0x1e,0x7d,0xbb,0x18,0x46,0x37,0xad,0xd4,0xff,0xec,0x87, + 0x97,0xbd,0x61,0x94,0xb8,0x5e,0xd2,0x82,0x37,0xfd,0xa9,0x93,0x5c,0xfa,0x61,0xaf, + 0x7d,0xf7,0x08,0xf1,0x77,0xd1,0x9a,0x7b,0xc3,0x2b,0x3f,0x6b,0x65,0xde,0x4d,0x86, + 0xa5,0xbd,0x96,0xe3,0xfe,0x32,0x4b,0xb3,0x5e,0xa7,0xdd,0xfe,0xa7,0xbb,0x47,0xc3, + 0xc8,0xbd,0x5d,0x8c,0x01,0x49,0x7b,0x9d,0xbd,0xf8,0x66,0xbb,0x63,0xef,0xee,0x69, + 0xe9,0x6d,0x9a,0x79,0xd3,0xd6,0xcc,0xb7,0x5a,0x4e,0x1c,0x07,0x5e,0x8b,0x5f,0x58, + 0xfa,0x99,0x77,0x19,0x79,0xda,0x0f,0xaf,0x74,0xeb,0x43,0x34,0x8c,0xb2,0xc8,0x4a, + 0x9d,0x30,0x6d,0xa5,0x5e,0xe2,0x8f,0xfb,0x43,0x67,0x74,0x75,0x99,0x44,0xb3,0xd0, + 0xed,0x5d,0x3b,0x89,0x81,0x40,0x35,0xfb,0x04,0x76,0xf1,0x1b,0xc0,0x68,0xf6,0x63, + 0xc7,0x75,0x61,0xbc,0x30,0xd0,0x2c,0x8b,0xa6,0xbd,0x67,0xed,0xf8,0xa6,0x8f,0x58, + 0x3d,0x0e,0xa2,0x79,0xeb,0xa6,0x37,0xf1,0x5d,0xd7,0x0b,0x61,0xe4,0x1d,0x1a,0x13, + 0x8d,0xb7,0xd7,0x79,0x0a,0x85,0xe8,0xe7,0xdc,0xf3,0x2f,0x27,0x59,0x6f,0x7f,0x0f, + 0x27,0xd7,0x55,0x8b,0xec,0xd6,0x8b,0xf4,0x69,0xc6,0x59,0x02,0x43,0x1c,0x47,0xc9, + 0xb4,0x37,0x8b,0x63,0x2f,0x19,0x39,0xa9,0xd7,0x0f,0xbc,0x2c,0x03,0x60,0xa5,0xb1, + 0x33,0x42,0xd8,0xd9,0xed,0x3d,0x6f,0x5a,0x1a,0x2a,0x2c,0xb1,0x29,0xe1,0xd8,0xed, + 0x02,0xf8,0x61,0x21,0x60,0xa4,0xd8,0x69,0x6f,0xec,0x27,0x69,0x86,0x8b,0x1a,0xb8, + 0x0b,0x2e,0xd2,0xca,0xa2,0x18,0xc1,0xed,0x2c,0xd4,0x36,0x00,0x0b,0x4c,0xa8,0x00, + 0x14,0xc3,0x4b,0x16,0xae,0x9f,0xc6,0x81,0x73,0xdb,0x1b,0x07,0xde,0x4d,0xdf,0x09, + 0xfc,0xcb,0xb0,0xe5,0x03,0x44,0xd3,0xde,0x08,0x68,0x83,0x97,0xf4,0x2f,0x9d,0xb8, + 0x87,0x3d,0x48,0xf8,0xf4,0x3a,0xd8,0x6b,0x67,0x1f,0xde,0xd4,0xe0,0x8a,0xe8,0x69, + 0xf6,0xf3,0x05,0x27,0x38,0x22,0x9a,0xa4,0x51,0xe0,0xbb,0x1a,0x97,0x41,0x7c,0x02, + 0x60,0x47,0x29,0xd0,0x97,0x28,0xec,0xa5,0x99,0x3f,0xba,0xba,0xed,0xd3,0x38,0xfb, + 0x9f,0x61,0x25,0x5c,0xef,0x06,0xfa,0x93,0xc3,0xd3,0xd2,0xeb,0xcb,0x05,0x0e,0xad, + 0x17,0x46,0xa1,0x77,0xf7,0xc8,0x9e,0x20,0x29,0x5b,0x4c,0x61,0x6a,0x44,0xa8,0x70, + 0x72,0xfc,0x4e,0x73,0xfd,0x6b,0x15,0xec,0x30,0xca,0x3a,0xe0,0xe6,0x13,0x98,0x1b, + 0x41,0xd7,0x83,0x06,0xe7,0x89,0x13,0xe7,0x4b,0x2c,0x16,0x98,0x97,0x26,0x7f,0xe9, + 0x05,0x81,0x1f,0xa7,0x7e,0x0a,0xbd,0x0c,0x1d,0xf7,0xd2,0x93,0x70,0x0d,0xbc,0x71, + 0xd6,0x73,0x66,0x59,0xd4,0xcf,0x07,0xd7,0x57,0x3a,0xef,0x34,0xac,0xb9,0x84,0x1f, + 0xee,0x99,0x67,0x08,0x3d,0x06,0x53,0xe2,0xb8,0xfe,0x2c,0xed,0x3d,0x7b,0xd6,0x08, + 0x50,0xd8,0xa0,0x66,0x6d,0x1a,0x72,0x30,0x36,0x10,0xf9,0x59,0xbc,0x50,0x6a,0x3d, + 0x19,0xef,0x78,0x07,0xee,0x8e,0xa8,0xf1,0xe4,0xc0,0xd9,0x1b,0xb5,0xdb,0x6b,0x91, + 0x9c,0xa5,0x2d,0xee,0x38,0xdd,0x71,0x67,0x57,0xb6,0xe8,0xb5,0x87,0xbb,0x7b,0x23, + 0xd8,0xe2,0x53,0xc7,0x0f,0x01,0x18,0x37,0x62,0x19,0xf6,0x77,0x11,0x45,0xe4,0xfe, + 0xd6,0x08,0x32,0x39,0xc2,0xec,0x23,0x7e,0xda,0x88,0x1c,0x8b,0x95,0x28,0xb3,0x04, + 0x57,0xca,0x80,0xa2,0x85,0x55,0x9b,0xee,0x13,0xad,0x61,0xaa,0xc4,0xd5,0xf8,0x87, + 0xdc,0x26,0x39,0x22,0xee,0xd2,0x30,0x32,0x67,0x98,0x96,0x91,0x1e,0x31,0x7c,0x77, + 0x39,0xf4,0x65,0x5f,0xbb,0xb5,0x35,0xeb,0x14,0x73,0x6e,0xe8,0x44,0x1b,0xce,0xe0, + 0x55,0xc8,0xd8,0xdb,0x91,0x53,0x6c,0xab,0xdd,0x10,0xda,0xd4,0x90,0x94,0x48,0x9e, + 0x1f,0x4e,0x80,0x76,0x65,0x2a,0x52,0xed,0xd8,0x7b,0x55,0xb4,0x6a,0x17,0x68,0x05, + 0x94,0x48,0xab,0x0f,0x11,0xe9,0xd3,0x68,0x96,0xa4,0xd0,0x43,0x1c,0xf9,0xb8,0xa1, + 0xcb,0xa3,0xb3,0x61,0x80,0x4b,0xd6,0xa4,0x46,0x20,0x97,0xc1,0x19,0x5a,0x1c,0x2f, + 0x2a,0x60,0xd8,0x21,0x30,0x8c,0x41,0x44,0x19,0x7a,0x41,0x0e,0xee,0x61,0x10,0x8d, + 0xae,0xd4,0x29,0x75,0x1b,0xa7,0x54,0x6e,0x6b,0x57,0x34,0x65,0x4f,0x60,0xfc,0xea, + 0x0e,0xef,0x50,0xe5,0x25,0xc4,0x91,0x28,0x1f,0x8d,0xc2,0x0f,0xe3,0x59,0x76,0x91, + 0xdd,0xc6,0xde,0x00,0xf7,0xf6,0x47,0x4b,0x79,0x11,0x3b,0x69,0x3a,0x07,0x80,0x95, + 0x5e,0x86,0xb3,0xe9,0xd0,0x4b,0x4a,0xaf,0x3c,0x40,0xf7,0xe0,0xa3,0x95,0x7a,0x81, + 0x37,0x22,0xc6,0xcc,0x58,0x8f,0x7c,0x2a,0x5f,0x80,0x03,0x24,0x8b,0x6d,0x31,0x9d, + 0xa6,0xf5,0xdb,0xaf,0x8c,0x96,0x81,0x5a,0x05,0x3e,0xf1,0x60,0x13,0xd9,0xf5,0x92, + 0x4d,0x21,0x78,0x72,0x75,0x5f,0x1c,0x20,0xbb,0x9a,0x65,0xc4,0xae,0x89,0x56,0x6a, + 0xda,0xf6,0xb7,0xda,0xd1,0x00,0xbb,0xed,0x69,0x29,0x80,0x23,0xd5,0xfc,0x77,0x67, + 0xda,0xe7,0x28,0x9a,0xb6,0xa2,0xb0,0x35,0x8e,0x46,0xb3,0x54,0x33,0x52,0xdf,0xf5, + 0xe6,0xce,0x2d,0xb0,0xca,0x51,0x12,0x05,0x01,0xc8,0x7d,0xda,0xc8,0x89,0x33,0xff, + 0xda,0xd3,0xd2,0x89,0xe7,0x65,0xa6,0xf6,0xed,0x36,0x83,0xb0,0x47,0x35,0x04,0x0c, + 0xf8,0xc7,0x42,0x0c,0xa1,0xca,0x5c,0x54,0x54,0x69,0x6b,0xf8,0x0f,0xf2,0x0c,0xa6, + 0x3a,0x53,0xff,0xc6,0x80,0x4e,0x52,0x10,0x1b,0xac,0xa2,0x86,0xd6,0xdd,0xfb,0x27, + 0x8b,0x18,0x62,0xec,0x24,0xc0,0x78,0x4c,0xb1,0x6e,0xb6,0xeb,0x27,0xd9,0xad,0xe8, + 0x94,0x7f,0x34,0x75,0x8a,0xd2,0x1b,0xe2,0x61,0x12,0xcd,0xeb,0x7b,0x9b,0xf8,0x23, + 0x7e,0x3a,0x02,0x3c,0x15,0xbb,0xb1,0xcc,0x40,0xd2,0xf9,0xbd,0x6c,0x10,0xe5,0x12, + 0x7f,0x7c,0xdb,0x12,0x42,0x73,0x8f,0x78,0x48,0x6b,0xe8,0x65,0x73,0x0f,0x98,0x46, + 0x8d,0x49,0x3e,0x43,0xce,0x4c,0x2d,0x6b,0x50,0x32,0xd4,0x86,0xcb,0xf7,0x40,0xe3, + 0xb6,0x56,0xaa,0xfa,0x02,0xe7,0x51,0x72,0x86,0x95,0x4d,0xa6,0x4e,0xd0,0xbf,0x77, + 0x17,0xe0,0x2e,0xbf,0x0c,0x16,0x39,0x9b,0x4d,0xbc,0xc0,0xc1,0x45,0xed,0xf3,0xa4, + 0x77,0x91,0x88,0x4e,0xb8,0xb7,0x2e,0x09,0x28,0x0a,0x8b,0x85,0x8a,0x1a,0x01,0xbf, + 0xa8,0x0e,0x04,0x23,0x0a,0x66,0x99,0xd7,0x8f,0x50,0x30,0xc9,0x6e,0x81,0x8c,0x29, + 0x3b,0x40,0x34,0x44,0xcf,0x92,0x05,0x34,0x50,0x1d,0x68,0x76,0xd6,0xd0,0xa4,0x1f, + 0x02,0xd3,0x29,0xd3,0xc5,0x95,0x88,0x4e,0x9c,0x92,0x50,0x85,0x1b,0xb2,0x3b,0x7b, + 0x69,0x5f,0x74,0xd3,0xf2,0xae,0x61,0x71,0x52,0x75,0x26,0xb3,0xde,0xd0,0x03,0x21, + 0xcb,0x5b,0xc8,0x95,0xd3,0xf5,0x7e,0x7d,0x10,0x82,0x5a,0xf4,0x89,0xaf,0xe3,0x83, + 0x98,0xde,0x41,0x01,0x27,0x7a,0x2e,0xf1,0x5a,0xd0,0x04,0xca,0x43,0xdb,0x03,0x00, + 0x54,0x46,0xa6,0xc2,0xb3,0x07,0x0c,0x77,0x74,0xe5,0xb9,0x5b,0xb3,0x3a,0xcd,0x65, + 0x99,0xac,0xa9,0xac,0x1c,0x3f,0x0d,0xad,0xdb,0x41,0x92,0x26,0x68,0xf7,0x30,0x0b, + 0x73,0xb4,0xf2,0x43,0x84,0x55,0x6b,0x5d,0xf4,0x55,0x84,0xbb,0xfd,0x9c,0x69,0xc0, + 0x22,0x2c,0xa3,0x5d,0x4d,0x42,0x2c,0x90,0x28,0x15,0xdd,0x3b,0xfb,0x75,0xa1,0xa6, + 0xc6,0x7d,0xfa,0x8d,0x13,0xef,0x57,0x08,0x08,0x91,0x46,0x75,0x9a,0x20,0x97,0x8c, + 0x16,0x6b,0x48,0x47,0xd5,0x7a,0x3d,0x00,0x8f,0x33,0x0c,0x3c,0x77,0x21,0x11,0xd7, + 0xde,0x93,0x23,0x72,0xbd,0xb1,0x33,0x0b,0xb2,0x72,0x37,0xd3,0x45,0x8d,0x3b,0xc9, + 0x39,0xee,0x0b,0xfa,0x8e,0xa2,0x0c,0x74,0x5c,0x85,0x7c,0x6d,0x5f,0xaf,0x92,0x01, + 0x91,0x1c,0x1e,0x34,0xcb,0x80,0xaa,0x80,0x89,0x10,0x05,0x49,0x14,0x44,0x64,0xd0, + 0xc4,0x69,0x55,0x7b,0x9d,0x7c,0x00,0x76,0x74,0xa5,0x82,0x64,0x19,0x75,0x8d,0xae, + 0x4c,0xad,0x53,0x21,0xae,0x25,0x98,0x45,0x08,0x32,0x6e,0x11,0x74,0xe2,0x75,0x9a, + 0x84,0x62,0xf7,0xb4,0x89,0x25,0x90,0x84,0x39,0xd7,0xde,0xd0,0x49,0x8a,0x4d,0x3f, + 0xf6,0x6f,0x3c,0x97,0x37,0x59,0xbb,0x9f,0x10,0x58,0x60,0xe3,0x33,0x9f,0x2f,0xc4, + 0xff,0x6e,0xfb,0x1e,0xd5,0x02,0x77,0x6b,0xb3,0xac,0xa8,0xe0,0x24,0xae,0x15,0x21, + 0x25,0x30,0xb4,0x60,0x64,0xd0,0xcf,0x2d,0xcd,0x0b,0xaf,0x8d,0xd4,0x19,0x83,0xaa, + 0x99,0x78,0x4e,0x8b,0x68,0x8f,0x10,0x34,0x4c,0xb3,0x2f,0x97,0x94,0xe4,0xb2,0x65, + 0xba,0x50,0x97,0x56,0x40,0x4c,0xcd,0x4e,0x27,0x15,0x96,0x53,0x7c,0x03,0xa2,0x5f, + 0x25,0xf3,0xca,0x47,0x3b,0x8d,0x05,0x33,0xc2,0x97,0x01,0xe8,0xfb,0x0f,0x11,0x82, + 0xab,0x0a,0x59,0x55,0x0c,0x65,0x3c,0xc5,0x66,0xa1,0xa3,0x6c,0x2d,0x25,0xef,0xe0, + 0xbe,0x56,0x9a,0xe7,0x13,0xfb,0x41,0xa0,0xbe,0x6f,0xd7,0xd8,0xd9,0xd3,0xf6,0x5a, + 0x88,0xbf,0xa6,0xf2,0xf3,0x00,0xdd,0x79,0xd7,0x9b,0x8a,0x01,0x7e,0xdd,0xbd,0x42, + 0x2d,0x8e,0x41,0x22,0xb4,0xf8,0x11,0xa0,0x3b,0xfa,0xaa,0xfb,0x86,0x5a,0x9d,0x3b, + 0x7e,0xb6,0x4e,0xab,0x24,0xfb,0xac,0x6e,0x56,0x8a,0x47,0x99,0x1f,0x78,0x85,0xf2, + 0x73,0x99,0xf8,0x6e,0x1f,0xff,0xd3,0x02,0x54,0x80,0x37,0xa0,0x19,0x43,0xa5,0xd9, + 0x34,0x4c,0x41,0x5c,0x88,0x3d,0x27,0x33,0x50,0x8d,0x6b,0x8d,0x61,0x30,0x16,0xc8, + 0x4b,0xa0,0xec,0x19,0x1d,0x54,0xf3,0xac,0xce,0x38,0x81,0xcd,0x92,0x8b,0x56,0xdc, + 0xee,0x32,0xf2,0xbc,0x0a,0x71,0x69,0x87,0x76,0xf3,0x26,0x1a,0x45,0x9d,0xa5,0x72, + 0x53,0x13,0x7d,0xfd,0xdd,0xc8,0x42,0xc3,0x28,0xe1,0xf9,0x41,0x93,0x85,0x87,0xcb, + 0xa5,0x0b,0x6a,0xde,0xf5,0x46,0x51,0xe2,0x10,0x7d,0xab,0xd9,0x01,0x96,0x08,0x67, + 0x23,0x27,0xbc,0x76,0xd2,0x45,0x5d,0x86,0xda,0x43,0x92,0x5f,0x9a,0x32,0x74,0x16, + 0x7a,0xd9,0xfa,0x66,0x1a,0xa5,0xcd,0xb5,0x54,0x4e,0xd2,0x42,0x4a,0x6c,0xbf,0x4a, + 0x43,0x15,0xbe,0xbe,0xd2,0xa6,0x53,0xe1,0xf4,0x04,0x1b,0x66,0x5a,0x48,0xf0,0x79, + 0x1e,0xbd,0xc0,0xc9,0x4d,0x54,0xe5,0x36,0xdb,0x5c,0x00,0x08,0x4d,0x2a,0x85,0xf4, + 0xaa,0x5e,0xb8,0x9e,0xa9,0xa6,0xc1,0xc8,0x23,0x9a,0x4e,0x01,0x9f,0x82,0x45,0x7d, + 0x35,0x40,0x4f,0x7a,0x45,0x4c,0x5c,0x4b,0x61,0x61,0x60,0xf2,0xa1,0x17,0x68,0xc6, + 0xdb,0x77,0xe7,0x1a,0xfc,0xc2,0xf6,0x01,0xf0,0x66,0x4f,0xcb,0x26,0x1e,0x69,0x50, + 0xa7,0x6f,0x4f,0x58,0x37,0xd2,0x60,0x6e,0x20,0x99,0xa5,0x5a,0x9c,0x44,0x97,0x89, + 0x33,0x9d,0x02,0x12,0x8c,0x00,0x7a,0xda,0x30,0x98,0x25,0x86,0x69,0x41,0x75,0x57, + 0x9b,0x4f,0xbc,0x90,0xaa,0x5e,0x79,0xb7,0xc3,0x08,0xb8,0x98,0xe6,0xa7,0xda,0x2c, + 0xa6,0x96,0x58,0xe7,0x4a,0x35,0x62,0x8b,0xb2,0xa7,0x54,0x8b,0xc6,0x63,0xf8,0xe2, + 0x61,0xbd,0x08,0x9b,0xbb,0xf2,0xbc,0x98,0x9a,0x20,0xa5,0x0b,0x4a,0x8e,0x7d,0x2f, + 0x00,0xd8,0xfb,0xa9,0x0f,0xb2,0x8d,0xad,0x9d,0x88,0x21,0x03,0x5d,0xe0,0x4d,0xa3, + 0x21,0x40,0x34,0xd0,0x96,0x40,0x69,0x0a,0x6e,0x35,0x58,0x77,0x2f,0xc1,0x06,0xb0, + 0xb1,0xb3,0xb3,0x57,0xcf,0x45,0x03,0xc0,0x94,0x53,0xac,0x94,0x4d,0x9c,0x4c,0xbb, + 0x9c,0x39,0xb0,0x4d,0x32,0xcf,0x73,0x5b,0xa2,0x61,0x50,0x15,0x01,0x24,0xc0,0x22, + 0x33,0xcf,0x71,0x6d,0xd4,0x03,0x6d,0x84,0x0f,0xf5,0xf5,0x60,0x66,0xb5,0x94,0xa1, + 0xaf,0xb4,0xdd,0xf4,0x5a,0xc2,0xc4,0xc9,0xf6,0x84,0xbc,0x7b,0xe4,0x9e,0x2d,0xb4, + 0x10,0xae,0xb5,0x2f,0xf6,0xe5,0x16,0x2e,0x2c,0x83,0x5f,0xd9,0x20,0x5b,0xa3,0x68, + 0xda,0x6e,0x65,0xc0,0x4f,0xf0,0x11,0xc0,0x03,0xcc,0x18,0xad,0x66,0x62,0xbb,0xef, + 0xee,0x5d,0x4f,0x0a,0xb4,0x2e,0x59,0xcd,0xda,0xc8,0x27,0x35,0x36,0x9d,0xc1,0x0a, + 0xc4,0x0d,0x46,0xab,0xfd,0x3a,0xc7,0x56,0xca,0x23,0x35,0xe5,0x7d,0x24,0x3b,0xab, + 0xf1,0xdd,0x6e,0xd3,0xc2,0xd0,0x12,0x16,0x8d,0x34,0x5a,0x88,0xa4,0xb6,0x12,0xc1, + 0x6e,0xae,0x4a,0x77,0x28,0x9c,0xed,0xef,0x4a,0x5d,0x2a,0x57,0x89,0x08,0xae,0xf4, + 0x84,0x9c,0xe6,0x27,0xa3,0x05,0x5f,0xcc,0x5c,0xe4,0xdb,0x6d,0xd4,0xff,0xae,0xca, + 0x6c,0x4c,0xd8,0x45,0xd6,0x36,0x89,0xad,0xd0,0x4a,0x0a,0x3d,0x56,0xd5,0xd7,0xba, + 0xcd,0x9a,0x64,0xbf,0xb0,0x73,0x3e,0x6b,0x5f,0xcf,0xe5,0xc4,0x59,0xf6,0x93,0x2d, + 0xa1,0x18,0x27,0x76,0x70,0x15,0x22,0x52,0xc3,0x95,0x93,0xdd,0x6b,0x37,0x1f,0x49, + 0xdc,0x27,0x7e,0x2e,0x51,0xe2,0x14,0x42,0x2b,0xde,0xe4,0xd2,0x16,0x63,0xa1,0x18, + 0x56,0xb3,0xac,0x1a,0xfb,0xa1,0xe0,0x43,0x5d,0xd5,0x28,0xd0,0x2d,0xb4,0xc2,0x9d, + 0x55,0x7b,0x1c,0x56,0xbb,0xc9,0xfc,0x53,0xd5,0x8a,0x9d,0xd0,0x9f,0x32,0x8b,0x4c, + 0xb4,0x4e,0xaa,0x61,0x13,0x20,0x03,0xfb,0xe1,0x18,0xcf,0x19,0xbd,0x7e,0x93,0x2e, + 0x75,0xf7,0xe8,0x9f,0x81,0x64,0x8e,0x81,0xae,0x02,0x85,0x4d,0x16,0x59,0xb4,0x28, + 0xf0,0x28,0x89,0x32,0x40,0x22,0x63,0x67,0xbf,0xed,0x7a,0x97,0xe6,0x1d,0xcc,0x03, + 0x24,0x27,0x3a,0x47,0x5a,0x54,0xc5,0xa9,0x92,0xa9,0xa5,0xd8,0x30,0x88,0xa5,0x24, + 0xf1,0xc2,0xb3,0xa2,0xe0,0x43,0x4b,0x57,0xd7,0x35,0x31,0xb7,0x5f,0xda,0x7a,0x6b, + 0x98,0x82,0xba,0x15,0xbd,0xb1,0xbd,0x0e,0x03,0xa5,0xbe,0x57,0x33,0xc8,0xab,0xeb, + 0x46,0x19,0xa9,0xc1,0xfa,0x03,0x25,0x85,0x18,0xa3,0x6c,0x0c,0x05,0x53,0x48,0xf9, + 0x5a,0xff,0xf0,0x03,0xbe,0x7b,0x0b,0x15,0x3f,0x1f,0xfb,0x53,0x3c,0xfc,0x05,0x8e, + 0x81,0xcc,0x35,0xca,0xbc,0xba,0xe2,0x5c,0xa3,0x92,0x6b,0x49,0x88,0x07,0x15,0x2b, + 0x1a,0xc9,0x79,0x4d,0xd6,0xe5,0xc3,0x6d,0x71,0x98,0x7c,0xb8,0x2d,0x0e,0xbf,0xf1, + 0xc0,0x50,0x1c,0x85,0x7b,0xc9,0x11,0x50,0x8a,0xc3,0xf4,0xfa,0x92,0x4d,0xb5,0x03, + 0xbd,0xbb,0xaf,0x6b,0xbc,0xd0,0xfc,0x8c,0xc7,0xd7,0xdf,0x45,0x37,0x03,0x9d,0xcc, + 0x93,0xbb,0xf0,0x7f,0x5d,0x43,0x51,0x77,0xa0,0xe3,0xf4,0x74,0x2d,0xcd,0x92,0xe8, + 0x0a,0xcf,0xcb,0x73,0xb4,0x96,0xef,0x5a,0xb2,0xc5,0xfc,0x05,0x2e,0xdf,0xc8,0x89, + 0x07,0x3a,0x4d,0x4e,0xc7,0xae,0xa1,0xf3,0x91,0x9f,0x8c,0x80,0x8b,0x8e,0xa0,0x8f, + 0x0e,0x94,0x1d,0xdd,0xf2,0xdf,0x04,0x6a,0xda,0x5d,0xd9,0x59,0xbd,0x79,0x31,0x80, + 0x6d,0xd1,0x4a,0xec,0x64,0x13,0xcd,0x1d,0xe8,0x6f,0x9e,0xda,0x4f,0x35,0xf8,0xd7, + 0xd9,0xd7,0xf6,0xb5,0xb6,0xf8,0xe7,0xc0,0xde,0x7f,0xd3,0xd9,0xb7,0x77,0x4a,0x1f, + 0x3a,0xe2,0xc3,0xae,0xfd,0x4c,0x83,0x7f,0x9d,0x0e,0x1e,0x11,0xe6,0x55,0x3a,0xbb, + 0x76,0xf7,0x4d,0xe7,0x99,0xdd,0xa9,0x7c,0xeb,0x88,0x6f,0xdc,0x31,0x00,0xf7,0xfa, + 0x92,0x1e,0x5c,0xff,0x5a,0x1b,0x01,0x52,0xa6,0x03,0x9d,0xce,0xd9,0xe4,0xec,0x26, + 0x1d,0xcd,0x87,0x61,0x4d,0x5a,0xe8,0x56,0xa0,0xe7,0x27,0xfe,0xb0,0x18,0x1d,0x51, + 0x02,0x6b,0x72,0x91,0x74,0x36,0xd4,0x8f,0x60,0xbb,0x00,0x94,0x32,0x58,0xd7,0x8d, + 0x09,0x61,0x56,0xff,0x70,0x1b,0x8a,0x70,0x6f,0xf2,0x81,0x2c,0x9e,0xa2,0x3b,0x3a, + 0x91,0xd2,0x10,0xf3,0x74,0xd1,0x0e,0xbd,0xd1,0x8f,0x60,0x70,0x50,0x4c,0x2e,0x3c, + 0x2e,0xf5,0x21,0x1e,0x47,0x1d,0x3d,0x7a,0x74,0xf8,0xb8,0xd5,0xd2,0x06,0xca,0xff, + 0xb4,0xd7,0xef,0xfe,0xf0,0xea,0x6d,0xf9,0x55,0xab,0x85,0x8e,0x08,0x38,0x94,0x28, + 0xa4,0x86,0xaf,0x5b,0x41,0x04,0xe8,0xa5,0xe7,0xf3,0xc4,0x2e,0xab,0x93,0x47,0x79, + 0x25,0x9f,0x7b,0xf7,0xe8,0xc4,0x05,0xca,0xa1,0x51,0x3d,0x18,0x46,0x57,0x2e,0x96, + 0x46,0x38,0x39,0xd0,0x6b,0x14,0x64,0xd9,0x31,0x85,0x44,0x69,0xa0,0x18,0xfa,0xd1, + 0x0b,0xa4,0xde,0x20,0x8e,0x81,0x84,0x18,0x46,0xae,0xb7,0x99,0x6a,0x0e,0x75,0x23, + 0x8f,0x29,0x34,0x03,0xa5,0xc0,0x14,0x00,0xae,0xa1,0x68,0xc6,0xb2,0x60,0x94,0x68, + 0x89,0x37,0x85,0x1d,0xa8,0x9d,0xbe,0x7e,0xc5,0x15,0x4c,0xfb,0x70,0x3b,0x16,0x43, + 0x42,0x7a,0x49,0xb3,0xa4,0xb1,0xb6,0xf0,0xa7,0x98,0x46,0x79,0x7e,0x63,0x80,0x2b, + 0x9d,0xd6,0x60,0x8b,0xb2,0x74,0x3c,0x87,0x39,0xbf,0x17,0xbd,0x1f,0x6e,0xd3,0x77, + 0x59,0x19,0xaa,0x93,0xa5,0x52,0xa3,0x13,0x12,0x5d,0x8e,0x51,0x57,0x3a,0xc3,0xea, + 0x74,0x16,0x38,0x8a,0x40,0xc1,0xf4,0x32,0x28,0x06,0x4a,0x02,0xea,0xa7,0xad,0xa2, + 0x38,0x7e,0x27,0xc1,0xf6,0x28,0x47,0x03,0x6a,0x9c,0xcd,0x72,0x39,0x32,0x64,0xb0, + 0x3e,0xdc,0x13,0x60,0xd3,0xd4,0xcf,0xd4,0x7e,0xf0,0xe3,0xd1,0x19,0x90,0x35,0x0d, + 0x57,0x83,0x2b,0x36,0xcd,0x51,0x32,0x09,0xb5,0x2e,0xbc,0xd3,0xd5,0x9e,0x0f,0xb7, + 0x11,0x44,0x0a,0x56,0x02,0xb6,0x31,0xae,0x34,0xe2,0xd7,0xd9,0x8b,0xf3,0x1f,0xde, + 0x6b,0x3f,0xbe,0xfa,0xb7,0x93,0x0f,0xcf,0xef,0x45,0xb3,0xb9,0xff,0x19,0xb1,0x68, + 0x25,0x9e,0x21,0x2d,0xe5,0xf1,0x8d,0x42,0xa7,0x45,0xbf,0x8e,0x5e,0xb1,0x06,0xf1, + 0xa3,0xff,0xd2,0x07,0xa1,0x1d,0xc5,0x02,0xc0,0x89,0x28,0x9e,0xc5,0xc7,0xda,0xab, + 0x0c,0x2a,0x46,0x29,0xb0,0x48,0x7f,0xac,0xdd,0x46,0xb3,0x44,0x8b,0x27,0x88,0x1b, + 0x69,0x00,0x4a,0x43,0x6a,0xf3,0x94,0x1c,0x6d,0x92,0x78,0xe3,0x81,0xfe,0x44,0x07, + 0xbc,0x19,0x05,0xfe,0xe8,0x0a,0xd6,0x2b,0x8a,0xdf,0xcd,0x32,0xc3,0xec,0x27,0x5e, + 0x36,0x4b,0x42,0x6d,0xec,0x04,0x29,0x91,0x3d,0xc2,0xdf,0xba,0x0e,0xa5,0x1f,0xbd, + 0x8b,0x3d,0x04,0x30,0xf7,0x32,0x4c,0xa2,0x79,0xea,0x25,0x87,0xdb,0xce,0x11,0x68, + 0x29,0xac,0xa2,0x70,0xf7,0xa8,0x0b,0x79,0x69,0x6a,0x17,0x9b,0x5a,0x99,0x1d,0x09, + 0x95,0x00,0x6f,0x9f,0x66,0x98,0x66,0x71,0x27,0x87,0x46,0x14,0xe2,0x3a,0xf8,0xca, + 0xb7,0x6e,0xf5,0xc5,0x4e,0xf5,0xc5,0xae,0x78,0xc1,0x5d,0x3d,0x52,0x88,0xce,0xfc, + 0x73,0x47,0x57,0xe8,0x50,0x7d,0x13,0xf3,0x36,0x3e,0x83,0xf1,0x00,0xe9,0xdb,0x98, + 0x02,0xff,0x8b,0xb2,0x3e,0x81,0xb8,0xd8,0xcf,0xbf,0x7b,0x47,0x8b,0xa1,0xc0,0xc2, + 0x8f,0x7d,0x90,0x17,0xe2,0x5b,0xfd,0xe8,0x94,0xc9,0x60,0xb1,0xcb,0x11,0x7c,0x04, + 0x39,0x5a,0x5e,0xd0,0x53,0x61,0x5b,0x5c,0x81,0x74,0xa0,0xf9,0xb0,0xb4,0x40,0x0d, + 0x13,0xcf,0x19,0x4d,0x68,0xfd,0xdf,0xfc,0xcb,0xf9,0xb9,0x06,0x40,0x07,0xfe,0x9c, + 0x16,0x3b,0x7c,0xc9,0x36,0x3e,0x7a,0x2b,0x5a,0x42,0xfa,0xac,0x19,0xa8,0xf6,0x99, + 0xf5,0x1d,0xac,0xd4,0x84,0x05,0xcd,0x97,0x5f,0xd8,0x03,0xf5,0xa2,0x64,0x65,0xb7, + 0xf3,0x2e,0x72,0x9d,0xcc,0x69,0x5d,0xa1,0x03,0xd9,0xd8,0x07,0xbd,0xdd,0x77,0xf3, + 0x09,0xf3,0x0f,0xda,0xfa,0x4e,0xec,0x67,0x20,0x73,0x7c,0x86,0x5a,0xa0,0xd8,0x4a, + 0x7a,0x90,0xa0,0x6e,0x2a,0xde,0x48,0x08,0x93,0xb6,0x52,0xee,0xb3,0x46,0x04,0x60, + 0xfe,0x23,0x49,0x08,0xf8,0x23,0xe2,0xf4,0x34,0x42,0x5a,0x18,0xcd,0x43,0xd8,0xe3, + 0x28,0xb7,0xdb,0x71,0x42,0x7f,0x9f,0xb3,0x8d,0xdf,0x30,0x15,0xc4,0x8f,0x00,0x8d, + 0xcf,0x00,0xb0,0xc6,0xa6,0x18,0xe7,0xa6,0x09,0xc4,0x03,0x5e,0x54,0x29,0x87,0xa4, + 0x00,0x65,0xb2,0xd4,0x08,0xeb,0x87,0x92,0xc8,0x12,0xe0,0x1a,0xa8,0x24,0x82,0xa5, + 0x79,0x95,0xf0,0x10,0x5c,0x3f,0x7a,0xed,0x39,0xd7,0x9e,0x36,0x0c,0x9c,0xf0,0x8a, + 0x18,0x00,0xda,0x29,0x70,0x77,0x0a,0xec,0xb1,0xb5,0xae,0xbd,0xbb,0x01,0x18,0x16, + 0xa6,0x71,0xff,0x0f,0xdf,0x7f,0x96,0xef,0x53,0x80,0x42,0x70,0x6b,0xaf,0x3d,0xad, + 0xb7,0x88,0x9e,0x88,0x3f,0xab,0xe7,0x55,0x46,0x06,0x92,0x07,0xd0,0x5d,0x30,0xf0, + 0xc2,0x4b,0x94,0x93,0x76,0x3a,0xab,0xe6,0x22,0x31,0x06,0xab,0xb5,0x78,0x76,0x67, + 0xa0,0xa7,0xc0,0x84,0x98,0xe8,0x81,0xf4,0x3f,0x21,0x43,0x0a,0x50,0x1e,0xde,0x00, + 0x20,0xfa,0xcf,0x52,0x7c,0x9f,0x3a,0x97,0x5e,0x5a,0x9f,0x8d,0xfa,0xd8,0xc0,0x44, + 0x72,0x44,0x98,0x7f,0xfe,0x43,0x64,0x74,0x61,0xf1,0xdf,0xc2,0xf8,0x7b,0xda,0x07, + 0x90,0x3f,0x23,0x15,0x07,0x1a,0xa9,0x4a,0xb7,0x4e,0xbd,0xd7,0x21,0x31,0xdd,0x82, + 0xc4,0x88,0x7e,0xbe,0x1a,0x8d,0xa1,0x55,0xe2,0x95,0x05,0x01,0x17,0xb4,0x2a,0xa2, + 0x17,0x11,0xc0,0x2e,0xd1,0x88,0xf3,0xc0,0x3e,0xc0,0x03,0x24,0x0d,0x05,0xec,0x08, + 0xf6,0x4f,0x86,0x32,0x58,0x6a,0x6b,0xef,0x01,0x0a,0x04,0x62,0xd8,0x2c,0xf0,0x96, + 0x89,0x10,0x68,0x5e,0x50,0x94,0x80,0x0e,0x5b,0x6a,0x0d,0x32,0xf3,0xc1,0xbb,0x44, + 0x06,0xc7,0x6d,0xd4,0xf1,0x84,0x4f,0xee,0xe5,0x22,0x27,0x31,0xc1,0x7f,0xe2,0x84, + 0x97,0xc8,0x64,0x3e,0x13,0x2c,0xce,0xbc,0xc0,0x40,0x6a,0x68,0x92,0x64,0x47,0xe5, + 0xd7,0xc0,0x97,0x04,0xf4,0xce,0x59,0xce,0xba,0xd7,0x40,0xe6,0xf3,0x9f,0x80,0x67, + 0xce,0x01,0x28,0x86,0xfb,0xdd,0xd4,0x5c,0x8d,0xd1,0xec,0x02,0x52,0xe0,0x74,0x06, + 0xa4,0x1c,0xe4,0xaa,0x81,0xde,0x7a,0x46,0xa8,0x0d,0x48,0xdd,0xce,0xe9,0x56,0x61, + 0x1d,0xe8,0x74,0xdb,0x25,0xa2,0x59,0xdf,0xb8,0x6f,0xd8,0x8b,0x56,0x0b,0xbc,0x4b, + 0x00,0x33,0x8f,0x07,0x96,0xd7,0x87,0x05,0x1c,0xde,0x02,0xa9,0x27,0x58,0x6e,0x4c, + 0x5d,0x27,0x9d,0xf4,0x35,0x3a,0x02,0x16,0x2b,0x92,0xcc,0x82,0x26,0x5c,0x6f,0x90, + 0x1d,0x00,0x9d,0x41,0x2c,0x94,0xcb,0xcc,0xc6,0x3d,0x3c,0x13,0xd8,0xcc,0x34,0x34, + 0x3f,0x79,0xee,0xb1,0x76,0x3a,0x89,0x40,0x64,0x80,0xdd,0x71,0xf4,0x27,0xe4,0xdb, + 0x42,0x26,0xcb,0xeb,0xc0,0x26,0x38,0xb2,0x34,0x54,0xcd,0x01,0x0b,0xc8,0x23,0xcc, + 0x42,0x3c,0x09,0xf1,0x95,0xd7,0xca,0x66,0x64,0x15,0x24,0xcc,0x69,0x94,0x2c,0xc8, + 0x06,0x71,0x12,0xc7,0x55,0xd9,0x02,0x64,0xe6,0x6b,0x27,0x1c,0xc1,0xe0,0x3c,0xd7, + 0xcf,0x22,0x92,0x1d,0xec,0x25,0xfb,0xb6,0xc2,0x97,0x96,0x0b,0x85,0xcc,0x0f,0xca, + 0x7b,0xba,0x03,0x98,0xf4,0x1d,0xe8,0x9a,0x35,0x51,0xb0,0x81,0x1a,0x94,0x79,0x4f, + 0x8e,0x5c,0x88,0x97,0xad,0x90,0xe8,0x5a,0xb9,0xed,0x9d,0x9c,0x5e,0x20,0x31,0x2a, + 0xf7,0x50,0xd5,0x67,0xca,0x94,0x63,0xe7,0xb7,0x51,0x8e,0x9d,0x82,0x72,0x70,0x8f, + 0x0a,0xe1,0x68,0xc4,0xf3,0x77,0x43,0x16,0x11,0x04,0xe3,0x8f,0x40,0xd3,0x07,0x8d, + 0xe0,0x21,0x14,0x7c,0xfa,0x6b,0x96,0xd9,0x5c,0xaf,0x99,0x87,0xaf,0xc2,0xf0,0xef, + 0xa3,0x39,0xcb,0x36,0x91,0x1c,0x07,0x53,0xa0,0x31,0xe2,0xb8,0x9f,0xc1,0xfe,0x1e, + 0x23,0xf6,0xc4,0xb3,0x21,0x60,0xe3,0x04,0x31,0x95,0xe8,0xf9,0x36,0x88,0x9a,0x57, + 0x40,0x84,0x72,0xb2,0xae,0xbd,0x0a,0x51,0x10,0x27,0xa3,0x11,0xe0,0x3f,0x91,0x37, + 0xb9,0x31,0x00,0xcf,0x0b,0xe5,0x27,0x8b,0x62,0x7f,0x04,0xdd,0x25,0x38,0xd4,0x09, + 0x13,0x3b,0xac,0x04,0x04,0x0e,0x50,0x13,0x94,0x6c,0x28,0x37,0xf7,0x33,0x16,0xa1, + 0x42,0xc9,0xd1,0xd6,0x67,0x82,0xaf,0x4e,0xce,0x4f,0xe4,0xce,0x1c,0x41,0xf5,0x07, + 0x43,0xd2,0x87,0x1f,0x65,0x86,0x58,0x87,0x2a,0xba,0xec,0x3b,0x23,0xd8,0x37,0x69, + 0x8e,0x92,0x4b,0x4d,0xd2,0x0f,0xa1,0x37,0x6f,0x81,0x21,0x78,0x29,0x10,0x00,0x3f, + 0x41,0x73,0x0d,0x4d,0xc0,0xd2,0x3c,0xfb,0xd2,0xc6,0xdd,0xff,0xfc,0xc5,0x5b,0xdc, + 0xeb,0xb6,0xf6,0x03,0x02,0x13,0xf7,0x34,0xc2,0x52,0x43,0x93,0x43,0xba,0x3e,0x80, + 0xde,0xcd,0x43,0x58,0x65,0x5a,0xd0,0x11,0x9e,0x72,0x3c,0x1c,0xd5,0xb0,0x81,0x12, + 0x84,0xf6,0x77,0xd7,0x16,0x1e,0x1f,0x26,0x38,0xbd,0x8b,0x05,0x3e,0x49,0x54,0xda, + 0xdf,0x6d,0x21,0xec,0x81,0x73,0xde,0x28,0x53,0xd0,0x22,0xa1,0x4c,0x61,0xcb,0x4e, + 0x88,0x4b,0x8f,0x98,0x83,0x48,0x39,0x0a,0x66,0x2e,0x03,0x0b,0xd1,0x4d,0xfb,0xe3, + 0x8f,0xe7,0x29,0x0a,0xeb,0x88,0xeb,0xfe,0xc8,0x13,0xf4,0x36,0x85,0xd5,0x42,0x70, + 0x0b,0x11,0x3e,0x60,0xee,0x8a,0x43,0xf1,0xa7,0x85,0xe4,0xff,0x50,0x10,0x93,0x17, + 0xe2,0x6a,0xe8,0x52,0x91,0x0a,0x78,0xc5,0xbb,0x87,0x6e,0xe4,0x1a,0xa8,0x80,0x88, + 0x83,0x52,0xd2,0x08,0x00,0xdc,0x87,0x34,0x39,0x60,0x1f,0x8a,0x66,0x03,0x60,0x4b, + 0x23,0x20,0x42,0x12,0x38,0x8d,0x13,0x46,0x42,0xc7,0x3a,0x4d,0x8d,0xb6,0x49,0x65, + 0x22,0x88,0xb2,0x54,0x3f,0xfa,0x47,0xb0,0x8a,0xee,0x6f,0x67,0x15,0xe5,0x86,0x76, + 0xa1,0xa1,0x0f,0x1e,0x1a,0x15,0x1f,0xc2,0x13,0x76,0x1f,0xca,0x13,0x72,0x06,0x45, + 0x5d,0xb5,0x28,0xac,0x46,0x67,0x46,0xb1,0xab,0x88,0x98,0xf4,0x55,0xdb,0x70,0xa6, + 0x71,0x5f,0x43,0x87,0x93,0x65,0x90,0xe5,0x66,0xd6,0x01,0x2d,0x8b,0x16,0x67,0xce, + 0x35,0xae,0x70,0xe2,0x0d,0x23,0x58,0x92,0x82,0xa2,0x02,0xc2,0x44,0xf2,0xec,0x11, + 0x14,0x91,0x44,0x10,0xe0,0xf3,0xa8,0x38,0xbb,0x8c,0x81,0xaa,0x6b,0xce,0x35,0xa0, + 0x22,0x3a,0x64,0x69,0x78,0x08,0x94,0x58,0x20,0xd6,0x84,0x48,0x88,0x50,0xfc,0x9c, + 0x7b,0xc3,0x99,0x0f,0x50,0x45,0x8a,0xd4,0x47,0xf2,0x8e,0x1f,0x2e,0xe5,0x07,0x7c, + 0x8b,0x9a,0x72,0xea,0x79,0xc8,0x45,0x34,0xc7,0x75,0xcb,0x56,0x86,0x87,0x63,0x42, + 0x19,0x94,0x68,0x9c,0x6e,0x62,0xf6,0xbf,0x4f,0x90,0x40,0xe0,0xab,0xad,0x92,0x01, + 0x08,0xa0,0xe8,0x19,0x26,0x41,0xd3,0x13,0x8b,0xc4,0x10,0x5d,0x82,0x3a,0xab,0x8c, + 0x58,0xd0,0x47,0xd9,0x82,0xa5,0xe0,0x59,0x5c,0x62,0x25,0xa5,0x13,0x22,0xe5,0x14, + 0x04,0x4f,0xc8,0xa0,0xfe,0x43,0x24,0xb9,0x26,0x7d,0x05,0x1b,0x69,0x10,0xf0,0x48, + 0x7f,0x58,0x6d,0x42,0x7b,0x73,0xf2,0xea,0xad,0x76,0xf2,0xfe,0xfd,0xbd,0xe6,0x33, + 0x27,0x8e,0x57,0xdb,0xce,0xd0,0xb1,0x9d,0xc1,0x42,0x4f,0x65,0x1d,0x90,0x68,0x22, + 0x70,0x0d,0x92,0xed,0x4a,0x56,0xa7,0x9a,0xe6,0x57,0xaf,0x84,0x84,0x14,0xa4,0xf7, + 0xba,0xc8,0x57,0x29,0x87,0x0a,0xbd,0x7e,0xc4,0x56,0xa4,0x55,0xe5,0x50,0xe8,0x49, + 0x71,0xe3,0xc2,0x9f,0x7b,0x74,0xce,0x8c,0xa5,0xd1,0x75,0x04,0xc6,0xb7,0x24,0x9e, + 0xdc,0x27,0x20,0xbe,0x55,0x14,0xfa,0x87,0xea,0xf1,0x4b,0x79,0x15,0x59,0x8f,0xa8, + 0x2a,0x1d,0x84,0x2c,0x53,0xf9,0x8b,0x51,0xbc,0x06,0x1a,0x91,0xcd,0xdc,0xe6,0x91, + 0x54,0xf5,0x2f,0xa0,0x16,0x88,0x74,0x1e,0xb4,0xec,0x84,0xb7,0x95,0x61,0x2c,0xed, + 0x21,0x0a,0x2f,0x1f,0xd0,0x45,0x14,0x2e,0xef,0xe2,0x7e,0xfa,0x58,0x5b,0x8b,0xd7, + 0xd1,0x07,0x47,0xa8,0xdc,0xa5,0x53,0x0d,0x3c,0xf3,0xd2,0x60,0xcb,0x2a,0x07,0x1b, + 0xac,0x6d,0xa0,0x93,0x96,0x7e,0xc4,0x84,0x00,0x29,0x1d,0xc6,0xb7,0xdd,0x8a,0xa3, + 0x8e,0x65,0x8b,0xca,0x54,0xf9,0x65,0xe2,0xfd,0x3a,0xf3,0xc2,0xd1,0xad,0xa5,0x0d, + 0x9d,0xd0,0x15,0x11,0x9c,0x67,0x2f,0xc9,0x6c,0x72,0xfa,0x41,0x9b,0xce,0x40,0x02, + 0x04,0x49,0x7a,0x34,0x61,0xa1,0x86,0xb4,0x7b,0xef,0xc6,0x21,0xe7,0x90,0x9c,0xbb, + 0x6b,0xf3,0x04,0x00,0x06,0xda,0x68,0x30,0xf3,0xb4,0xcc,0xb9,0x22,0x59,0x26,0xe7, + 0xe4,0xe3,0x31,0x51,0x71,0x90,0x23,0xb5,0x19,0x88,0xd8,0x81,0xe2,0xb3,0x82,0xec, + 0xdd,0x77,0x02,0x7b,0x25,0x6e,0xdc,0xb7,0x58,0xf9,0x1c,0x34,0xe3,0xcd,0xf7,0x9f, + 0xcd,0xfb,0x97,0x2c,0xb9,0x84,0xea,0x50,0x49,0x2e,0x5a,0xdb,0x6e,0xb7,0x3b,0x42, + 0x4b,0xef,0xec,0xb5,0x85,0x9a,0xde,0xdd,0x6b,0xb7,0xd7,0x44,0x97,0xef,0x24,0xe8, + 0x34,0xe3,0x6a,0xed,0x11,0x0c,0xe7,0x4a,0xff,0xb2,0xfb,0xa7,0xa2,0xf3,0x7a,0xdf, + 0xbf,0x0b,0x44,0x67,0x71,0xe2,0x39,0x78,0x2e,0x0a,0x54,0x78,0x44,0xe4,0xb5,0x22, + 0x09,0x16,0xe6,0x96,0x7c,0x7c,0x29,0x56,0x8f,0x48,0x92,0x3b,0xda,0x3b,0xdc,0x16, + 0x4f,0xf2,0xcd,0x7e,0xed,0xcd,0xd3,0xda,0x9b,0x83,0xda,0x9b,0x67,0xb5,0x37,0x9d, + 0x76,0xfd,0x55,0xa7,0xfe,0xaa,0x5b,0xbc,0x92,0x76,0x9e,0xb5,0x56,0xe6,0x34,0xa2, + 0x59,0x83,0x54,0xe1,0xad,0x33,0xe7,0x51,0xf2,0x15,0xe7,0xdc,0x38,0xd0,0xdf,0x6c, + 0x6d,0xfa,0x8a,0x46,0xa6,0xdf,0x40,0x92,0x24,0x7b,0x5e,0x46,0x4d,0x56,0x21,0xa1, + 0x42,0xdb,0xbb,0xf9,0x24,0x4f,0xfc,0x24,0xf3,0x41,0xbe,0x2f,0xe3,0xe3,0xca,0x49, + 0x3a,0xe3,0x62,0xc3,0xc8,0xfd,0xd2,0x2e,0xdb,0xfc,0x1b,0x2d,0x7e,0xce,0xb8,0xe5, + 0x8e,0x1a,0xad,0x7d,0x2b,0x46,0xba,0x5b,0x58,0x29,0x7f,0xd2,0x5c,0x2f,0x70,0x6e, + 0x81,0x40,0xa6,0xeb,0xf1,0x83,0xe4,0x86,0x2a,0x34,0x8d,0x56,0x90,0x96,0xb5,0x37, + 0x77,0x69,0x4c,0x7b,0x9d,0xae,0x8a,0x24,0x3c,0xaa,0x07,0x00,0x30,0xbb,0x77,0x5c, + 0x6b,0xe1,0x4c,0xe5,0x58,0xae,0xc4,0xbc,0x0f,0xa0,0x24,0xb3,0x9d,0xe1,0xd1,0xe9, + 0xc9,0x73,0xcd,0x20,0xbb,0x61,0xa8,0x71,0x20,0x8a,0x46,0x36,0x89,0xa9,0x9f,0x99, + 0x28,0x93,0x1f,0xfa,0x47,0xa7,0x13,0x27,0x44,0x17,0x37,0x98,0x83,0x7f,0xed,0x67, + 0xb7,0x30,0xa7,0x8c,0xe5,0x36,0x3e,0x9b,0xe3,0xc3,0xfa,0x62,0xbb,0x2a,0xcc,0x30, + 0xbb,0x0c,0xf4,0xf2,0x74,0xc9,0xe6,0x39,0x8c,0x6e,0x8a,0x09,0x8f,0x1c,0xc0,0xe2, + 0xc3,0x19,0x34,0x34,0x3b,0xca,0xd9,0xe1,0x5a,0xf3,0xe8,0xec,0x2b,0x13,0x01,0x0c, + 0x00,0xce,0x8a,0xf6,0x4f,0xed,0xd2,0xa1,0x63,0x62,0x1c,0xfb,0xd9,0x4f,0x9d,0xee, + 0xfe,0x0d,0x88,0xdf,0x23,0xcf,0x47,0x15,0x94,0xec,0x4c,0x5f,0x65,0xd8,0xc4,0xd4, + 0xed,0xe4,0x06,0x3b,0xfb,0x8d,0xe3,0xdf,0xe9,0xaa,0xe3,0x27,0x97,0x6c,0x31,0xec, + 0x97,0x51,0x32,0x47,0x57,0x52,0xe2,0xe4,0xb0,0x1e,0xe3,0xb1,0x3f,0xb2,0xb5,0x77, + 0xc0,0xa4,0x07,0x6c,0xe4,0x0d,0x5b,0x64,0x30,0x33,0x52,0x60,0xd5,0x81,0x34,0xc5, + 0xa5,0x24,0x10,0x48,0xcb,0x5b,0x8a,0x5e,0x02,0x5f,0x63,0xa2,0x34,0xb0,0xb5,0xa7, + 0xa8,0x1e,0x33,0x2a,0x5a,0x08,0x2a,0x21,0xca,0xd4,0xf7,0x77,0xef,0x17,0x20,0xc9, + 0x2e,0xee,0xb8,0x18,0x20,0xa3,0x19,0x53,0xc5,0xc6,0xb9,0x9a,0x00,0x51,0x05,0x9b, + 0x9c,0xfc,0x40,0xd6,0xa9,0x6e,0x9e,0xdd,0x7b,0x88,0xd1,0x51,0x1b,0x40,0x0c,0xd2, + 0xd0,0x1a,0x84,0x28,0x17,0x6d,0x82,0x28,0x72,0xf3,0x81,0x4e,0x92,0x74,0xbd,0x81, + 0x8e,0xb1,0x9a,0xbd,0x7a,0xb8,0x9d,0xfd,0x83,0xf5,0x86,0x6b,0x69,0x1e,0x28,0x6e, + 0xda,0xce,0x46,0x48,0xd2,0x1e,0xd4,0x6b,0x32,0xc5,0xac,0xc7,0x52,0x14,0x99,0x95, + 0xc6,0xd8,0xaa,0x31,0x1a,0x9a,0x31,0xb2,0x6c,0x56,0x6e,0xe1,0x57,0x9c,0xae,0x16, + 0x61,0xbf,0x8f,0x62,0x40,0xdc,0x29,0x6a,0xf6,0x68,0x4b,0xa2,0x76,0x25,0x62,0x2b, + 0x62,0xe8,0x98,0xf1,0x3e,0xb5,0xb5,0xd7,0xe2,0xe8,0x04,0x84,0xd5,0x94,0x32,0x99, + 0x38,0xe4,0xfd,0xec,0xa7,0xb7,0x5a,0x88,0x5e,0x5e,0x43,0x10,0x74,0xd3,0x3e,0xc8, + 0xd1,0x91,0x86,0xbe,0xd0,0x2c,0xcf,0xc6,0x4e,0x92,0xa1,0x7b,0x75,0x71,0xb0,0x18, + 0x01,0xf8,0xe1,0x37,0x9d,0xb1,0xdb,0x6b,0x2b,0x37,0xdd,0x83,0x7b,0xb1,0xf3,0x8d, + 0x73,0x23,0x66,0x31,0xa1,0xc9,0xaf,0xbd,0xe0,0xb0,0xae,0x95,0x45,0x2e,0xed,0x85, + 0xc6,0x35,0x16,0xab,0xba,0xbf,0x6b,0x6b,0xe2,0xc4,0x5b,0x83,0xe7,0x07,0xa0,0x28, + 0x8e,0x56,0x20,0xe8,0xc3,0x87,0x2b,0x70,0xf4,0x6b,0x8c,0xfa,0xc0,0x5e,0x8d,0x96, + 0x2b,0xf8,0x2a,0x2d,0x8a,0x32,0x9d,0x59,0x98,0x8e,0xa2,0x18,0xcf,0x08,0x7e,0xe3, + 0x2a,0xd8,0xb2,0x85,0xfa,0xc4,0x7e,0xcb,0xc9,0xdf,0x09,0xa8,0x73,0x78,0x04,0x02, + 0x9a,0x1d,0x1f,0x75,0xa4,0x7c,0x2a,0x11,0x46,0xf2,0x70,0x81,0x7a,0xb3,0x35,0x05, + 0x2e,0xf7,0xda,0x87,0xef,0x27,0xa4,0xdd,0xbd,0x7d,0x85,0x62,0xc2,0x2e,0x53,0x98, + 0xf3,0x92,0xb3,0xda,0x42,0x23,0x8e,0x62,0x9b,0x8b,0xe7,0x02,0x35,0xef,0x38,0x61, + 0x3c,0x7e,0x87,0x64,0xb0,0x2c,0x3a,0xcb,0xef,0x00,0x31,0x7f,0x0a,0xe4,0xea,0xe8, + 0x0d,0x3f,0x2c,0x2d,0x07,0xbb,0x1a,0xe5,0x7a,0x28,0x28,0x9e,0x96,0x95,0x4c,0xb3, + 0xc4,0xc7,0x81,0x9c,0xd1,0xdf,0xba,0x80,0xbe,0x02,0xf4,0xcf,0x13,0x0c,0x7e,0x67, + 0x3c,0x90,0xb0,0xe7,0x63,0xd1,0x00,0x35,0xaa,0x5b,0x0d,0x08,0x58,0x72,0x5b,0xf2, + 0xbb,0x9b,0xc0,0x0a,0xd8,0x1a,0xf7,0x05,0x74,0x66,0xa0,0x8d,0x3d,0xa4,0x37,0xcc, + 0xeb,0x70,0x11,0x03,0x1a,0xac,0x2b,0x65,0x22,0x17,0xba,0x88,0x81,0xe2,0xad,0xf0, + 0x46,0x68,0x34,0xed,0x90,0x5d,0xe9,0xa1,0xe7,0x82,0xaf,0xe8,0x40,0x2d,0xbb,0xfd, + 0xbd,0x67,0x81,0xff,0xcf,0x1c,0x01,0xca,0x43,0xbf,0x3e,0x7a,0x84,0x57,0x0e,0xfd, + 0x6c,0xed,0x3b,0x72,0x73,0x19,0x7c,0xcd,0xb3,0xbb,0xff,0xf2,0x23,0xbb,0xdf,0x71, + 0xac,0xf6,0x8f,0x3c,0x4d,0x5b,0xb5,0xaa,0xcb,0x4f,0xca,0x90,0x9b,0x52,0xaf,0x9b, + 0x69,0xe5,0xc0,0x0c,0x50,0x4e,0x9c,0x22,0x99,0xbf,0xeb,0xb0,0x6b,0xcd,0x33,0xae, + 0xdf,0xa2,0x14,0xbf,0x67,0x2c,0x84,0xbd,0xbb,0x5a,0x42,0x39,0x57,0x4e,0xb5,0x40, + 0xb2,0xf6,0x10,0xad,0x41,0x2c,0x9f,0xc5,0x41,0xe4,0xb8,0xa9,0x3c,0x9e,0x20,0x0f, + 0xa5,0xbf,0xff,0xed,0x7f,0xe2,0x11,0xf5,0x84,0x4c,0x35,0x49,0x34,0xd5,0x1c,0x74, + 0x97,0xbc,0xc2,0x7d,0x80,0xf1,0x56,0xbf,0x00,0xb8,0xe9,0x00,0x0d,0xca,0x4b,0x41, + 0xc4,0x5e,0xa5,0x12,0x14,0x4a,0x00,0x39,0x64,0xf1,0x81,0xb9,0xd0,0x04,0xde,0x7b, + 0x89,0x0f,0xb2,0xd6,0x08,0x5d,0x7f,0x82,0x6c,0xb2,0x4d,0x86,0x65,0x4d,0xb4,0x2c, + 0x5c,0xa5,0xe8,0x54,0xf7,0xab,0xc8,0xfa,0x04,0x6b,0x6e,0xf5,0x21,0x3a,0x4d,0x31, + 0x01,0x01,0x6d,0x49,0x86,0xc5,0x24,0xde,0x38,0x29,0x52,0xd9,0x14,0x18,0xe2,0x68, + 0xc2,0x0e,0x6d,0xa0,0xb5,0x08,0x8f,0x00,0x29,0x05,0x02,0x1e,0x44,0xf3,0xaf,0x37, + 0x0b,0x31,0x82,0xdf,0x36,0x8d,0x13,0xd7,0xd5,0x12,0x67,0xae,0x71,0x74,0x8a,0x98, + 0xc5,0x49,0x40,0xe7,0xc0,0xa0,0x62,0x51,0xac,0xde,0x0c,0xa6,0x80,0x7b,0x85,0xca, + 0xc8,0x15,0xc1,0x4a,0x5f,0x79,0x39,0xa0,0xc9,0xdf,0x36,0x89,0x0f,0xac,0xf4,0xba, + 0x95,0xc5,0x00,0x45,0x13,0x8f,0xae,0x25,0xa3,0x44,0x97,0x32,0x61,0xe7,0x15,0x06, + 0xe0,0xaf,0x38,0xf4,0x9b,0xaf,0xa1,0x36,0xde,0x27,0xdc,0x9e,0x0b,0x7b,0x45,0xa6, + 0x4e,0x75,0xb5,0x19,0x51,0x0e,0x30,0xbb,0x79,0xb0,0xe4,0x83,0x07,0x4a,0x27,0xb0, + 0xf4,0xe7,0x3f,0x2d,0x2b,0x21,0xc4,0x65,0xa4,0x73,0x42,0xe8,0x96,0x66,0x87,0xa5, + 0x32,0x4d,0x13,0x3d,0xae,0xac,0x53,0xa1,0x24,0x21,0x0a,0xa6,0x0f,0x91,0xfe,0xcf, + 0x98,0x4c,0x48,0x1d,0xf3,0x01,0xca,0x34,0x33,0xcb,0xb2,0x6e,0xda,0x91,0x72,0x72, + 0xfb,0xf7,0x1f,0xa2,0xac,0x72,0x0c,0x70,0xe2,0x78,0x7d,0xcf,0x80,0x5a,0xcb,0xe7, + 0x68,0xb7,0x64,0xf5,0xf4,0xec,0xed,0x9b,0xf7,0x6b,0x9c,0x9a,0x9d,0xbf,0x17,0xbe, + 0xd7,0xeb,0x32,0xe2,0x30,0x8b,0x97,0xf0,0xdd,0x38,0x70,0x46,0xde,0x24,0x0a,0x40, + 0xe2,0x45,0x47,0xfc,0x28,0xc0,0xb2,0x20,0x6e,0x5d,0xae,0x64,0xc0,0x1c,0x1d,0x02, + 0x1b,0x17,0xe3,0x83,0x24,0xc3,0x19,0x05,0xb0,0x3f,0xef,0xe3,0xad,0xeb,0x9c,0x2e, + 0x20,0x40,0x28,0xe0,0xd7,0x78,0xff,0xee,0xec,0xd5,0x4f,0xeb,0x08,0x89,0x99,0xa8, + 0xb2,0xce,0x2c,0xdf,0x9c,0x9d,0x3f,0x7d,0xf3,0xfc,0xdc,0x7a,0xb3,0x63,0x77,0xed, + 0xb6,0xf5,0xa6,0xd3,0xb1,0x3b,0xf6,0x7d,0x47,0x33,0x0d,0x7a,0x56,0x87,0xc5,0x27, + 0x1e,0xdb,0x0f,0xe7,0xa7,0x14,0x2c,0x5d,0x38,0x9e,0xae,0x36,0x9d,0x8a,0xf1,0xda, + 0x5c,0x47,0x5a,0xdb,0x31,0x28,0x8b,0xad,0x29,0xbb,0xeb,0x5a,0x74,0x4b,0x74,0x14, + 0xf1,0x47,0x03,0xf1,0x35,0xcc,0x72,0x0a,0x0a,0x2c,0x12,0xb6,0x26,0x1e,0x69,0xf9, + 0x09,0xda,0xdb,0xbf,0x02,0xc1,0x4c,0xc3,0x69,0xbc,0x36,0xb5,0x54,0xf6,0x36,0x0e, + 0x0e,0x84,0xb2,0xe9,0x2c,0x24,0x8d,0xe1,0xde,0x45,0xc5,0x7e,0xec,0xbc,0xc2,0x12, + 0xe1,0x7f,0x6d,0xfd,0x86,0xce,0xc3,0x1f,0xaa,0xdf,0x50,0x20,0x84,0x0c,0x17,0x8b, + 0xc2,0xd5,0x72,0x19,0x5a,0x9f,0x2f,0x51,0xd2,0xa2,0x5a,0x09,0x83,0x9e,0x5d,0x53, + 0x8a,0x26,0xf2,0x53,0x4e,0xa2,0x92,0xe4,0x8a,0x32,0x47,0xc3,0x28,0xaa,0x6d,0x64, + 0x14,0xcd,0x3d,0x59,0x40,0x21,0x04,0xa9,0xdd,0x21,0x4b,0x30,0xbf,0xf5,0xe6,0x85, + 0x5b,0xfd,0x4b,0x1f,0x1d,0xd1,0xa1,0x79,0x7c,0xfb,0xea,0x3d,0x4b,0x77,0x74,0xa6, + 0x9a,0x44,0x33,0xdc,0x9d,0x11,0x73,0x49,0x3e,0x11,0xc5,0x01,0xa4,0xa0,0x29,0xda, + 0xeb,0xb8,0xda,0xff,0x97,0x45,0x6b,0x10,0x1d,0xfd,0xff,0x21,0x5c,0x43,0x0e,0xf4, + 0x1f,0x10,0xaf,0xf1,0x7b,0xc2,0x34,0xd6,0x34,0x4f,0xb5,0xbb,0xc5,0x69,0xd4,0x7b, + 0x32,0x5e,0xb2,0x63,0xd6,0x3d,0x46,0x18,0xee,0x1a,0xcb,0x93,0x2b,0x51,0x83,0x9d, + 0x45,0xd8,0x58,0x66,0xd3,0x65,0xf2,0x06,0x85,0x93,0x82,0xea,0x80,0xb4,0x5d,0x24, + 0xbd,0x42,0x9b,0xec,0x1c,0xfd,0x42,0xd1,0x0d,0x2b,0x1c,0xdd,0x9a,0x4b,0xed,0x33, + 0xce,0x4d,0xe1,0xb4,0x6e,0x0c,0x1d,0x4c,0x86,0xa0,0x96,0x6e,0x38,0x1e,0x5d,0x97, + 0x03,0xbf,0x71,0x50,0x70,0x08,0xf1,0x44,0x72,0xcd,0x03,0xc9,0x25,0xd8,0xb5,0xcc, + 0x2f,0xcf,0x8d,0x3e,0x90,0x33,0x85,0x41,0xae,0x79,0xe4,0x56,0x11,0x92,0x0d,0xa0, + 0x86,0x36,0xeb,0x35,0x2c,0x62,0x06,0x61,0xa3,0xb7,0xca,0x81,0x24,0x6e,0xf4,0x9a, + 0x5e,0x93,0x4f,0x17,0x46,0x21,0xc2,0x73,0xcd,0x65,0x6c,0x5d,0x8a,0xc9,0x9e,0x41, + 0x0f,0x25,0x99,0xcf,0x29,0x79,0xb7,0xe0,0x2a,0x1c,0x1c,0x07,0xcd,0xb4,0x80,0xd0, + 0x29,0xd3,0x88,0x9c,0x4c,0xc4,0x7d,0xab,0x81,0xe1,0x7b,0xed,0x5a,0xfa,0x0b,0x4a, + 0x40,0x50,0xc9,0x7c,0xd1,0xd6,0x8f,0xee,0x71,0x46,0xa1,0xec,0x42,0xc2,0xfd,0x8a, + 0x1e,0x7f,0x0b,0x56,0xbc,0x4c,0x3c,0x0f,0x75,0x8e,0x58,0x33,0xfe,0xf4,0x9d,0x49, + 0x5d,0x1d,0x72,0xb6,0x1c,0x9e,0x56,0xec,0x24,0x57,0x98,0xf9,0x23,0x2e,0xa2,0xb9, + 0xc9,0xc2,0xb9,0xcd,0x85,0xca,0xde,0x50,0x7e,0xea,0x91,0xd9,0x2f,0x3f,0x85,0x6f, + 0x6c,0x0d,0x4f,0x0d,0xbc,0x55,0xcd,0xdd,0x7b,0x18,0x82,0xe2,0x1e,0x2f,0x5c,0xfd, + 0x2c,0x44,0x0d,0xda,0x6b,0x94,0x63,0xb1,0x9e,0x10,0x64,0xd7,0x8e,0x03,0xc2,0xed, + 0x4c,0x24,0x42,0xbb,0xf5,0x32,0x7b,0x05,0x6e,0x29,0x9e,0x77,0x87,0xdb,0x6a,0x90, + 0x74,0x4a,0x01,0x64,0x4e,0xc2,0x7e,0x76,0xaa,0x68,0xc3,0xf9,0xc9,0x78,0x62,0xf2, + 0x07,0xb5,0x38,0xcc,0x5f,0xb5,0x28,0x54,0x73,0x78,0x54,0x12,0x62,0xd2,0x58,0x57, + 0xa4,0x9c,0xe6,0x3d,0xa5,0xa5,0x53,0x65,0xdf,0x20,0xd1,0x4f,0x32,0x50,0x96,0x78, + 0x87,0xe2,0x8f,0x92,0x2f,0x5c,0x43,0x13,0xd3,0x62,0x5c,0x95,0x4d,0x88,0xaf,0x48, + 0x16,0xf0,0xd2,0xdc,0xb7,0x92,0x03,0x8d,0x14,0x07,0x3b,0xb9,0xe3,0x08,0x04,0x7e, + 0x35,0xe7,0x4f,0x4f,0x9b,0x46,0xa8,0x0f,0x73,0xbe,0x9c,0xf9,0xc4,0x07,0x29,0x0c, + 0xf5,0x5e,0x25,0x65,0x0e,0x79,0x29,0xd1,0x21,0xb2,0x9f,0xd5,0x41,0x97,0x67,0x7b, + 0x29,0x7c,0xbb,0x28,0xed,0x0b,0xbd,0xac,0x79,0x2b,0x8a,0x14,0x36,0xec,0xa4,0x3f, + 0xbc,0xcd,0x83,0xf4,0x18,0xaa,0x15,0x2e,0xab,0xca,0x8f,0x2b,0x60,0xfb,0x7b,0x38, + 0x2d,0x49,0x4e,0xc4,0x6a,0xb3,0x64,0xe6,0xd1,0x92,0xa4,0x35,0x36,0xfb,0x0f,0xea, + 0x9b,0x62,0x98,0xa9,0x6f,0xe8,0xf7,0x14,0x7f,0xe4,0xdd,0x96,0xe3,0x88,0x73,0x98, + 0xe2,0x31,0x37,0x80,0x05,0xdf,0x2d,0xf5,0x72,0xcd,0xd3,0xf2,0x90,0x74,0x54,0xc1, + 0x56,0x3a,0x96,0x57,0xe5,0xe8,0x7c,0xcb,0x08,0x1c,0x51,0x49,0x1b,0xe6,0x7b,0x11, + 0xa4,0x8d,0x1e,0x8f,0x4a,0x98,0x24,0x33,0xe6,0xc6,0x51,0xdc,0xa2,0x13,0x45,0x4e, + 0xb8,0x52,0x43,0x10,0xf1,0x9e,0x1b,0xf2,0x6e,0x7c,0xcc,0xb5,0x51,0xa0,0x45,0x4d, + 0xd9,0xd9,0x45,0x5b,0x71,0x35,0x8f,0x96,0x92,0x7f,0xa1,0x6c,0x0e,0x51,0x62,0x02, + 0xeb,0x29,0x61,0x28,0xee,0x38,0xf3,0xc3,0x99,0x57,0x8f,0xe0,0xce,0x13,0x36,0x44, + 0x41,0x03,0x01,0x42,0xf2,0x43,0x49,0x59,0x64,0x96,0x14,0xfb,0x59,0x9e,0x2a,0x9f, + 0x53,0x92,0x56,0x33,0xad,0x8f,0x2f,0xcd,0x82,0x02,0x06,0xfe,0xd1,0x39,0x10,0x73, + 0x0c,0x36,0x91,0x5a,0xab,0x11,0x91,0x16,0x7b,0x8a,0x9c,0x3f,0xc0,0x37,0xa6,0x0c, + 0x27,0x03,0x39,0x13,0x4d,0xbf,0x64,0x52,0xf6,0xf1,0x7c,0x2c,0x04,0xf4,0xb1,0x0f, + 0x87,0x49,0x55,0x6f,0x6a,0xf4,0x3f,0x6e,0xa0,0x92,0xaf,0xc6,0x9a,0x93,0x5e,0x79, + 0xae,0x05,0xa4,0x40,0x86,0xbc,0xfd,0x90,0x62,0x24,0x7d,0x36,0xc1,0x85,0x7a,0x85, + 0xb0,0x09,0x3d,0x52,0xdc,0xb4,0x6d,0xfc,0x7a,0x96,0xc1,0xba,0x09,0xc5,0x01,0x35, + 0xb7,0x21,0x86,0xa8,0x09,0x24,0x81,0xb9,0x28,0xd3,0xa2,0x70,0xf8,0x33,0x67,0xec, + 0x24,0x3e,0x8a,0xfd,0xa7,0x93,0x24,0xa2,0xbc,0x0c,0x3c,0x76,0xbe,0x4d,0xc3,0xae, + 0x56,0xfa,0x43,0x84,0x4a,0x3b,0x90,0xd2,0xfa,0x0c,0x9e,0xca,0x78,0x71,0x42,0x8c, + 0x59,0x02,0x04,0x63,0x92,0x65,0x71,0x6f,0x7b,0xbb,0xf3,0xac,0x6b,0x77,0xf6,0x0f, + 0xec,0x5d,0xbb,0x43,0x54,0x37,0x6f,0x13,0xe4,0xb0,0x60,0x89,0xab,0x7e,0x83,0xad, + 0x0c,0x93,0xb8,0xea,0x95,0x09,0x4a,0xbb,0x24,0xcc,0x5c,0x66,0xf0,0x68,0x9d,0x61, + 0xc4,0x1f,0x41,0x44,0x8d,0x48,0xb7,0xb5,0x7f,0x55,0xc3,0xfb,0x81,0x5d,0x26,0x80, + 0x4b,0x29,0x86,0x15,0xe2,0xf1,0x99,0x2f,0x9d,0x41,0x0b,0x4d,0xcb,0xe3,0xd8,0xc1, + 0x42,0x27,0x13,0xab,0x2c,0x10,0xcf,0xc2,0xd4,0x68,0xd8,0x06,0x86,0xcb,0x24,0xd7, + 0xfe,0xb5,0xc7,0x5a,0x9b,0x48,0x7d,0x86,0x29,0x80,0x8a,0x03,0xb5,0x4a,0x7a,0x8d, + 0xd5,0x0e,0xed,0xed,0xa5,0x0e,0xed,0x6e,0x34,0x9a,0x4d,0x91,0x14,0x5d,0x7a,0xd9, + 0x8b,0xc0,0xc3,0xc7,0xef,0x6e,0x5f,0xb9,0xc6,0xa6,0xd8,0x8c,0x9b,0xa6,0x4d,0x40, + 0x7c,0x0d,0xc4,0xc5,0xc6,0xac,0x1a,0xd7,0x9e,0xb1,0x89,0x69,0x93,0x36,0xd7,0xf4, + 0x80,0x87,0xf5,0x1d,0x52,0xc0,0x80,0xf0,0x7c,0x57,0x18,0xb1,0x42,0x2f,0x84,0x93, + 0xef,0x5a,0x64,0x82,0xcb,0xaa,0x84,0x42,0x00,0xa3,0x46,0xcd,0xf2,0xdc,0x0d,0x44, + 0x3c,0x76,0x76,0x8b,0x6c,0x4e,0x3b,0xbb,0x7c,0xe6,0xa4,0xb0,0x90,0x2a,0xf9,0xa0, + 0x30,0x00,0x8d,0x92,0x8e,0x95,0xfa,0x15,0x11,0x26,0x2c,0x3f,0x97,0x72,0xb7,0xe4, + 0x64,0x23,0x56,0xcb,0x4f,0xd3,0xcb,0x7c,0x20,0x0d,0xd1,0xcb,0x25,0xd2,0xa6,0x1f, + 0x15,0x69,0x4a,0xe2,0x25,0x08,0xab,0xa4,0x6f,0xea,0xee,0xd7,0x13,0xb7,0x95,0xc6, + 0x3a,0x8a,0x66,0x61,0xa6,0x2f,0x03,0x3c,0xe0,0x95,0x1f,0x67,0x47,0x8f,0x74,0x0c, + 0x2d,0x11,0xa7,0xc9,0xfd,0x47,0x30,0x3a,0xed,0xec,0xc5,0xdb,0xf3,0x57,0x6f,0x5f, + 0xbc,0x1e,0xe8,0xdf,0x8a,0xff,0x89,0x0f,0x69,0x36,0x58,0xe0,0x09,0x75,0x4f,0xd7, + 0x2d,0x0a,0x2e,0x74,0x7b,0xb4,0xfc,0xd6,0xc4,0x49,0xdf,0x80,0xcc,0x27,0x7e,0x01, + 0xf9,0x4f,0x7b,0x6d,0x6b,0x34,0xbe,0xec,0x85,0xb3,0x20,0xb0,0xf0,0xdc,0xb4,0xb7, + 0xb8,0xb3,0x28,0x5d,0x39,0x3e,0x70,0x70,0x74,0xda,0xbb,0xf8,0x68,0x85,0x24,0xe9, + 0x41,0x69,0x12,0x17,0xd1,0xe8,0x96,0xc0,0x0f,0xa0,0x18,0x59,0x6f,0x81,0x62,0x2d, + 0x95,0x41,0x81,0x14,0x1e,0xee,0x2c,0xe4,0x74,0xe7,0x00,0x10,0x0f,0xd3,0x56,0xf3, + 0x2f,0xae,0x71,0xd7,0x7f,0xf4,0x68,0x3c,0x0b,0xd9,0xb8,0xf1,0x8d,0x91,0x9a,0x0b, + 0x81,0x9e,0x39,0x86,0x83,0x9c,0x92,0xdc,0x9e,0x91,0x76,0x16,0x25,0x50,0xe0,0x4e, + 0x29,0xaf,0x56,0x38,0x49,0x12,0xe7,0x16,0x58,0x73,0x94,0x45,0xc8,0xc0,0xed,0x34, + 0x40,0xba,0x35,0x72,0x40,0x2a,0x6b,0x6e,0x0b,0xe5,0xb5,0xd4,0x54,0xdb,0x23,0x96, + 0x68,0x4c,0xcd,0x05,0xc2,0x2c,0x1b,0x7c,0x63,0xe8,0x4f,0x98,0x4b,0x9a,0xfd,0xcc, + 0xc6,0xad,0x7a,0x2a,0x6e,0x1a,0x9a,0xc2,0xef,0x62,0x7f,0x01,0x13,0x31,0x74,0xdc, + 0x5c,0x50,0x8e,0xcc,0x98,0x38,0x37,0x54,0xa3,0x32,0xfb,0xd3,0x04,0xab,0x7e,0x9a, + 0x0c,0x00,0x6c,0xf2,0xad,0xec,0xce,0x30,0x17,0x59,0x7d,0x97,0x8a,0x86,0xee,0xac, + 0xee,0x7e,0xbb,0xad,0x0e,0x0e,0x64,0x18,0x65,0xb6,0x78,0xa2,0x1f,0x5e,0xc2,0x0b, + 0xa8,0x47,0xf6,0x49,0x63,0xfb,0x62,0xe3,0xf0,0x48,0xff,0xb8,0x7d,0x69,0xe5,0x1d, + 0x8c,0x64,0xf1,0x85,0xbe,0xa1,0xf7,0x74,0xb2,0x12,0xeb,0x96,0x7e,0x88,0xcf,0xa0, + 0x3b,0xc3,0xe3,0x11,0x3e,0x5e,0xe2,0xe3,0xa6,0xbe,0x09,0x8f,0xbf,0xce,0x22,0xf8, + 0x71,0x77,0x31,0xfa,0x78,0x07,0x7d,0x17,0x9d,0x3b,0xb1,0x6f,0x60,0x34,0xa5,0x05, + 0xfa,0x32,0x0c,0x02,0xb0,0x12,0x1f,0x06,0xf8,0x9f,0x2f,0x5f,0x16,0x77,0x98,0xcd, + 0xce,0x1f,0x1b,0x8f,0xf1,0x37,0x80,0x1c,0xc4,0x4d,0xb3,0x78,0x44,0x42,0x0e,0x4a, + 0x05,0x50,0x64,0xbd,0xaf,0x6d,0x6f,0x53,0xc2,0x48,0xfa,0x90,0xa2,0x65,0x15,0xb6, + 0xc8,0xf7,0xe7,0xe7,0xef,0xb5,0x93,0xf7,0xaf,0xb4,0x3f,0xbc,0x38,0x4f,0x35,0x67, + 0x94,0x44,0x40,0x96,0x53,0xa0,0xcd,0xd0,0x73,0xca,0x2d,0x53,0x6b,0x19,0x03,0x70, + 0x63,0x43,0xf0,0xd3,0x93,0x61,0x94,0xd0,0x92,0x60,0xf2,0x49,0x2f,0xa1,0x61,0x69, + 0xd8,0xc3,0x39,0x50,0xdf,0x22,0xb9,0x25,0x07,0x2e,0xa2,0x18,0xed,0x68,0x63,0x0f, + 0x0f,0xe6,0xa4,0xa5,0x0d,0x3d,0x1e,0xae,0x39,0xbb,0x80,0x03,0x8c,0xce,0x71,0x15, + 0x4b,0x5b,0x5f,0x36,0xe6,0x60,0x2f,0x18,0x19,0x49,0x8c,0xdd,0xc1,0x9e,0x36,0x53, + 0xa0,0x7c,0x59,0x72,0x4b,0x0e,0x61,0x14,0x19,0x96,0xa2,0xc4,0x8d,0x54,0x9e,0x6a, + 0x21,0xfa,0x8c,0xb2,0x60,0x80,0xc6,0xb5,0xca,0x20,0x0d,0xb3,0x4f,0x73,0xc1,0xec, + 0x37,0x4e,0x30,0x80,0x52,0xe2,0x91,0xfb,0x6b,0x46,0x13,0x2c,0x45,0xc3,0x30,0x00, + 0x2f,0x54,0x50,0x50,0x1a,0xc1,0x3b,0xf8,0x57,0xd2,0x74,0x9c,0x9e,0xb2,0x50,0x36, + 0xc6,0x11,0x17,0x2d,0x49,0x10,0x01,0x40,0x13,0x71,0xfa,0x39,0x18,0x0c,0x76,0xdb, + 0x1d,0x73,0x81,0x58,0xf7,0x1a,0xf3,0x08,0xc1,0x00,0x33,0x90,0x00,0xe6,0x64,0x19, + 0x7c,0x91,0x24,0xb0,0xe7,0x74,0x24,0x19,0x80,0x92,0x54,0x57,0xf4,0x94,0xd8,0xbf, + 0xa4,0x38,0xb6,0x4a,0x0f,0xbf,0x98,0x0b,0xc4,0x83,0xc4,0x8e,0xae,0x36,0x36,0x64, + 0x1f,0x8f,0x07,0x83,0x6e,0xbb,0x6b,0x56,0x9b,0xfd,0x05,0x73,0xdd,0x45,0xc9,0x97, + 0x2f,0x86,0x4e,0x18,0xa0,0x6f,0xc9,0x0a,0x66,0xce,0xa4,0x7e,0xb9,0xe3,0x29,0xd2, + 0x85,0x53,0x39,0x36,0xc6,0x11,0x6c,0x53,0x9a,0x25,0x66,0x4a,0xcb,0xf7,0x44,0x8e, + 0xa4,0x8b,0xa9,0x07,0x62,0x91,0xdb,0xd3,0xdf,0xbf,0x3b,0x3b,0xd7,0x2d,0x4e,0xae, + 0x95,0xf6,0x16,0xba,0xd8,0xbf,0xad,0x73,0x20,0x10,0x80,0xf9,0x18,0x99,0x22,0x82, + 0xac,0xb7,0x71,0x3a,0xfa,0x1d,0x35,0xd8,0xfb,0xe3,0xd9,0xbb,0xb7,0x76,0x4a,0x3b, + 0xcc,0x1f,0xdf,0x1a,0xd4,0x49,0x75,0x3f,0xc4,0xc1,0xed,0x4b,0x0f,0x06,0x9b,0xa0, + 0x9a,0x86,0x60,0x05,0x62,0xb4,0x79,0x21,0xed,0x64,0x1f,0x81,0x01,0x03,0x76,0xbd, + 0x00,0x34,0x2f,0x80,0xe3,0x05,0x02,0xfe,0x5e,0xa0,0xec,0xfb,0x2c,0xba,0xbc,0x0c, + 0x60,0xdf,0x93,0xaa,0x65,0x3d,0x36,0xe0,0x15,0xd2,0xe1,0x8d,0xb7,0x64,0xf4,0x87, + 0x4a,0x36,0x36,0x0a,0x88,0x81,0xaf,0x4d,0x33,0x87,0x07,0xa3,0x19,0x39,0x03,0x9d, + 0x82,0xae,0x4e,0xd4,0xaa,0xe6,0xcc,0x68,0x8a,0x9d,0x99,0x17,0x33,0xf3,0xa7,0x35, + 0x86,0x60,0x74,0xba,0x07,0x5f,0xba,0x7b,0xfb,0xa2,0x57,0x68,0x07,0xbe,0x08,0x96, + 0x61,0x32,0xd0,0xfb,0x68,0xf5,0x81,0x5d,0xf2,0x0e,0x14,0xd0,0xef,0xcf,0xdf,0xbc, + 0x06,0x31,0x15,0x63,0x20,0x85,0x9b,0x2e,0xee,0xae,0x5b,0xed,0xc5,0xd9,0xfb,0x9d, + 0xae,0x96,0xa0,0x01,0x99,0xb6,0xe6,0xd8,0x4f,0xa6,0x73,0x27,0x41,0x1b,0x35,0x9e, + 0xfc,0xa5,0x28,0x3f,0x51,0x96,0x0f,0x3f,0xe3,0xc6,0x9c,0x51,0x36,0x83,0x8d,0x76, + 0x8b,0xde,0x4b,0x98,0x12,0x86,0x4c,0x0f,0xc3,0xc4,0x77,0x2f,0xbd,0x3e,0x6e,0xd6, + 0x28,0x01,0xa9,0xdd,0x81,0x96,0x87,0x33,0x3f,0x70,0x53,0x8d,0x49,0x27,0x17,0x4b, + 0x1c,0x6a,0x2a,0x83,0x3d,0xce,0x8d,0x21,0x66,0xe3,0x66,0x1f,0xf1,0x1e,0x14,0x1e, + 0x53,0x24,0xbf,0xe1,0x89,0x63,0xe8,0x22,0x81,0xc0,0x40,0x7c,0x5a,0x53,0x5b,0xc0, + 0x15,0x2d,0x21,0xe7,0xce,0x10,0xa0,0xba,0xf9,0x44,0xb9,0x66,0xe6,0xa2,0x14,0x47, + 0x07,0xab,0xdc,0x07,0xa0,0x88,0xb2,0xa6,0xf8,0x2b,0x29,0x39,0xc1,0x8c,0x78,0x08, + 0x7b,0x47,0x99,0x55,0x96,0x41,0xe0,0xce,0x4b,0x61,0x62,0x85,0x7b,0x8a,0x50,0xe6, + 0xa8,0x7b,0xca,0x54,0x4d,0x38,0xf7,0xf6,0xaa,0xe4,0x74,0x32,0x4b,0x4c,0x4e,0x7f, + 0x48,0x86,0x27,0xf2,0x5d,0x1d,0xfb,0x97,0x33,0x0e,0x99,0xe5,0xa3,0x09,0xa8,0x56, + 0x04,0xca,0x46,0x42,0x5d,0x7b,0x7d,0xf2,0xd6,0xd6,0x95,0xee,0x8b,0x7c,0x39,0x95, + 0xee,0x39,0x7b,0x0e,0x74,0xc9,0x29,0x60,0x42,0x91,0x1d,0xa6,0x70,0x35,0x51,0x5b, + 0x51,0x12,0x5d,0x54,0x9a,0xe1,0x18,0xe2,0x52,0x59,0x35,0xe6,0xd8,0xb4,0x7d,0x98, + 0x65,0x82,0xb8,0x0b,0x5d,0x56,0x12,0x55,0xd4,0xe2,0x8f,0x1b,0x9a,0xa1,0x78,0x5b, + 0xd3,0x96,0xa2,0xb9,0x42,0xb6,0x45,0x60,0xf6,0x1d,0xd2,0x2e,0xcc,0xcd,0xdc,0xca, + 0xff,0xa7,0x91,0xd8,0xac,0xfc,0xfe,0x76,0xbb,0xa0,0x2d,0x6c,0xea,0x45,0x4a,0x81, + 0x04,0x4d,0xdf,0x86,0xff,0x6e,0x0b,0x57,0x95,0x2a,0xb9,0x4d,0x05,0x41,0x81,0xb5, + 0x45,0xf1,0x6e,0x90,0xd2,0x9f,0x3e,0x2e,0x35,0x49,0x79,0xf0,0x02,0x1f,0xfa,0xc5, + 0xb6,0x1d,0x3c,0x7e,0x0c,0x85,0xe0,0xa1,0x2f,0x36,0xf9,0x00,0xb9,0x74,0xec,0x0c, + 0xfd,0xc0,0xcf,0x7c,0x0f,0xb8,0x79,0x1b,0xbf,0xb0,0x74,0x07,0xdf,0x12,0x0c,0x92, + 0x9b,0x7a,0x9f,0xe8,0x37,0x7e,0xa4,0xfe,0x10,0x02,0x22,0x61,0x62,0x19,0xda,0xa9, + 0x8d,0x2f,0xbf,0x7c,0xd1,0xa5,0x12,0xa6,0xab,0x15,0x30,0x7d,0x62,0xb5,0x3c,0x92, + 0x85,0x2d,0x5d,0xfb,0x3f,0xff,0x1b,0xe8,0x7f,0x6a,0x8f,0xe7,0xc5,0x33,0x25,0x99, + 0xee,0xe7,0x0c,0x75,0x38,0xe0,0x46,0x38,0x77,0xa2,0xd9,0x1f,0x36,0xc8,0x4e,0x05, + 0x6e,0x33,0xb5,0x62,0xa8,0x0c,0x40,0xaf,0x40,0x05,0x4e,0x37,0x17,0xc3,0x0a,0x8a, + 0x61,0xd2,0x3b,0xbd,0xd4,0x14,0x0b,0x73,0x5c,0xfc,0x0e,0x7d,0xcd,0xab,0x75,0x00, + 0x7f,0x75,0x66,0x85,0x15,0x26,0xd0,0x57,0xf9,0x63,0x01,0xf1,0xe3,0x62,0x15,0x85, + 0xf8,0x8c,0x7b,0x12,0x83,0x18,0x55,0x16,0x2f,0xe4,0xb4,0x42,0xc0,0xbe,0xbb,0x33, + 0x7b,0xea,0x4f,0x26,0xfb,0x95,0xf5,0x8f,0x8b,0xf5,0x17,0x45,0x07,0xb1,0x7c,0xfa, + 0xf2,0xe5,0xe2,0x63,0x01,0x89,0xac,0x06,0x0a,0xa4,0x8b,0x3f,0x52,0x18,0xb7,0x21, + 0x58,0x3b,0x79,0xd6,0x23,0xfb,0xce,0xf1,0xa7,0x24,0x17,0x14,0x85,0x16,0x45,0x3c, + 0xf5,0x1d,0x0f,0xab,0x32,0x1f,0xaf,0x10,0x34,0x3c,0x5b,0x38,0x45,0x82,0x14,0x20, + 0x04,0x89,0xc5,0x32,0x6c,0xd0,0x95,0x83,0x4e,0xbc,0x96,0x00,0x54,0x71,0x74,0x41, + 0x23,0x59,0x0b,0x48,0xb8,0xde,0x57,0xc4,0x23,0xdc,0x25,0x56,0xb7,0x4d,0x32,0xb2, + 0x14,0x10,0x8a,0x3d,0x84,0xc3,0x36,0xae,0xcd,0xc5,0x85,0xfe,0x44,0x26,0xc5,0xb4, + 0xf0,0x51,0x24,0x2e,0xa4,0x67,0x8c,0xc2,0xfe,0x58,0xe7,0xd2,0xbe,0x8b,0x03,0x84, + 0xff,0x36,0x13,0x4e,0xe8,0xe9,0x1b,0x68,0x79,0x39,0xf2,0xcd,0x62,0xe0,0x11,0x1e, + 0x9a,0x6d,0xbf,0x03,0x85,0x51,0x95,0xe0,0x15,0x58,0x12,0x5c,0x8d,0x62,0x70,0x66, + 0x8d,0x42,0xd0,0xfb,0x0a,0x89,0x40,0xb8,0x29,0xd9,0x2f,0x4d,0x1c,0xd7,0x0b,0xb4, + 0x44,0xbe,0xa6,0x50,0x19,0x90,0x16,0x64,0x4e,0xc9,0x42,0x07,0xf0,0xae,0x69,0x2d, + 0xbc,0xeb,0x9a,0xd5,0x52,0x8a,0x10,0xc3,0x2c,0x1c,0x14,0x2d,0xa3,0x31,0x1a,0x36, + 0x57,0x16,0xda,0xf2,0xca,0x99,0x01,0x5a,0x51,0xfb,0x45,0x09,0x0c,0xd3,0xaf,0xac, + 0x1b,0xed,0x72,0x16,0x3e,0x19,0xd3,0x05,0xc8,0x7f,0x87,0x20,0x46,0xf8,0xd3,0x24, + 0x8d,0x2d,0xe4,0xb1,0x66,0xaf,0x18,0x12,0x1e,0x69,0x9a,0x36,0x1d,0xf5,0x81,0x9c, + 0x66,0x42,0xdd,0xba,0xe4,0x7b,0xaf,0xe0,0x2a,0xb6,0x60,0xda,0x93,0x62,0xa8,0xf5, + 0x4b,0xef,0x97,0xbb,0xe6,0xf6,0x6e,0x04,0x82,0x97,0xe0,0x44,0x4a,0x74,0xbe,0xe5, + 0x6e,0x6c,0x14,0xae,0x01,0x43,0x61,0xb1,0x73,0x7a,0x5c,0x01,0xa5,0x32,0x6e,0x04, + 0x62,0x79,0x5b,0x89,0x0d,0xb9,0x0a,0xf0,0x37,0x76,0x2e,0x44,0x73,0x16,0x50,0xb1, + 0x6d,0xf4,0x7e,0xe3,0xb6,0x04,0xfa,0x57,0x1f,0xef,0xca,0x85,0x55,0x36,0x24,0xf5, + 0xa3,0xd3,0x46,0x83,0x7f,0x73,0xa4,0x2e,0xce,0x0c,0x17,0x24,0x96,0xe7,0xeb,0x0f, + 0xef,0x00,0x01,0x6a,0x24,0x4b,0x85,0x06,0x0f,0x40,0x25,0x31,0x0d,0x43,0xbe,0xab, + 0xef,0x0d,0x16,0x35,0x34,0x74,0x58,0xd5,0xb6,0xb5,0x21,0xfa,0x32,0x2c,0xe1,0xa5, + 0xa3,0xf1,0xe5,0x9f,0x9d,0xc0,0xb8,0x32,0x17,0x28,0x0a,0x4e,0x9d,0x18,0x14,0xbd, + 0x9f,0x81,0x8a,0xfc,0x4c,0x2e,0xbf,0x99,0x90,0x5c,0x44,0xda,0x30,0x8e,0x18,0x67, + 0x4c,0xb3,0xd8,0x25,0x02,0x99,0xe4,0xf8,0x52,0x6c,0x94,0xd1,0x80,0x7f,0x5a,0x53, + 0x2b,0x19,0x8c,0x6c,0x12,0x39,0xac,0x5f,0xe1,0x09,0xf9,0x29,0xa8,0xc1,0x6c,0x11, + 0x41,0x73,0xc7,0x1c,0x5e,0xa2,0x58,0x25,0xa4,0x66,0x63,0x3a,0xb8,0xb2,0x29,0x6e, + 0xdd,0xd8,0xfe,0x2b,0x16,0x36,0x2e,0x3a,0xad,0x67,0x1f,0xcd,0xbf,0xd8,0xc6,0x5f, + 0xe6,0x5b,0xe6,0x37,0xdb,0x20,0x60,0x93,0xcd,0x21,0x1d,0x18,0xbf,0xda,0x82,0xd5, + 0x5e,0x7c,0x34,0x2f,0xb6,0xa6,0x17,0x9d,0x8f,0xad,0xce,0x47,0xd2,0xb1,0xb1,0xc0, + 0xf5,0x20,0xbd,0x98,0x5e,0x74,0x3f,0x7e,0x94,0x0a,0xd2,0xf5,0x60,0x80,0xa6,0x9a, + 0x63,0x5d,0xef,0x09,0x93,0xc0,0x35,0x21,0x0f,0xbb,0xa0,0xe2,0xc4,0x09,0x93,0xd0, + 0x79,0x9b,0x13,0x21,0xf4,0xf2,0x7d,0x80,0x3f,0xfb,0xf4,0x01,0xb3,0x12,0xf4,0xca, + 0x86,0x85,0xc4,0x86,0x97,0xa6,0xf8,0x0c,0x1b,0xb2,0xfe,0x39,0x0a,0x05,0xc7,0xa3, + 0x32,0xd9,0x4d,0xbd,0x48,0x76,0x23,0x1a,0x70,0xc6,0xf5,0x8f,0xce,0x58,0xad,0x2e, + 0x83,0x54,0x6b,0xc5,0xc4,0x07,0xd1,0x50,0xb6,0xac,0x58,0x26,0x8b,0x15,0x4d,0x62, + 0xb8,0x65,0x31,0x59,0xf8,0x75,0x8c,0x2e,0x8d,0x3d,0xf2,0x75,0xe0,0xd6,0x4a,0x91, + 0x8d,0x45,0x51,0x7e,0xa1,0x96,0x56,0xc6,0xc9,0x31,0x82,0x4a,0x61,0x7a,0xd1,0x5c, + 0xb8,0x88,0x83,0xaa,0x8d,0x97,0x3e,0x7d,0x82,0x4f,0x66,0x53,0x79,0x19,0x88,0xb4, + 0xbc,0xda,0x27,0x2e,0xd1,0x5c,0x3b,0x8f,0xf7,0x59,0x51,0x5f,0x96,0x51,0x5b,0x50, + 0xe3,0x64,0x8a,0x19,0xe2,0xdb,0x4f,0xfc,0x56,0x29,0x5b,0x0d,0xe7,0xab,0xaf,0x2f, + 0x15,0xf8,0x24,0x0b,0xd4,0x47,0x7a,0x6f,0x0b,0x3c,0xd8,0x15,0xed,0x14,0xfe,0x3f, + 0xb2,0xee,0x9c,0x7e,0xf6,0x8b,0xaf,0x48,0x60,0x8b,0x8f,0xf0,0x4b,0xfd,0x96,0x3b, + 0xa3,0x28,0x25,0xe4,0x3b,0xa5,0x1b,0x35,0x84,0x44,0x96,0xfc,0x55,0xbc,0xe8,0x17, + 0x25,0x28,0xd4,0xa2,0xf8,0x8e,0x3f,0xab,0x8d,0x08,0x29,0xbe,0x28,0xc4,0x2f,0x6a, + 0x88,0x59,0xf2,0xeb,0x2e,0x4a,0x8b,0x37,0xcd,0xc8,0x96,0xbb,0x50,0x17,0xe5,0xe1, + 0x57,0x73,0xd3,0xca,0x66,0xfd,0x15,0x7e,0x28,0x5f,0x12,0xf5,0x4b,0x72,0xb3,0xa2, + 0xab,0x65,0xeb,0xf6,0xab,0x5d,0x2c,0x55,0x51,0x1a,0x5d,0x48,0x8b,0x86,0xe1,0x57, + 0x0d,0xbe,0x14,0xec,0xa1,0x80,0x17,0x7f,0x2b,0x0d,0x70,0x5c,0x44,0xf1,0x9d,0x7e, + 0xab,0xf4,0x47,0xba,0x70,0x2a,0x13,0x13,0xaf,0xfa,0xa5,0x02,0xd2,0x67,0xb2,0x36, + 0x6c,0x59,0xe0,0x13,0x17,0x50,0x11,0x8d,0x5c,0x16,0x95,0x55,0x83,0x9f,0x35,0xc0, + 0x56,0xdc,0x0d,0xcb,0xa5,0x3f,0xe5,0x1f,0xca,0x06,0x3b,0x14,0x9c,0x54,0x13,0x6f, + 0x80,0xcc,0x4a,0x98,0x89,0x50,0x76,0x0e,0x6c,0x3a,0x8b,0x1e,0x28,0x7e,0x94,0xc2, + 0xee,0x42,0x36,0x24,0xbe,0x59,0xb1,0xb2,0x48,0xc5,0x77,0xe2,0x67,0xa5,0x0e,0x52, + 0x3c,0x2d,0x82,0x66,0xad,0xeb,0x95,0x5d,0x14,0x6d,0x0f,0x8c,0x6b,0xfc,0x14,0x85, + 0xac,0x55,0x91,0x40,0x22,0x5b,0x1e,0x5c,0x97,0xda,0x26,0x6a,0x7a,0x1a,0x4d,0x87, + 0x11,0x2b,0xaf,0xc8,0xae,0xe2,0x01,0xf0,0xad,0x6f,0x40,0xc2,0xb8,0x10,0x49,0x2d, + 0x3e,0xea,0x4b,0xec,0x62,0xf1,0x85,0x62,0xe8,0x82,0x72,0x03,0xd9,0x0b,0x9b,0xba, + 0xc4,0xa4,0x62,0x1b,0x33,0xa5,0x6c,0x81,0xfc,0xbe,0x15,0xdb,0xc3,0xb9,0x78,0x48, + 0xc7,0xe2,0x61,0x94,0x94,0x46,0x84,0xf2,0xc1,0x29,0x89,0x0a,0x3c,0x22,0xc5,0x4c, + 0xc8,0x52,0x0a,0xcb,0x11,0x35,0xad,0x7a,0x54,0x68,0x55,0xc0,0xed,0x07,0x23,0x54, + 0x88,0x71,0xbb,0xe3,0x64,0x52,0x71,0x79,0xeb,0x80,0x8d,0xde,0x55,0xa3,0xd8,0x82, + 0x2c,0x52,0x67,0xc8,0xc1,0x41,0xac,0x2a,0x3c,0xbb,0x41,0xa2,0x56,0x3f,0xe4,0xb9, + 0xe0,0x84,0xa0,0x57,0x80,0xe8,0x6a,0x29,0x84,0xc4,0xe1,0x2b,0x82,0xf5,0x6a,0xa0, + 0x00,0xeb,0xca,0xba,0x1e,0xe4,0x62,0x4e,0x5f,0x94,0x12,0x23,0xbe,0xb8,0xfa,0x08, + 0xab,0xa4,0xac,0x7a,0xbf,0x64,0x7a,0x94,0x9a,0x0b,0xcd,0x48,0xcf,0x2b,0x63,0x17, + 0x81,0x33,0x1c,0x50,0x59,0x4c,0xc9,0x0e,0x52,0x9d,0x3d,0x86,0xef,0x24,0x04,0x4d, + 0x06,0xf0,0x6d,0x63,0x03,0xfe,0x53,0x39,0x9a,0xd1,0xe9,0xee,0x45,0x9d,0x4c,0x62, + 0xa3,0x89,0x39,0x9a,0x94,0x0c,0x61,0xd2,0x68,0xa9,0x4e,0x76,0x05,0x3e,0xe4,0x48, + 0x26,0xf6,0xa7,0x10,0xb7,0x2a,0x58,0xb2,0x7a,0x3e,0xb2,0x43,0x01,0x0c,0x6e,0x61, + 0xa0,0xe2,0x69,0xdf,0x19,0x7f,0x0f,0xe4,0xca,0x30,0x9b,0xd6,0x12,0xc6,0x65,0xe0, + 0x8c,0xfd,0x41,0xa7,0xef,0x1f,0x0e,0x72,0xab,0x48,0xdf,0xdf,0xda,0x42,0x6d,0x01, + 0x50,0x31,0x9d,0xe0,0x8a,0xbe,0x44,0x8f,0x9a,0xd4,0xf8,0xa6,0xbc,0xe2,0x96,0x8f, + 0x56,0xec,0xa6,0x42,0xc5,0xea,0x43,0x19,0x46,0x80,0x8a,0xd2,0xd8,0x60,0xf3,0x9e, + 0x3a,0xc9,0xd5,0x73,0x9c,0x97,0x01,0x0b,0x6e,0x15,0x34,0x02,0xf7,0x67,0xb1,0xda, + 0xe6,0x02,0xe4,0x20,0xe0,0xd5,0x9a,0xc4,0x53,0x78,0xc7,0x56,0x0c,0xe5,0xc5,0xe0, + 0xfa,0x4e,0x6e,0x7e,0xb3,0xd1,0x14,0xcd,0xf0,0xb3,0xae,0xf0,0xa0,0x5b,0xd6,0x93, + 0x16,0xe0,0x2b,0xa4,0x07,0x20,0xd0,0x99,0x2a,0xe8,0x6a,0xc3,0xbf,0x7b,0x24,0x8f, + 0x61,0x5e,0xbf,0xda,0x4c,0xb5,0x9f,0xdd,0x59,0x76,0x3b,0xba,0x1d,0x05,0xde,0xcf, + 0x1a,0xd9,0x12,0x33,0x12,0xbc,0xd1,0xbe,0x87,0x57,0xb9,0x93,0x96,0xef,0x67,0x1a, + 0x5e,0x48,0xa3,0xcd,0x13,0x3f,0xc3,0x0c,0x0f,0x9c,0x92,0xe5,0x13,0x67,0x14,0xd1, + 0x9c,0x14,0x5b,0xec,0xb4,0xdb,0xdb,0x86,0x3b,0x32,0x5b,0x1d,0x5b,0x3b,0x13,0x76, + 0x5d,0x34,0x07,0xa2,0x7f,0x3a,0xa0,0x0b,0x0a,0xf1,0xd8,0x93,0x46,0x5d,0x69,0x43, + 0x0f,0x2f,0x85,0xd6,0x7e,0x76,0xc6,0x3f,0x6b,0x97,0xf9,0xb1,0x3c,0x3b,0xd3,0x63, + 0x6b,0x9c,0xe0,0x2f,0x4a,0xd2,0xc2,0xe0,0x9c,0x78,0x4e,0x8a,0xe6,0xfd,0x21,0x3a, + 0x55,0xcc,0x85,0x73,0x85,0x83,0x9e,0x41,0x51,0x28,0xaf,0x2b,0x1b,0xe3,0x99,0x2d, + 0xf6,0x1c,0xf1,0xd9,0x11,0xe5,0x9d,0x1e,0xdb,0xca,0xe1,0x80,0x80,0x8c,0xa4,0x82, + 0x1e,0xa9,0xd7,0x9c,0xf8,0x85,0x76,0xc7,0x63,0x2f,0xb7,0x9e,0x73,0x09,0x67,0x3c, + 0x88,0x9d,0x24,0xf5,0x5e,0xa2,0x6b,0xa3,0x91,0x1f,0x24,0x70,0x9a,0x19,0x3c,0x4b, + 0xa0,0xbd,0xc0,0x24,0xb8,0xa4,0xa0,0xf9,0xe9,0x4b,0xba,0x78,0xc8,0x00,0x71,0x7a, + 0x63,0xc3,0x19,0x1f,0x0d,0xda,0xc7,0xfa,0xdf,0xff,0xfb,0x7f,0xd3,0xf4,0x2d,0xe3, + 0x8d,0x93,0xc1,0x16,0xc4,0x8b,0x5c,0x0c,0x80,0x1b,0x00,0xce,0x19,0x6f,0x75,0x4c, + 0x73,0xbb,0xd3,0x36,0xb7,0xf4,0x7f,0x52,0x00,0xa5,0xf7,0x98,0x0f,0xe5,0xc7,0xa4, + 0x75,0xb3,0x02,0xb9,0x15,0xd7,0xad,0x0a,0x34,0xbb,0x60,0xe0,0x5d,0xdb,0x19,0x1d, + 0xee,0xf6,0x73,0xa6,0x92,0x53,0xa7,0xe2,0x54,0x8e,0x32,0x28,0xba,0x33,0xd6,0xf4, + 0x3d,0x06,0x66,0xaa,0x19,0x6c,0x92,0xd1,0xae,0x53,0xb4,0xa7,0x69,0xe9,0x04,0x0f, + 0x06,0x40,0x39,0x4b,0xc9,0x99,0x26,0xbd,0x0d,0x47,0x92,0x68,0x14,0x40,0xd9,0xdc, + 0x52,0x7b,0xd8,0xda,0xd4,0x1b,0x8f,0x5b,0x22,0x3a,0x8b,0x8a,0x1e,0x0f,0x80,0x90, + 0x99,0x4c,0x03,0x23,0x2b,0x67,0xb1,0x39,0x75,0x28,0x36,0x56,0x89,0xa8,0xe6,0x05, + 0x71,0xaf,0xd1,0x7e,0x94,0xe6,0xb1,0x12,0x09,0x12,0xd3,0x63,0xdd,0xac,0x44,0x5a, + 0xd6,0xa3,0x75,0x30,0xc8,0x68,0xf9,0x0e,0xbc,0x7e,0x9c,0x6f,0x6e,0xa6,0x5e,0x0d, + 0xa3,0x16,0x79,0x00,0x91,0xb8,0xe7,0x43,0x49,0x07,0xd2,0x46,0x8d,0xf4,0x38,0x07, + 0x3e,0xa2,0xbf,0x00,0xb7,0x48,0x95,0x8e,0x71,0x03,0xe8,0xd9,0xa6,0xe1,0x5d,0x1b, + 0x69,0x96,0x53,0xc1,0x8d,0x8d,0xd4,0x66,0xd7,0xe8,0xd4,0xe6,0xe8,0xdb,0x12,0x03, + 0x9a,0x0e,0x5a,0x9d,0xfe,0x87,0x93,0xe7,0xaf,0xde,0x7d,0x7a,0xff,0xe1,0xc5,0xd9, + 0x8b,0xf3,0xb3,0xfa,0xcc,0x62,0x20,0x6e,0xb8,0x00,0x23,0x84,0xc7,0x8b,0x5f,0x0d, + 0xee,0x91,0xa1,0x13,0x9b,0x30,0x5c,0x73,0x3a,0xf0,0xef,0x0a,0x8e,0x25,0xe8,0xfd, + 0x14,0x51,0x58,0x10,0xfd,0xa9,0xd9,0x33,0xe4,0xfc,0x4a,0xb4,0xe8,0x58,0xc7,0xf9, + 0xe8,0xbd,0x54,0xd9,0x18,0x2c,0x57,0xdd,0x95,0xad,0x13,0x15,0xc2,0x24,0x71,0x36, + 0x1c,0xbc,0x1b,0x62,0xf4,0xaa,0x8d,0x88,0x66,0xe4,0xcd,0x8a,0xa9,0xe6,0xbb,0x32, + 0x8e,0xff,0xcc,0x37,0x08,0x0e,0x1e,0x7f,0x63,0x48,0x93,0xa1,0x6a,0xfa,0x13,0xf9, + 0x3a,0xd2,0xda,0x41,0x8d,0x70,0x37,0x35,0x1b,0x96,0x96,0xce,0xf7,0xad,0xf0,0xa8, + 0x0d,0xfb,0x35,0xef,0xa0,0x54,0xb3,0x15,0x56,0xcc,0x30,0xe1,0x96,0x11,0x02,0xd9, + 0xed,0x1c,0xeb,0xc2,0x13,0x14,0x76,0xac,0xf4,0x09,0xd5,0xcb,0x6c,0x42,0x71,0x45, + 0x5d,0xa8,0xf2,0x4f,0xcd,0x0a,0xc3,0xbe,0x0e,0x3a,0x7b,0xab,0x82,0xae,0x68,0x36, + 0xd8,0x59,0x88,0x4b,0x2d,0x33,0xad,0xa4,0x92,0xa9,0x7d,0x9f,0x4d,0x03,0xc3,0xb7, + 0xc6,0x16,0x0a,0x92,0x16,0x85,0x01,0x58,0x78,0x9e,0xa3,0xc8,0x5c,0x9b,0xa5,0x70, + 0x02,0x2d,0x1d,0xb7,0x36,0xb7,0xc6,0xb0,0x6f,0x65,0x1c,0xc1,0xe6,0x16,0xfd,0xdd, + 0xda,0x6c,0x8c,0x63,0xd8,0xdc,0xc2,0xbf,0x50,0xbc,0x14,0x96,0xb6,0xb9,0xe5,0x6f, + 0x6d,0xda,0xdc,0xce,0xef,0xc8,0xb1,0xbc,0xb9,0x65,0xe0,0x60,0x8f,0x37,0xeb,0x71, + 0x6a,0x9b,0x5b,0xf8,0x17,0x07,0x85,0x9e,0x37,0x9b,0xbd,0xcd,0x4d,0x33,0xff,0x51, + 0x02,0xba,0x22,0xdb,0xa5,0x82,0x33,0x23,0xfe,0x4c,0xa2,0x14,0x9d,0x48,0xf0,0x15, + 0x51,0x7d,0xfc,0x5d,0x21,0xfc,0x13,0x80,0x9d,0x34,0x9f,0xae,0x14,0x37,0xf2,0xad, + 0x4d,0x7e,0x17,0x9b,0x8d,0x91,0x11,0x1c,0xae,0x0d,0xfc,0x08,0x1e,0xf2,0xc8,0x86, + 0xcd,0x7e,0xe5,0x84,0xa0,0x61,0xa7,0x02,0x0d,0x82,0x56,0xb7,0x6a,0xcd,0x02,0x9d, + 0x4d,0x47,0x46,0x4c,0x96,0x22,0x13,0x97,0xab,0xf2,0xa2,0xe8,0x44,0xee,0xe3,0xe6, + 0x76,0x46,0xc0,0xd7,0xa3,0xa9,0x7e,0xc4,0x7f,0xff,0xfe,0xb7,0xff,0xac,0x8e,0x0e, + 0xc1,0x80,0xd5,0x54,0xa7,0x62,0x98,0xbb,0x58,0x70,0x7c,0xc4,0xc1,0xf8,0x34,0x04, + 0x41,0x2f,0xb6,0xca,0xa5,0xd1,0x5d,0x6a,0x78,0x84,0x6b,0xa0,0x51,0x41,0xf6,0x2f, + 0xa4,0xa5,0x6a,0xac,0xa0,0x84,0xd0,0x94,0x2e,0x7c,0x68,0x8a,0x9e,0x65,0x4c,0x63, + 0xf0,0x21,0x0c,0x68,0x92,0x9b,0x95,0x90,0x91,0xbc,0x9b,0xea,0xbe,0xd0,0xb3,0xe8, + 0xca,0xc3,0xe3,0x87,0x3c,0x1c,0xc7,0xd2,0x4f,0x46,0x23,0x74,0x3d,0x94,0x5f,0x74, + 0x73,0x69,0xe5,0x59,0xea,0x25,0x64,0xb7,0xb3,0x38,0xf8,0xc9,0xd2,0x7f,0x28,0xde, + 0xac,0xa8,0xa7,0x74,0xa6,0x3c,0xbe,0x2f,0x1e,0x57,0xd4,0x65,0x27,0xff,0xa2,0x47, + 0x0e,0x5e,0x25,0x74,0xbe,0xa7,0xd3,0x28,0xc1,0x12,0x22,0x5c,0x11,0xba,0xe3,0xdf, + 0x2b,0x6a,0x50,0x08,0x79,0xd1,0xd3,0x39,0xa5,0x6d,0x97,0xb7,0x38,0x63,0x0b,0x45, + 0xe8,0x65,0xda,0xd3,0x16,0x68,0x54,0xb9,0xd3,0x16,0xec,0x93,0x0a,0x0f,0x48,0x17, + 0xf0,0x0f,0xc2,0xf1,0x6e,0x45,0x37,0xce,0xcc,0xf5,0xbd,0x70,0xa4,0x40,0xf1,0x8f, + 0x3f,0x9e,0x6b,0xca,0xdb,0x3c,0x47,0x38,0xee,0xa1,0xe2,0x3a,0x1a,0x84,0xfe,0x36, + 0xc2,0x4f,0xe3,0x93,0xab,0x02,0x95,0x24,0x15,0x60,0xae,0x83,0xa0,0x51,0xce,0x91, + 0x11,0x9d,0x2b,0x54,0xb9,0xaa,0x07,0x60,0x0d,0x64,0x90,0xc2,0x71,0xaa,0x81,0x34, + 0xe0,0xe9,0xfd,0x80,0x1a,0x2e,0xab,0x59,0x42,0x0c,0x52,0x37,0x85,0xf0,0x43,0x78, + 0x4c,0x57,0xd1,0x96,0x5b,0x61,0xac,0x7d,0x11,0x0c,0xf0,0x5b,0x73,0x4b,0x4d,0x58, + 0x8e,0x2d,0x4a,0x5e,0x09,0xb8,0x36,0x90,0xcd,0x48,0x9b,0x02,0x7f,0x72,0xbd,0x31, + 0x59,0x9f,0xfb,0xf7,0x50,0x17,0x18,0x1b,0xd3,0x0c,0x60,0x62,0x44,0x3a,0xb0,0x62, + 0x5c,0x38,0xb1,0x84,0x9e,0xe7,0xa6,0x03,0x51,0x40,0x52,0x8b,0x63,0xf9,0xd0,0xc3, + 0x60,0xaf,0x63,0xf8,0xd7,0xa6,0x72,0x3d,0xa6,0x76,0x79,0x65,0xe4,0xa7,0x6f,0x9c, + 0x78,0xc0,0x58,0xd0,0xe3,0xb6,0x06,0x03,0xb9,0xed,0xe4,0x06,0x2a,0x3e,0xe0,0x1b, + 0x5c,0x53,0xfd,0xcb,0x97,0xfc,0x9d,0xe8,0x8a,0x4f,0x9d,0xf2,0x03,0xa6,0xb5,0xaa, + 0xf0,0x6e,0xe9,0x2d,0x69,0x0a,0x76,0x40,0xfd,0x13,0x61,0x7d,0xfd,0xb5,0xc4,0xc7, + 0xda,0x17,0x32,0x35,0x94,0x24,0x16,0x9e,0x73,0x83,0x3c,0x39,0x56,0x58,0xc5,0xbc, + 0x69,0xd1,0x75,0x1b,0xb8,0xaf,0xbe,0x35,0x26,0x8c,0x99,0x9b,0xf3,0xe5,0x6e,0x40, + 0xa2,0x93,0x8b,0xf1,0x47,0x45,0x1f,0x5d,0xa1,0x23,0x08,0xd1,0xe4,0x5e,0x25,0xc1, + 0x9a,0x36,0xa8,0x09,0x1b,0x1b,0xc6,0xb4,0x64,0xd5,0x68,0x3e,0x2f,0x61,0x2c,0xe3, + 0x03,0x13,0x9a,0xe7,0x85,0xaa,0x73,0x5b,0x8a,0x6e,0xdd,0x70,0xb2,0x4b,0xbc,0xb9, + 0x49,0x25,0xc7,0x0f,0x16,0x9d,0xb5,0x08,0xf9,0x9a,0xa5,0xc8,0xaa,0x34,0x84,0x2e, + 0x84,0xdb,0xe8,0x05,0x3e,0x0b,0x96,0x8b,0x45,0x68,0x03,0x79,0x19,0xa1,0xdf,0x96, + 0x15,0x5d,0x59,0xd3,0xf4,0x32,0x87,0x41,0xa3,0x49,0x05,0xb7,0x2d,0x7c,0xa8,0xec, + 0x5a,0x20,0x78,0x83,0xd5,0x06,0x16,0x28,0x61,0xc2,0xbf,0x25,0x13,0x0b,0x9f,0x51, + 0xe5,0x2b,0x34,0x02,0x65,0x36,0xf3,0x84,0x77,0xb8,0x81,0x31,0x62,0xc2,0x8a,0x37, + 0xe2,0x35,0xc7,0xf4,0xd9,0x22,0x8d,0x33,0x68,0x8b,0xd1,0xd5,0xb1,0x1e,0x5d,0x81, + 0x60,0x49,0xa7,0x80,0xfd,0x51,0xd9,0xdf,0x35,0xbd,0xc4,0x8a,0x0d,0x43,0x22,0xae, + 0x89,0x27,0xd1,0x71,0xec,0x85,0xee,0x29,0x5e,0xf7,0x6a,0x8c,0xca,0x22,0xa9,0x1a, + 0xd0,0x04,0x6d,0x26,0x8e,0x35,0x1a,0xe6,0x40,0x81,0xd5,0x9c,0x3a,0xb1,0xb0,0xa6, + 0x81,0x9e,0x72,0x8a,0xfe,0xb3,0x38,0x7f,0x41,0xb9,0xf0,0x4e,0xf1,0x04,0x48,0x25, + 0xdf,0xdb,0x03,0xa3,0xc6,0xb0,0x44,0xf4,0x21,0x14,0x9f,0xe5,0x07,0x74,0xc5,0xc0, + 0x47,0x3f,0x11,0x8a,0xa5,0xfd,0x48,0xb8,0x82,0xfa,0x29,0x5e,0x5b,0x91,0x8a,0xab, + 0xc5,0x48,0xe1,0xa7,0xf0,0x28,0x50,0xeb,0x67,0xa1,0x0f,0xd3,0x91,0xba,0x10,0xa6, + 0xaa,0x0e,0x35,0xe0,0x3d,0xa0,0x17,0x23,0x0e,0xa1,0xc6,0x41,0x37,0x10,0xc2,0xf8, + 0xed,0xea,0x06,0xcc,0x55,0x86,0x14,0xdd,0x32,0x73,0x14,0x73,0xac,0xa1,0xb2,0x01, + 0x63,0x67,0xb0,0x5d,0xe0,0x2c,0x40,0x14,0x96,0xdd,0x31,0x8f,0xf3,0xda,0x17,0xce, + 0x47,0xdc,0xe5,0x44,0xcf,0x8e,0xdb,0xbd,0x8e,0xd9,0xeb,0x16,0x3a,0x5c,0x3c,0xac, + 0x57,0x1e,0xaa,0x95,0x87,0x4b,0x2a,0x4b,0x5b,0xa9,0xd3,0x8a,0x87,0x5f,0xbe,0x38, + 0x36,0xdd,0xbb,0xe4,0x9d,0x62,0x3e,0xa3,0xc4,0x83,0x26,0xc4,0xd9,0x70,0x6d,0x7b, + 0x80,0xaa,0xce,0xab,0x81,0x26,0x22,0xd5,0x7e,0x54,0x78,0x0a,0x44,0xee,0xed,0x00, + 0x0b,0xf5,0xb8,0xa0,0x5c,0x34,0x4c,0x09,0x87,0x0e,0x1a,0xb4,0x30,0x78,0xa6,0x41, + 0x1b,0x21,0xf1,0x28,0x59,0x96,0x13,0x60,0x94,0x39,0xca,0x3b,0x4e,0xe2,0xc3,0x22, + 0xb9,0x94,0xc4,0x2c,0x8f,0x5f,0xb7,0x35,0x76,0xc9,0x17,0x9e,0x7f,0x63,0x0c,0x3e, + 0x77,0xb4,0x74,0x46,0x12,0xd2,0x78,0x16,0x68,0x43,0xca,0xf7,0x2d,0xfc,0x6f,0xd9, + 0x09,0x50,0xfb,0x25,0x02,0xa5,0xab,0xb8,0x0c,0x47,0x8a,0xf4,0x1c,0xe0,0xe6,0x84, + 0x2e,0x37,0x96,0xdf,0x36,0x0a,0x84,0x4d,0x38,0x18,0xf2,0x1d,0xf0,0x9c,0x29,0xb1, + 0xb8,0xc7,0x55,0x13,0x77,0x1e,0x53,0x63,0xd9,0x3c,0x12,0x36,0x28,0x5b,0x9c,0xde, + 0x12,0xd2,0x6e,0x6c,0xd0,0x1f,0x9b,0x5d,0xf4,0x4d,0xf4,0x5a,0x55,0xe3,0xcf,0x43, + 0x81,0xc9,0xc5,0x07,0x0a,0xb3,0x2e,0xde,0x9b,0x08,0x3c,0x51,0x9b,0xdd,0x00,0xb8, + 0xf5,0x12,0x56,0x71,0x51,0xa1,0x86,0xc2,0xea,0x82,0x8e,0xf8,0x58,0xa9,0x97,0x6b, + 0x6d,0x6f,0x45,0x52,0x26,0xbc,0x47,0x02,0x0f,0xb0,0xa4,0xbf,0xec,0x9d,0xaa,0x45, + 0x92,0x27,0x47,0xc5,0x8b,0xe3,0x91,0xa6,0x29,0xa7,0xf4,0xc2,0xfe,0xcd,0x5e,0xb4, + 0x35,0x0d,0x31,0x8e,0x82,0xe0,0x03,0x11,0x3b,0x63,0x34,0xb4,0xda,0xf0,0x0f,0x85, + 0xd3,0xb1,0x3b,0xc4,0x1a,0x0e,0x3f,0x83,0xb2,0xc3,0x4f,0xd3,0x98,0xd8,0x01,0x00, + 0x8d,0x04,0x43,0x73,0x34,0x34,0x28,0xe8,0x80,0xde,0xa9,0x13,0xca,0x5d,0xba,0xd1, + 0x89,0x44,0x9b,0x3a,0xe8,0x16,0x7a,0x8d,0x17,0x6f,0x84,0x78,0x7d,0x0b,0xfa,0xb4, + 0x68,0x68,0xa7,0xbb,0x9c,0x90,0x4d,0x11,0xf6,0x4b,0x1c,0x85,0xa9,0xa7,0xcd,0x1d, + 0xd8,0xf8,0x30,0x57,0x79,0xd7,0xec,0xb5,0x27,0xdb,0x12,0x99,0xdb,0x00,0x97,0xbc, + 0x31,0x20,0x24,0x20,0x6c,0x10,0xb0,0x6d,0xf0,0xe4,0xfd,0x66,0x4a,0xce,0x8d,0x26, + 0x09,0x82,0x08,0x01,0xbe,0x9a,0x09,0x33,0x1e,0x11,0x28,0x30,0x09,0x1b,0x30,0xd5, + 0x00,0xaf,0xec,0x10,0x1c,0xbe,0x0a,0x25,0x31,0x81,0x9c,0x65,0x92,0x43,0xeb,0x95, + 0xe7,0xf6,0xd4,0xe4,0x0c,0x38,0x39,0x18,0x7d,0xb7,0xdd,0xcd,0x3b,0xc0,0xe9,0x59, + 0x39,0xa2,0x13,0xd6,0x53,0x6e,0x87,0x21,0x52,0xa8,0x08,0xb7,0x83,0xe7,0xda,0xd8, + 0xda,0x89,0x12,0xbd,0x29,0x22,0x70,0x26,0x44,0x7f,0xc5,0xe5,0x79,0xfe,0x58,0x38, + 0xde,0xe2,0xa9,0x13,0x26,0x85,0xa0,0xdd,0xe4,0x61,0xb6,0x51,0x22,0x87,0x9c,0x4b, + 0x17,0x89,0x22,0xb6,0xc6,0x2e,0xed,0xa9,0x06,0x8b,0x82,0x7b,0x61,0x86,0x59,0xad, + 0x74,0xc2,0x0c,0x91,0xbf,0x8e,0xa0,0xa8,0xb3,0x3f,0x04,0x45,0xa7,0xc2,0x76,0xe6, + 0x6c,0xf9,0xa0,0x24,0xdb,0xaa,0x57,0xb6,0x0a,0x09,0x60,0x23,0xa9,0xe5,0x23,0xa0, + 0x2c,0x9a,0x3d,0x7b,0x21,0xc1,0xcb,0x01,0xfe,0x07,0x3d,0x09,0xe9,0xe3,0x80,0xfe, + 0x2b,0x1c,0x0b,0xf3,0xa6,0xd0,0x8b,0xc5,0xb8,0x0f,0x6d,0x24,0xc2,0x29,0xb8,0xc3, + 0x3d,0xf5,0xc9,0x52,0xc7,0x5b,0xc5,0xe1,0x13,0x32,0xb4,0x00,0xb9,0x16,0x3a,0xf1, + 0x6a,0xa5,0xa9,0xe5,0x48,0xc2,0x7e,0x66,0xe4,0x51,0x02,0x9b,0x0a,0xa6,0xe7,0x8f, + 0x6f,0x81,0x19,0x52,0xf4,0xad,0xe2,0x8a,0xc6,0x37,0xd9,0x29,0x5e,0x31,0x08,0x5e, + 0xf2,0x4f,0xe3,0xa3,0x9c,0xea,0xd9,0xd2,0x36,0x63,0x8d,0x6e,0x2d,0x84,0xc7,0x7e, + 0x6f,0xb7,0xdd,0x6e,0xdf,0xdd,0xeb,0x98,0x4f,0x9b,0x07,0x39,0x2a,0xba,0xbd,0x11, + 0x5d,0x6e,0x08,0x0b,0xa8,0xa2,0x1e,0x43,0x9b,0x37,0xa9,0x05,0xfd,0x94,0x37,0x51, + 0xd1,0x36,0x7a,0xe2,0xb9,0x24,0x43,0x4b,0x9b,0x1e,0x6e,0x0a,0xa8,0xdc,0xcb,0xf1, + 0x50,0x0b,0xfc,0x2b,0x2f,0xb8,0x15,0x2b,0x8d,0x61,0x71,0xd7,0x02,0x02,0x08,0x42, + 0x34,0xa6,0xa3,0xbd,0xde,0x01,0x46,0x8a,0xc2,0x05,0x92,0x39,0x3c,0x04,0x55,0x5a, + 0xcb,0x36,0x25,0x9e,0xa1,0x53,0x52,0x80,0x26,0xf3,0x9c,0xb8,0x2b,0xe0,0x1b,0x06, + 0x7e,0x2c,0x6a,0xc1,0xf8,0x68,0x02,0x87,0xbb,0xeb,0xcd,0xb7,0x40,0xb0,0xad,0x8e, + 0x58,0xf8,0x3b,0xeb,0x69,0x75,0xd6,0x9a,0xc0,0xa5,0x7e,0x21,0x60,0xb1,0xca,0xb6, + 0x9c,0x50,0x2a,0xd8,0x45,0x1e,0xf6,0x50,0x3f,0x1d,0xb4,0x2d,0x32,0x43,0x32,0xaf, + 0x52,0x8b,0xa0,0x8f,0x0a,0x4b,0x84,0xe4,0xb2,0xd3,0xe0,0x4f,0x98,0x95,0x4c,0xa7, + 0xd1,0xd5,0x60,0xfb,0xaf,0xef,0xfe,0xb4,0xed,0x33,0x47,0xf7,0x33,0x0a,0xe0,0xb9, + 0xcd,0xed,0xa0,0x14,0x46,0x73,0x65,0x52,0xaf,0x5b,0x5b,0xca,0x5b,0x9f,0x98,0x04, + 0xe2,0x05,0x1b,0x44,0xf3,0x56,0xe9,0xeb,0x36,0x13,0x82,0x5a,0xb3,0xa6,0x3a,0x6c, + 0xc9,0x02,0xf8,0x7f,0xdf,0xe4,0x36,0x4d,0xed,0x21,0x26,0xea,0x25,0x87,0x6c,0x12, + 0xaa,0xb9,0xbc,0xbf,0x24,0x80,0x6b,0xb3,0xd4,0x25,0x19,0xf3,0x79,0x66,0x42,0x8f, + 0x2d,0x26,0x0c,0x42,0x79,0x59,0x98,0x46,0x11,0xf5,0xdd,0x9f,0xf4,0x9e,0x9c,0x5c, + 0x11,0xfa,0xf4,0xd7,0x17,0x49,0x62,0x44,0x89,0x79,0x7c,0xd1,0xb3,0x3e,0x1e,0xff, + 0x25,0xfd,0x76,0xdb,0x47,0xb3,0x43,0xf5,0xe0,0x31,0x8f,0x90,0xa0,0x2b,0x4a,0xcc, + 0xe5,0xaa,0x8f,0x02,0x34,0x53,0xa5,0x33,0x0b,0x20,0x34,0x49,0x6d,0x5b,0xd1,0x5a, + 0x99,0x4c,0x6f,0x78,0xdd,0x74,0x79,0x05,0xa5,0x91,0x9a,0x40,0x1e,0x44,0xa2,0x44, + 0xdc,0x3e,0x78,0xed,0x13,0x0b,0xa8,0xf2,0xbc,0x55,0x1e,0x2b,0x24,0x39,0x93,0x2f, + 0xce,0x70,0xcb,0x47,0xcd,0x1a,0xe9,0xba,0x22,0x17,0x88,0x7e,0x9e,0xdf,0x59,0x95, + 0xca,0xc8,0x54,0x14,0x09,0x72,0xa1,0x08,0x51,0xdb,0x15,0x57,0x6d,0xe7,0x57,0xa5, + 0x7e,0x90,0x52,0x0f,0xc7,0xcb,0x3a,0x59,0xee,0xc5,0x8f,0x84,0x8c,0xae,0xde,0xcd, + 0x99,0x94,0xc3,0x87,0x74,0x9c,0x10,0xa8,0xc5,0xa2,0xb7,0xe7,0xe6,0x17,0x56,0xe9, + 0xcc,0xe8,0xd0,0x3c,0x24,0x6e,0xe8,0xe4,0x1e,0xff,0xfe,0xef,0xff,0x43,0x50,0x53, + 0x11,0x69,0x59,0x1c,0x2c,0x97,0xf6,0xa0,0x42,0xa4,0x99,0x3c,0x63,0x4d,0xbd,0x38, + 0xb2,0x2d,0xcf,0xbd,0xb0,0x65,0x9f,0x45,0xe3,0x4c,0x9e,0x90,0x3e,0x50,0x0c,0x29, + 0x91,0x82,0x42,0xb4,0xc0,0x74,0x21,0x3e,0x92,0x31,0x09,0x0b,0x72,0x75,0xec,0xe5, + 0x50,0x94,0x02,0x03,0x06,0x92,0x5d,0x85,0x91,0xbc,0x6c,0x96,0xae,0x27,0x17,0xf7, + 0xc9,0xc8,0xb6,0x4e,0xde,0x73,0xd6,0x47,0x49,0x18,0x8d,0x31,0xa0,0xd8,0x44,0x9e, + 0x69,0x92,0xa4,0xc1,0xf2,0x85,0xad,0xd1,0x8d,0xa5,0x48,0xd2,0x70,0xd5,0x10,0x60, + 0xd8,0x28,0x5b,0xb6,0x64,0x6b,0x68,0xf8,0x4a,0x55,0xe1,0x83,0x6f,0xac,0x99,0x7b, + 0x20,0x05,0x85,0x18,0x60,0x91,0x61,0xce,0x65,0x07,0x73,0x38,0xd8,0xf9,0xbc,0x81, + 0x2c,0x1e,0x76,0xdb,0xeb,0xd3,0x50,0xa0,0x9e,0x2a,0x9b,0x6e,0xa0,0xa1,0x02,0x6c, + 0xf5,0x33,0xe9,0xea,0xa2,0x90,0x93,0x65,0xe2,0xb5,0xf0,0xd4,0x4e,0x63,0x07,0x29, + 0x99,0xd9,0x5c,0x9e,0xa5,0x82,0xf6,0x3b,0x1c,0x7a,0x09,0x5f,0xbe,0x46,0x73,0xc2, + 0x3d,0x9e,0x36,0xb0,0xce,0xfb,0xdd,0x32,0xd6,0x76,0x9d,0xa8,0xfb,0x4c,0xf4,0x15, + 0x07,0x89,0xc2,0x79,0x22,0x77,0x94,0x50,0x0e,0xd4,0xd7,0xf2,0x30,0xf8,0x9a,0x2e, + 0x0d,0x6b,0x78,0x2d,0x2c,0x77,0x19,0x68,0x72,0x9f,0x6d,0x08,0x3f,0xa1,0x88,0xa6, + 0x6d,0x3a,0x70,0x5d,0x62,0xc6,0x28,0x5c,0x91,0x11,0xe8,0xb9,0xb7,0x3a,0x1d,0x80, + 0x91,0x29,0x40,0x3d,0x5b,0xaa,0xef,0xc2,0x66,0xc7,0x7f,0xb1,0xdb,0x5f,0xb2,0x3c, + 0x95,0x45,0xec,0xc1,0x2b,0x97,0x9b,0x47,0x4a,0x01,0x53,0x78,0xe9,0x59,0x93,0x77, + 0x3b,0x85,0xd8,0x34,0x5b,0x98,0x86,0x85,0x81,0xa9,0x30,0xb1,0x88,0x1c,0x1e,0x6c, + 0x66,0x51,0x8d,0x2c,0xdf,0xc8,0x6e,0x34,0x59,0xa4,0xbe,0x66,0x37,0xe6,0xe2,0xa6, + 0x81,0x45,0x40,0x61,0xeb,0x06,0x88,0xca,0x50,0xac,0xd4,0x85,0x3c,0x80,0xe5,0x98, + 0x30,0x8b,0xaf,0x4e,0xb3,0xc4,0xcd,0x68,0x0d,0x66,0xa8,0x8c,0x05,0xdb,0xac,0xa5, + 0x6f,0x65,0xcb,0x79,0x50,0x06,0x60,0x1b,0xe6,0x88,0x91,0x89,0xce,0x60,0x1e,0xca, + 0x4b,0x0a,0xe7,0xa0,0x6e,0x4c,0xce,0x79,0x82,0xcf,0x86,0x90,0x7f,0xd3,0x2c,0x8a, + 0xe5,0x8b,0x46,0x73,0x16,0x25,0xd8,0x5d,0xb2,0xfc,0xe3,0x69,0xf6,0x43,0x8c,0xe1, + 0x45,0x64,0xe6,0x1d,0x90,0x2b,0x01,0x25,0x20,0x32,0xd2,0xed,0x83,0x7d,0x94,0x2d, + 0xad,0x49,0xe9,0xed,0x3f,0xd1,0xdb,0xed,0x1d,0x0c,0x5a,0xb6,0xa6,0xe5,0x4f,0xf8, + 0x72,0x7b,0xbf,0xad,0x3a,0x72,0xb9,0x47,0xed,0x63,0x77,0x4b,0x77,0x35,0x7d,0x6b, + 0xb2,0xa5,0x4f,0xf4,0x9e,0x31,0x81,0x37,0xf8,0x08,0x6f,0xa6,0x5b,0xfa,0x54,0xef, + 0xd1,0x7f,0x4b,0xd1,0xd9,0x80,0x35,0x86,0x6f,0x0d,0xad,0x3c,0x0a,0x7a,0xb3,0x9a, + 0xb5,0x09,0x93,0x0f,0x1e,0x89,0xf3,0x1f,0x1f,0x0f,0x84,0x36,0xb7,0x86,0x7c,0x16, + 0xb4,0xb9,0x65,0xa4,0xc7,0x9b,0xda,0x61,0x0a,0x4f,0x7c,0x78,0x53,0x39,0xd0,0x53, + 0x0d,0x56,0x0a,0x2c,0x17,0x2a,0x18,0x91,0x78,0xca,0xe7,0x94,0xdd,0x39,0x39,0xf2, + 0x1d,0xc3,0xbb,0x5f,0x09,0xb7,0x47,0x23,0x2f,0x65,0xed,0xb4,0xcb,0x21,0xdc,0x4a, + 0x5b,0x0b,0xf6,0x35,0x2a,0x9a,0x00,0xd9,0x02,0x4d,0x5f,0x79,0x2b,0xe5,0x8f,0x95, + 0xee,0xda,0x77,0x77,0x65,0xbd,0x4b,0xb6,0x5a,0x0b,0x1a,0x5b,0x1e,0x33,0x46,0xf1, + 0x51,0x0d,0xba,0x06,0x07,0x1a,0xca,0xa4,0x5b,0xb5,0x78,0x80,0x20,0x60,0x9b,0xc9, + 0xdf,0xff,0xf6,0x9f,0x7a,0x89,0x4b,0xac,0xac,0xa7,0x17,0xf2,0xf4,0xd5,0x70,0xa0, + 0x78,0xa6,0xa4,0x36,0x26,0x0b,0xf8,0x34,0x4e,0x3c,0x6f,0x1b,0x53,0xcb,0x29,0xa2, + 0x1a,0xe7,0xde,0x52,0xe3,0xf3,0x84,0x18,0x41,0x88,0xa0,0xff,0x10,0xa3,0x4a,0x05, + 0xc4,0x80,0x51,0xd5,0x9e,0xc5,0x1c,0xa8,0x66,0x9a,0x5b,0xa5,0x72,0xdf,0x71,0xae, + 0x37,0xdd,0x82,0x32,0x98,0xf7,0xed,0xd3,0xf4,0x7a,0x1b,0xfd,0x61,0x60,0x84,0xd1, + 0x4b,0xbc,0xfb,0xcd,0xe8,0x9a,0x96,0xfe,0x67,0xbd,0x52,0x2d,0xcf,0xdf,0xa5,0x5b, + 0x57,0x43,0x4b,0xff,0xd3,0x77,0x18,0x96,0x36,0x05,0x86,0xa6,0x6f,0xd5,0x87,0x0f, + 0xef,0x79,0xf4,0x95,0x46,0xe8,0x3a,0x8a,0x20,0x88,0x46,0xba,0xd5,0x50,0x09,0xdd, + 0xb8,0xf1,0x23,0x57,0xc5,0x3e,0xaa,0x83,0x50,0x32,0x7f,0xe9,0x56,0x6a,0x53,0x3e, + 0x05,0x4b,0x77,0xbf,0x9b,0x56,0x0b,0xbe,0x06,0xc2,0xaa,0x7d,0x38,0x3b,0x7b,0x85, + 0xc5,0x12,0x10,0xd5,0xa8,0x14,0x0e,0xf9,0xec,0xed,0x07,0x8a,0xa6,0x4b,0xc3,0xa4, + 0x52,0x47,0x5e,0x7a,0x75,0xfe,0x53,0x01,0x44,0xe9,0x75,0x55,0x83,0xa2,0x2c,0xfc, + 0x41,0x29,0x9c,0xc0,0xf8,0x97,0x95,0x7f,0x2f,0xb2,0xf9,0x62,0x79,0x28,0xe9,0x8d, + 0xae,0x2d,0xb4,0x2c,0xd3,0x50,0xa0,0x1e,0x3c,0x2e,0xa9,0x70,0x4e,0x15,0xd0,0xb6, + 0x50,0x5d,0x10,0xba,0xf6,0xe0,0xc3,0x4f,0xdb,0xe7,0x79,0x93,0x9f,0xc8,0x45,0x1c, + 0xa4,0xed,0x6d,0x9e,0x22,0x54,0xe2,0x57,0x95,0xaa,0xcf,0x7d,0xf4,0x01,0xa8,0xd4, + 0x75,0xe9,0x65,0xb9,0x32,0xbf,0xab,0xd4,0x3e,0xff,0x49,0x03,0x15,0x66,0xe6,0x61, + 0xcd,0xec,0xe6,0x13,0x3d,0xe7,0x45,0x14,0x87,0xc1,0x63,0xb1,0xe6,0x18,0x85,0x9c, + 0x57,0x40,0x9e,0x20,0xaa,0xf4,0xf4,0xea,0xaa,0x91,0x70,0x2e,0x57,0x0d,0xd9,0xc6, + 0xa7,0x7c,0xe9,0xaa,0x45,0x5f,0xbd,0xc7,0x32,0x7e,0xfc,0xe5,0x8b,0x0e,0xb2,0xa2, + 0x14,0x93,0xe3,0x59,0x3a,0xa1,0x11,0x20,0xff,0x40,0xa4,0x02,0x5c,0x05,0x32,0xa5, + 0xbe,0x65,0x9c,0x11,0xb8,0x23,0x45,0x0b,0x4c,0x17,0x47,0xae,0x88,0x4a,0x1a,0x3a, + 0xd3,0x52,0x1b,0x02,0x8a,0x53,0x29,0xc4,0xd9,0xe5,0x8a,0x52,0x6a,0x83,0x15,0x27, + 0xd8,0xc2,0x0f,0x29,0x18,0xa4,0x45,0x3c,0x4b,0xbf,0x44,0x27,0xa4,0x0f,0xa4,0xb2, + 0xbf,0xd3,0x40,0x98,0x3e,0x8f,0xe1,0x69,0xea,0xc4,0xf5,0x78,0x2b,0x71,0xc8,0x81, + 0xf9,0x51,0xd2,0x81,0x71,0x63,0x83,0x36,0x4d,0x27,0x90,0x5f,0xbe,0xdc,0x60,0x20, + 0x14,0xff,0x30,0x31,0x1a,0x46,0x87,0x55,0xc5,0xef,0xb0,0xc0,0xd1,0x15,0xad,0x31, + 0x95,0x80,0x9f,0x78,0xc0,0x51,0xd6,0x48,0x4a,0x7c,0xe4,0xea,0x9a,0xb8,0x88,0x70, + 0x25,0xb8,0xb1,0x43,0x19,0x91,0x8a,0xae,0x0f,0x37,0x85,0xeb,0x03,0xf1,0x97,0x52, + 0xf2,0x9a,0x18,0xcd,0x1d,0x9b,0xa2,0x1c,0x51,0x56,0xc5,0x69,0xa2,0x78,0x23,0xd2, + 0xd7,0x6c,0x6e,0xf1,0x34,0x4a,0xbe,0x0a,0x52,0x61,0xb5,0xd1,0xc2,0x6d,0x00,0xc2, + 0xf4,0x80,0x06,0xc8,0xac,0x7b,0x79,0x88,0xb5,0xe7,0xda,0xba,0xe2,0xf2,0xb4,0x54, + 0xea,0x2b,0x58,0x04,0x62,0x84,0x83,0x5e,0xdf,0x8e,0x4d,0xcf,0xd7,0x24,0x12,0x39, + 0x02,0xdc,0x47,0xc0,0x9c,0x1d,0x3b,0x9d,0xf8,0x28,0xc4,0xab,0xfc,0x8a,0x50,0x00, + 0xb6,0x2e,0x4a,0x1c,0xb9,0xa4,0xe5,0xc6,0xc9,0x40,0x64,0xe0,0x60,0x75,0xe5,0x3d, + 0x50,0xd1,0xe0,0x03,0x1a,0xd4,0xbe,0x7c,0xe9,0x60,0xec,0xd3,0x35,0x48,0x35,0xa8, + 0x50,0xfd,0x48,0x17,0x78,0x4e,0x06,0x7b,0xfb,0x74,0xec,0x74,0x6d,0x53,0xce,0x9c, + 0xc1,0xfc,0x5b,0x68,0xa2,0x0f,0x3f,0x45,0xa6,0xc2,0x09,0xfd,0x16,0xad,0x5f,0x62, + 0xf5,0x4b,0x8f,0x59,0xc7,0x0d,0xc8,0x70,0x5d,0xcc,0x62,0x70,0x69,0xa7,0x78,0x88, + 0x61,0x40,0x41,0x0b,0xfe,0xc5,0x17,0xc4,0x2e,0x41,0x9f,0xcd,0x0c,0x34,0xba,0xce, + 0xad,0x89,0x14,0x90,0x70,0xb0,0x62,0x62,0x87,0xdd,0xca,0xc1,0xda,0x34,0x64,0xe6, + 0x03,0x04,0xdb,0xa6,0x33,0x23,0xb6,0x1b,0xd2,0xfc,0xac,0xe9,0x8d,0xf8,0x88,0x11, + 0x38,0x95,0x8f,0xa2,0xed,0x29,0x4a,0x7e,0xd3,0xd0,0x5c,0x4c,0x6f,0xb6,0x06,0x9d, + 0xfe,0x34,0x6c,0x0d,0x3a,0xb8,0x06,0x97,0x18,0xa0,0x18,0x5d,0x79,0x67,0x94,0xfb, + 0x87,0x86,0x3f,0x8d,0x41,0x6f,0x76,0xe9,0x45,0x91,0x80,0x46,0x3e,0x88,0x53,0x39, + 0x13,0x67,0xfa,0x3e,0xc1,0x73,0xa9,0xec,0xf6,0xcf,0xa8,0x1c,0x18,0x7a,0xab,0xe5, + 0x8c,0x46,0xc8,0x3f,0x13,0x7f,0x6a,0x98,0xb0,0xdf,0x9f,0x74,0x87,0x4f,0x5d,0xef, + 0x19,0xad,0xf8,0xa5,0x8d,0x29,0xcc,0x08,0xae,0x83,0x6e,0x9f,0x7f,0xfd,0x31,0xc2, + 0xec,0xcf,0xc4,0x5a,0x74,0x78,0x35,0xf4,0x40,0xed,0x7a,0x0f,0xf3,0x60,0x9d,0x80, + 0xa0,0x51,0x93,0x41,0xaf,0x85,0xef,0x01,0x43,0xe5,0x66,0xe0,0x6f,0xab,0x60,0x6b, + 0x75,0xcc,0x6f,0x8d,0x79,0x6b,0xd7,0xdc,0xea,0x5a,0xb7,0x83,0x49,0x6b,0xb7,0x65, + 0x5c,0xb7,0x60,0xd6,0xdb,0x30,0x7f,0xfc,0xfb,0xad,0x01,0x45,0xda,0x72,0xef,0x1f, + 0xf3,0x30,0xce,0x23,0xe3,0xc6,0xba,0x35,0x7b,0x97,0x36,0xaa,0x48,0xe2,0x57,0x91, + 0xac,0x42,0x42,0xc8,0x10,0xbf,0xc6,0x3e,0x4a,0x2d,0x04,0x2d,0x15,0x76,0x30,0x01, + 0xcc,0x8c,0x84,0x79,0x66,0xe3,0x1b,0x2d,0xbd,0x05,0xc9,0x7f,0xda,0x9a,0xf9,0x38, + 0x31,0x44,0x87,0x13,0x4c,0x95,0x05,0x93,0x45,0xbc,0xd1,0x8b,0x86,0xce,0x11,0x53, + 0x70,0x02,0x17,0xa5,0x59,0x7c,0xb4,0xe6,0xad,0xae,0x45,0x23,0xad,0xa9,0x40,0x48, + 0x6e,0x39,0x65,0xe2,0x12,0x11,0x18,0xcf,0xa9,0xfe,0xe4,0xdd,0x52,0x14,0xba,0xc1, + 0x8a,0x85,0x7a,0x9c,0xcd,0x6f,0x36,0x36,0x96,0x65,0xe2,0x12,0x35,0xfa,0xb9,0x9b, + 0xf5,0x30,0x98,0x25,0xc5,0x39,0xad,0xe3,0x15,0x06,0x32,0xba,0x6f,0x50,0x1e,0xd4, + 0x0a,0x44,0x73,0xbc,0x8d,0x0d,0x07,0x55,0xa7,0xbc,0x14,0x1e,0xd2,0xe0,0x3b,0x6a, + 0xc8,0x14,0x7f,0x8d,0xf2,0x3e,0xcf,0x73,0x0c,0x2b,0xe3,0x45,0xc9,0x31,0xcf,0xbe, + 0x34,0x28,0xfc,0x74,0x41,0x55,0x7f,0x83,0xc7,0xa0,0x7c,0xa7,0x39,0x26,0x7b,0xcc, + 0x2f,0xf2,0x18,0xe3,0x4d,0x63,0x04,0x63,0xbe,0xac,0x82,0xcf,0xc5,0xa8,0xa6,0x92, + 0x43,0xb2,0x87,0x99,0x7c,0x1e,0xe5,0xae,0xbd,0x6c,0xa8,0x18,0xc3,0x70,0x53,0x4f, + 0xba,0x4a,0x5f,0xb3,0x77,0x23,0xfa,0x53,0x8b,0x9e,0xae,0x04,0x44,0xf3,0x83,0x0e, + 0xee,0x9a,0x32,0x55,0x3e,0x12,0xa6,0x63,0x14,0xc0,0x6f,0xd3,0xbc,0x32,0x9d,0xf0, + 0xcc,0x27,0xc2,0x34,0x55,0xe4,0xf5,0x49,0xbc,0x31,0x74,0x45,0x77,0x84,0x50,0xfa, + 0x6b,0xb5,0x79,0xfb,0x51,0xdd,0x0e,0xd9,0xbc,0x40,0x16,0x0d,0x80,0xfc,0x65,0x95, + 0x14,0x97,0x45,0x7e,0x95,0x81,0x17,0x6c,0x6c,0xd4,0x8e,0xed,0x45,0x56,0x95,0x8d, + 0x8d,0xb1,0x8d,0xa7,0xaa,0x61,0x86,0x17,0x9a,0x98,0xea,0x0f,0x60,0x6b,0x40,0xb6, + 0xb3,0xef,0xe8,0x5a,0x25,0x83,0x5a,0xb5,0xd0,0x83,0xe5,0x26,0x3b,0x83,0x59,0x81, + 0x00,0x4d,0x8d,0xd0,0xfb,0x95,0x89,0x0c,0x00,0x22,0x43,0xe8,0xb7,0xe5,0x8d,0xc7, + 0x98,0x87,0x48,0x4e,0x90,0xa7,0x6c,0x50,0x9e,0x4d,0x34,0xb1,0x27,0x9e,0x13,0x88, + 0x24,0x7e,0xfd,0x0a,0x9c,0x60,0xc7,0xc0,0x10,0xf0,0xc2,0x22,0x3c,0xb9,0x6b,0x42, + 0x6b,0xec,0xa7,0xd9,0x5c,0xd4,0x54,0xfa,0xce,0xea,0xec,0xf1,0xe6,0x2f,0x12,0x6d, + 0xf2,0x99,0x97,0x82,0x89,0x4a,0x1e,0xcc,0x45,0x15,0xb6,0x4b,0x32,0x92,0x94,0x52, + 0x67,0x49,0xac,0x25,0xed,0xa7,0xa6,0x9b,0x51,0x97,0x51,0x32,0x62,0x7b,0x5f,0xde, + 0x3e,0xa5,0xd2,0x54,0x05,0x8a,0xcd,0xaf,0x99,0x56,0x73,0x93,0x4f,0x4f,0x81,0x6c, + 0xe5,0xdd,0x1f,0x0b,0x45,0x0b,0x5e,0x1c,0x27,0x94,0x64,0x74,0xd0,0x01,0xd1,0x23, + 0x7f,0x59,0x71,0xce,0x1d,0x3a,0x49,0x6a,0xa0,0x88,0xc7,0x7a,0x75,0x38,0xc0,0xe7, + 0xa3,0xd6,0xde,0xde,0xf1,0x6e,0x8f,0x1f,0xf7,0x9f,0x1e,0xef,0x88,0xc7,0xa7,0x07, + 0xc7,0xdd,0x5e,0x87,0x62,0x6e,0xa2,0x66,0x17,0xd1,0x5d,0x72,0x0d,0x8d,0xd0,0x71, + 0x52,0x4d,0x26,0xe9,0xfa,0x69,0x1c,0x38,0xb7,0x3d,0x4e,0x0b,0xdb,0x1a,0x62,0x02, + 0xc2,0xbe,0xc8,0xa5,0x17,0xdf,0xc8,0x04,0x83,0x09,0x27,0xc1,0x84,0x17,0xc3,0x28, + 0x81,0xcd,0x47,0x19,0x54,0x66,0x29,0xbd,0x11,0xd9,0xf6,0x40,0x67,0xde,0xd9,0xf2, + 0xbf,0xdd,0x01,0x99,0x05,0x8b,0x81,0x80,0x7e,0x49,0x5c,0x06,0x3f,0x40,0xf7,0xe1, + 0xb1,0xce,0x29,0xf1,0x80,0x5f,0x99,0x30,0x6b,0xfe,0x81,0x7d,0x9a,0x3a,0x09,0x3e, + 0x52,0xd0,0xe9,0x2b,0x7e,0xc0,0x2b,0x06,0x8a,0x19,0x62,0xfb,0xb4,0x3e,0x2d,0x1f, + 0xa8,0x7e,0xda,0xc3,0x17,0x2d,0xd0,0x4b,0xe5,0x78,0x30,0x85,0x27,0xf9,0x5f,0x6e, + 0xe9,0xa2,0x69,0xbd,0xaa,0x06,0xe3,0xca,0xcc,0x92,0x20,0x57,0x84,0xf1,0x79,0xad, + 0xb3,0x36,0x5c,0xad,0x50,0x1c,0xb6,0x29,0xa8,0x37,0x58,0x6e,0x45,0xa5,0xbe,0xd4, + 0xa5,0xa6,0x7d,0x51,0x31,0x9d,0xb2,0x77,0x1b,0xca,0xaa,0x89,0x2d,0xd3,0xe2,0x8a, + 0x73,0x22,0x3f,0x00,0x14,0x2c,0x1a,0x0d,0xf3,0x0c,0x05,0x21,0xf9,0x14,0x48,0x33, + 0x20,0xd6,0x6e,0x72,0x32,0x11,0x85,0x87,0xa4,0xed,0xb5,0x1c,0xfa,0x73,0xa7,0x86, + 0x00,0x78,0x5e,0x88,0xd6,0x73,0xea,0x9d,0x1a,0x69,0xe8,0x11,0xe5,0x76,0x28,0x77, + 0xc1,0x5d,0x7e,0x34,0xd5,0xd4,0x90,0x7d,0xf5,0x0b,0x20,0x9c,0xf8,0x86,0x67,0x48, + 0xea,0x81,0x4a,0xf3,0xc6,0xa3,0x0e,0x85,0x2c,0x4f,0xcf,0x25,0x69,0x3e,0xcc,0xa5, + 0x79,0x89,0x16,0x22,0x19,0x6f,0x39,0xf3,0xae,0x4c,0x03,0xea,0xe5,0x8e,0xc0,0x29, + 0x26,0x49,0x64,0x79,0x9a,0x07,0x26,0x04,0x6c,0xda,0x58,0x21,0xc1,0xc0,0xcc,0xcf, + 0xb7,0xb6,0x36,0xcb,0x3b,0x3a,0x95,0xa2,0xb8,0xac,0x9a,0x63,0xd1,0x16,0xbc,0xf2, + 0xc2,0xd1,0xb1,0xbe,0xf1,0xa4,0xd3,0x3d,0xe8,0x3e,0xdd,0xed,0xeb,0xa4,0xac,0xe9, + 0x87,0xe9,0x14,0x34,0x74,0x28,0xc0,0x6d,0x53,0x0d,0x7a,0x93,0x27,0xf5,0xd5,0xeb, + 0x12,0xfb,0x66,0x91,0x03,0xb2,0x96,0x38,0x32,0xa7,0x38,0x5d,0x44,0x65,0x10,0xed, + 0x25,0x52,0xc0,0xde,0x86,0x9d,0x65,0x63,0xca,0xc7,0xcd,0x65,0xa6,0xdc,0x55,0x74, + 0x6e,0xdd,0x2e,0x11,0x6b,0xc5,0x81,0x36,0x77,0x56,0x58,0x5d,0xd5,0xa6,0x7f,0xbf, + 0xe9,0x15,0xd1,0xbd,0xc1,0xf0,0x7a,0x0f,0x53,0x2e,0x09,0x2b,0x22,0xe4,0x54,0x58, + 0xcd,0x0b,0xbb,0x27,0xc5,0x78,0xf3,0x27,0x24,0x21,0x08,0x26,0x1a,0xac,0x41,0xb9, + 0xe5,0xe8,0x49,0x06,0x2c,0x2d,0x86,0xb3,0x21,0xc8,0x10,0x69,0x8f,0xd1,0x96,0xdd, + 0xdf,0x0a,0xd6,0xd4,0x68,0x18,0x15,0x11,0x32,0x65,0xb1,0x10,0xd8,0xf0,0x07,0xba, + 0x7e,0x0f,0xd8,0x2c,0xd9,0x7b,0x0b,0x07,0xb4,0xa1,0x73,0x45,0xb7,0xa0,0x01,0x9f, + 0x1e,0x39,0x98,0x93,0xb3,0xc8,0x18,0x7b,0xf2,0x9e,0x52,0x98,0x85,0x11,0xdf,0x49, + 0x04,0x20,0x21,0x07,0x8e,0xb3,0x68,0x06,0xcc,0xa3,0xa7,0x61,0x5a,0xdc,0xb4,0xb7, + 0x8d,0x34,0x04,0x8d,0xe5,0x93,0x11,0xf0,0x69,0x3b,0xfc,0x4c,0x34,0xe5,0xba,0x23, + 0xce,0x41,0x34,0x83,0xff,0x16,0x25,0xfc,0xc8,0xb4,0xb4,0x3f,0xce,0x02,0xf4,0x1e, + 0xd9,0xa7,0x06,0xc7,0x83,0x37,0xdf,0x7f,0x86,0x91,0xcc,0x07,0x57,0xdf,0x7f,0xe6, + 0x73,0x46,0xca,0x03,0x83,0xa7,0x8e,0xd0,0x3f,0x66,0xbc,0x10,0xa3,0x1e,0x5b,0xc3, + 0xb9,0x95,0x8e,0xad,0x51,0xf2,0xb3,0x4d,0xa9,0x41,0x4b,0x51,0x3c,0x83,0x0b,0x80, + 0xcf,0x22,0xeb,0xe9,0x3f,0x9c,0x9d,0x6c,0x9f,0x3a,0xa1,0xe3,0x3a,0x9a,0x81,0x67, + 0x86,0x53,0x58,0x21,0xd7,0x73,0x4d,0x40,0x80,0x9e,0xfe,0xac,0xd3,0xb6,0xf7,0xba, + 0x7b,0x3a,0x34,0xd5,0xd3,0xf7,0xbb,0x36,0x3c,0xa5,0xf0,0xfa,0xa9,0x0e,0xad,0xf6, + 0xf4,0x3d,0x4e,0xde,0x82,0xad,0x9c,0xcc,0x40,0xb2,0x07,0x8a,0xee,0x88,0x6a,0x7b, + 0xf6,0x41,0xbb,0xcd,0xd5,0xba,0x7b,0x6d,0xae,0xd5,0x69,0x2f,0xaf,0xa6,0x19,0x6f, + 0x9d,0x24,0x89,0xe6,0xb2,0xdb,0x7d,0x7b,0xef,0xe9,0xb2,0x6e,0x0f,0x1a,0xeb,0xbf, + 0xf1,0xf3,0x31,0xef,0xd9,0x6d,0x59,0xb9,0xd3,0x15,0x75,0x9f,0x2d,0xef,0xbb,0xa7, + 0x9d,0x9d,0x58,0xda,0x8f,0x27,0x5c,0xbb,0xbb,0x63,0x77,0xea,0x33,0x3e,0x58,0xde, + 0x75,0x4f,0xfb,0x97,0xd7,0xcf,0xd7,0xa8,0xab,0x74,0xfd,0x5d,0xe2,0x7c,0xf6,0x83, + 0x87,0xf5,0xf7,0xe2,0x87,0xed,0x1f,0xfe,0x54,0x06,0xd3,0xc1,0xfe,0x33,0x7b,0xbf, + 0x73,0xb0,0x76,0xdd,0xe7,0x1e,0x60,0x32,0x46,0xe4,0xb9,0x45,0xfd,0x7c,0x75,0x8b, + 0x65,0xea,0xd4,0xc6,0x7b,0xfa,0xd9,0x1b,0x4d,0x60,0x57,0x88,0x3b,0x14,0x6b,0x83, + 0xd8,0xdd,0xe9,0xde,0x8f,0x22,0x2f,0x7e,0xd0,0x76,0x77,0x76,0x00,0x7b,0x35,0xe3, + 0x75,0x14,0x5e,0x6a,0x1f,0xd0,0x4f,0x94,0xdb,0x80,0xf7,0x36,0x66,0xc6,0xbd,0x77, + 0x20,0x4a,0x1b,0xea,0x18,0x4a,0xf5,0x57,0x01,0xe2,0x2d,0x39,0x07,0xa2,0x3f,0x59, + 0xba,0x0a,0x82,0xf5,0xc1,0xbf,0x05,0x7a,0xf3,0x6f,0x20,0x78,0x43,0x4d,0x81,0x64, + 0x4f,0xed,0x9d,0xa7,0x6b,0x80,0x4e,0xa9,0x58,0xc5,0x71,0xa5,0x85,0x55,0x5d,0x63, + 0x68,0xc5,0xec,0x12,0x88,0x11,0xcc,0x32,0x9f,0x6d,0x43,0x4d,0x81,0xe1,0xfb,0x0d, + 0x35,0x0f,0xf6,0x0f,0xd6,0x98,0xae,0x52,0xf3,0x6c,0xee,0x67,0x9f,0x19,0x50,0x0f, + 0xc3,0xb4,0x3f,0xfb,0x5e,0x16,0x3a,0xd3,0xca,0x54,0xbb,0x6d,0xbb,0xbb,0x74,0x79, + 0xf6,0x1a,0x6a,0x57,0x31,0xb5,0xd4,0x42,0x13,0xb8,0x1f,0x7d,0x54,0x42,0x01,0xcb, + 0xf1,0x87,0x8b,0x22,0xa3,0xc0,0x8a,0x74,0x02,0xaa,0x42,0x23,0xc2,0x18,0x49,0xf8, + 0x42,0xad,0x2c,0x04,0x06,0x96,0x00,0xde,0x8f,0xd8,0xb1,0xb6,0xa7,0xf1,0x24,0x40, + 0x96,0xc4,0xbf,0x30,0x18,0x8d,0xc6,0x34,0xc0,0xab,0x8b,0xdb,0x76,0x5b,0x17,0x91, + 0x1e,0xce,0x97,0x2f,0x39,0x43,0xd4,0x72,0x3f,0x22,0xb6,0xc2,0x38,0x36,0x08,0xc4, + 0x3e,0x70,0x2f,0x4b,0x37,0xad,0x5b,0xe0,0x78,0xc5,0xcf,0xb2,0xf8,0xdf,0x06,0xf1, + 0x9f,0xa5,0x7f,0x68,0x52,0x24,0x0a,0xbd,0xb9,0xf0,0x3f,0x9a,0x8f,0x07,0x03,0xf1, + 0xf3,0x16,0x7f,0xd6,0xfa,0x51,0x84,0xb8,0xb2,0x81,0x90,0x80,0xf3,0x9c,0xa2,0xbb, + 0xca,0xb0,0xd1,0x70,0x5f,0xe5,0x09,0x11,0xbf,0xfb,0x51,0x82,0xaa,0x78,0x77,0xf6, + 0x52,0x42,0xad,0x78,0x77,0xfa,0xa1,0x06,0xc0,0xf9,0xe7,0x0f,0xc8,0x81,0x5e,0xfa, + 0x14,0x9b,0x98,0xbb,0x7f,0x07,0x6a,0xa8,0xaa,0x35,0x9a,0x25,0xe5,0x80,0xd7,0x2f, + 0x5f,0x58,0xf5,0xc9,0x83,0xe5,0xaa,0xb1,0x65,0x20,0xd2,0x70,0xc4,0x16,0xdf,0x5e, + 0x45,0x3c,0xba,0xc8,0xed,0xad,0x86,0x99,0x91,0xfd,0x0f,0x85,0x05,0x0c,0x5e,0x85, + 0xdf,0xeb,0xc4,0xaf,0xe6,0xbe,0x3f,0xcd,0x41,0xac,0x30,0x5c,0xd3,0xe4,0x36,0xfd, + 0x72,0x24,0x5b,0x35,0x90,0xce,0x57,0xc3,0xe7,0x32,0x10,0x5b,0xf3,0xfc,0xf2,0x9b, + 0x5b,0x25,0xe0,0xab,0x51,0x75,0x85,0xf5,0xac,0xb9,0x59,0x0a,0x82,0x3d,0x22,0x47, + 0x12,0x99,0xc3,0x4a,0xba,0xfa,0x94,0x1b,0x01,0x30,0xd7,0xc2,0x93,0xd8,0x45,0x1a, + 0xbb,0x26,0xf0,0xe5,0x06,0x16,0xf6,0xfd,0x11,0x17,0x4a,0xd3,0xdc,0x30,0x1a,0x1f, + 0x1d,0x5f,0xe6,0x12,0x4f,0xfa,0x9c,0x92,0x73,0x4e,0x87,0x53,0xa8,0x07,0xa3,0x53, + 0xdf,0x24,0x62,0xcf,0x15,0xb4,0x92,0x62,0xad,0xa3,0x41,0xdb,0x4c,0xab,0x3e,0x0f, + 0xf4,0x45,0x09,0x56,0x30,0xd8,0x99,0x63,0x63,0x83,0xff,0x8a,0x20,0x67,0x99,0x52, + 0x3a,0xc7,0x8c,0x16,0x4c,0xaf,0x9c,0xa9,0xf3,0x94,0xe7,0x1b,0xdc,0xf6,0x30,0xe5, + 0x3f,0x1b,0xe7,0x47,0x94,0x1d,0xc4,0x6c,0xc2,0x59,0xfe,0x3c,0x9c,0x9b,0x55,0xc4, + 0xe5,0x0f,0xe9,0xd8,0xac,0x62,0x2f,0x7f,0x18,0x25,0xa4,0x26,0x40,0x0f,0xb4,0xba, + 0x5b,0x3c,0x81,0xc3,0xf6,0xb1,0xae,0x19,0x6e,0xe4,0xd1,0xad,0x01,0xf4,0x0a,0x20, + 0x90,0x48,0xf9,0x90,0x81,0x64,0x92,0x0e,0x21,0xa1,0xbf,0x22,0xe9,0x46,0x7d,0x8f, + 0x00,0x46,0x97,0x02,0x49,0xaf,0x07,0x39,0x20,0x2d,0xc2,0xc4,0x7e,0x91,0xfb,0x41, + 0xd7,0xbf,0x7c,0xa1,0xbf,0x84,0x0c,0xe6,0x82,0xbe,0x97,0xf7,0xd0,0x9d,0xc8,0xdc, + 0xb2,0xe0,0x94,0x2c,0x25,0xbc,0xbf,0xd8,0xba,0xfe,0xd8,0xe7,0x3a,0x65,0xdc,0xbe, + 0x63,0x04,0x71,0xd1,0xf3,0x93,0x25,0x5a,0x8c,0xcc,0x77,0xb5,0x9f,0xa9,0x49,0x91, + 0x3f,0x6d,0x92,0x90,0xdf,0x35,0x3b,0x4a,0x4d,0xd1,0x98,0x97,0x4d,0x50,0xe0,0xa4, + 0x0b,0x56,0xdd,0x6b,0xbc,0x18,0xc3,0xd5,0x3c,0xd7,0xcf,0xa2,0xe4,0xd1,0x9a,0x5e, + 0x32,0x4a,0xf0,0x44,0x02,0x9a,0x2b,0x0d,0x4d,0x25,0x8b,0xbe,0x7b,0x33,0x58,0xe0, + 0x3a,0xf7,0xda,0x48,0xf7,0x3b,0x48,0xf3,0xbb,0x48,0xf0,0x77,0xee,0x2a,0x9e,0x34, + 0xd2,0xbd,0x4e,0x60,0x20,0xb5,0x77,0x01,0xd5,0x8b,0x0f,0x4b,0xe3,0xee,0xa9,0xd7, + 0x5a,0xec,0x7d,0xb1,0x1d,0xd3,0xdf,0x56,0xb9,0x1e,0xb4,0x4f,0x65,0x2b,0xe1,0x32, + 0x4a,0x5e,0xce,0x92,0xef,0x8d,0xc8,0x5d,0x59,0x77,0xbf,0x21,0xfb,0x86,0x42,0x5e, + 0xef,0x3f,0x0f,0xa2,0xcc,0xb4,0x61,0x1e,0x7d,0x58,0x9c,0xdd,0x6d,0x6c,0x60,0xa0, + 0xf9,0x8e,0x19,0x0e,0x76,0xc9,0xb7,0xa5,0x63,0x75,0xad,0x1d,0x6b,0xb7,0x29,0x4d, + 0xa6,0x58,0x2a,0xde,0xa3,0xfa,0x96,0xbf,0xdc,0x95,0xc5,0x07,0x60,0x84,0xaa,0xc9, + 0x20,0x8b,0x97,0x94,0x47,0xdf,0x1a,0xb4,0x2c,0x29,0xa0,0x86,0xf1,0xf1,0x90,0xf2, + 0x3c,0x12,0xef,0x13,0x0f,0x6d,0xfe,0xc5,0xa5,0xec,0x74,0xd5,0x60,0xee,0xf7,0x98, + 0xdf,0xae,0x4e,0x7a,0x10,0x39,0xc4,0xcf,0x27,0xa0,0xef,0x0a,0x77,0x64,0x0a,0xee, + 0x44,0xeb,0x73,0x9e,0xdf,0x3d,0xd4,0xbc,0x1b,0xf6,0x7d,0x97,0x89,0x05,0xd9,0x83, + 0xcf,0x09,0x67,0xb0,0x99,0x11,0x7b,0x29,0x67,0x09,0x79,0x42,0x0b,0xf7,0x35,0xcf, + 0x2d,0x12,0xbd,0x47,0x94,0x23,0x7a,0xfe,0x79,0x47,0xbb,0x68,0xba,0x74,0x3e,0xf7, + 0x69,0x25,0x53,0xd0,0x34,0x0f,0xac,0x11,0x89,0x72,0x25,0x92,0x88,0xb4,0xb9,0x7a, + 0x7e,0x9e,0x1a,0x6d,0x6c,0x3c,0x8e,0x18,0x75,0x61,0x51,0xa6,0xe8,0x83,0xcb,0x78, + 0x1c,0x4e,0xfb,0xd1,0x83,0xf5,0xde,0x3b,0x11,0xa8,0x2a,0xa0,0xb9,0x6b,0x52,0xc8, + 0x3a,0xa7,0x37,0xe6,0x43,0x09,0x71,0xa2,0x97,0x8e,0x30,0x4f,0xfd,0x79,0x84,0x87, + 0x6a,0xd5,0xa4,0xeb,0xf1,0xbb,0x99,0xc8,0x6a,0x82,0x5e,0x93,0xc0,0x62,0x0b,0x78, + 0x63,0x20,0x0a,0x5d,0x15,0xea,0x84,0xe9,0x9c,0xdd,0xfb,0x28,0xcb,0xbf,0xb8,0x4e, + 0x28,0x89,0x86,0xc2,0x07,0x50,0xd3,0x45,0x68,0x8f,0x2e,0x0e,0x00,0x44,0x92,0x6e, + 0xb2,0xfa,0x85,0x4a,0xc6,0xfe,0xa1,0x87,0x11,0x09,0x53,0x1f,0xa3,0x1e,0x72,0xef, + 0x41,0x34,0x47,0xc7,0x32,0xe9,0x0b,0x9e,0x9e,0x8b,0xf0,0xaf,0xb9,0x13,0x5c,0x15, + 0x8d,0x61,0xb4,0x66,0x4e,0x9b,0x12,0x0f,0x0f,0x45,0x64,0x1d,0x8c,0xc7,0x74,0x02, + 0x71,0x81,0x8b,0x9f,0xb0,0xd9,0x5c,0xdc,0xe3,0x62,0x97,0x9c,0x67,0xb8,0xe4,0x36, + 0xde,0xa7,0x52,0xcd,0x63,0xba,0xd2,0x7d,0x1f,0xd3,0xde,0xa3,0xd5,0x11,0x11,0x3d, + 0xbf,0x00,0xa7,0xec,0x0d,0x43,0x05,0x8a,0xed,0x20,0xef,0x4f,0x32,0x97,0x5c,0x29, + 0xb1,0xc4,0x1a,0x24,0x9c,0xe9,0x4e,0xa3,0x59,0xe0,0x22,0x1b,0x82,0xa9,0x8c,0x94, + 0x1d,0x40,0x1e,0xa5,0xc9,0xad,0xe6,0x50,0x76,0x43,0xb3,0x42,0x00,0x4a,0xcb,0x2f, + 0xd9,0x0c,0x82,0x61,0x70,0xf1,0xd1,0x72,0x73,0x24,0x2d,0x05,0x65,0xc0,0x67,0x83, + 0x32,0x87,0x61,0x39,0x3e,0x46,0x6e,0x3a,0x38,0xdf,0xdc,0x0a,0x90,0x65,0xd2,0xf1, + 0x38,0xb3,0xd1,0x6b,0xc1,0x43,0xd9,0x8c,0xce,0x81,0x12,0xd8,0x98,0xae,0xa6,0x11, + 0xd7,0x2d,0xf7,0x42,0x89,0xa8,0xfa,0x98,0x6f,0x8d,0xca,0x5b,0xdd,0xc0,0xec,0x41, + 0xc4,0x5f,0xd9,0xa1,0x2c,0x6f,0xa8,0x88,0x73,0x97,0x2d,0x61,0x08,0xd6,0xc7,0x63, + 0xfd,0xef,0x7f,0xfb,0x5f,0x95,0xff,0xeb,0x3d,0xa3,0xdc,0xba,0x28,0x69,0xc0,0x5c, + 0x29,0x52,0xd1,0x45,0xee,0x6d,0x90,0x3f,0xb4,0x18,0x9f,0xa9,0xe7,0x49,0xf0,0xdd, + 0x86,0xbd,0x6b,0xd2,0x38,0xde,0x4a,0xda,0x03,0x63,0x68,0x2a,0x24,0x05,0x9f,0x64, + 0x34,0x70,0xa5,0x7c,0x5b,0x13,0x77,0xad,0x24,0xe6,0xa8,0xea,0x35,0x64,0xd4,0x55, + 0x19,0x56,0x92,0x11,0xc8,0xff,0xb1,0x8c,0xb3,0xa6,0xf1,0x7d,0x60,0xae,0x63,0x24, + 0xf1,0x71,0x02,0x62,0x28,0xc8,0x3c,0x88,0x26,0x6c,0xec,0x4c,0x46,0x0a,0x9b,0x15, + 0xd6,0x4c,0xf4,0x9b,0x30,0xb7,0x0c,0x31,0x56,0x76,0xab,0x50,0x61,0xa4,0x80,0x24, + 0xbb,0x61,0x10,0x9c,0xff,0xa4,0x51,0x12,0x45,0x84,0x40,0x76,0x03,0x3d,0x90,0x33, + 0x4b,0xfd,0xfa,0x00,0xde,0x31,0x54,0xe5,0x9d,0x4a,0xc8,0x69,0xf5,0x4a,0x04,0x54, + 0xc1,0x84,0xca,0xfb,0x2a,0x2e,0x88,0xf6,0x5e,0x9d,0x9c,0x9f,0xc0,0x24,0x65,0x3b, + 0x94,0x98,0xb1,0xd6,0x8a,0x7c,0xab,0xb6,0x01,0x7c,0xe8,0x87,0x38,0xf6,0x92,0x53, + 0x10,0x21,0x0c,0x53,0xa1,0xdb,0x78,0xf7,0x5b,0x3e,0x2c,0xca,0x54,0x58,0x1f,0x15, + 0xbf,0x2e,0xe8,0xf7,0x3c,0x64,0x80,0xbc,0xc3,0x0f,0x28,0x2e,0xe9,0x16,0xbc,0x93, + 0x1e,0x17,0x9d,0xee,0x31,0xfe,0xa2,0x4b,0x6a,0x80,0xd6,0x76,0xba,0xb0,0x47,0xd0, + 0xa5,0xae,0x87,0xf5,0x8a,0x8e,0xbd,0x69,0xde,0x2f,0x67,0x40,0xac,0xf5,0x2b,0x5e, + 0xe7,0xfd,0x7a,0x53,0xb5,0x5b,0xfe,0x6a,0xc1,0x4b,0x2e,0xb0,0x46,0x92,0x11,0x99, + 0xb9,0x4f,0xf4,0x0b,0x9c,0x7a,0x4b,0xcf,0xd3,0x02,0x54,0x7a,0x2f,0x7f,0x54,0x62, + 0x37,0xe2,0x8d,0x8d,0xf8,0xb1,0x8c,0x4e,0xe5,0x01,0x91,0x43,0x0d,0x54,0xb0,0xe2, + 0x52,0xba,0x1e,0x31,0x4f,0x8c,0xad,0x2c,0x25,0xe5,0x71,0xeb,0x47,0x24,0x57,0x52, + 0x33,0x7d,0xbc,0xfd,0x57,0x03,0xb7,0xed,0x5f,0xec,0x2f,0x88,0x32,0xdf,0x7c,0x21, + 0xf4,0xfc,0xe6,0x4b,0x76,0xf3,0xcd,0x17,0x1c,0xd6,0x5f,0x04,0x8e,0x88,0x1f,0xb8, + 0xd4,0xf2,0x3d,0x42,0x45,0x3c,0x13,0x6c,0xe8,0x99,0xa2,0xcb,0xff,0x62,0x9b,0x22, + 0xca,0xf6,0x0a,0xf3,0x10,0x17,0xa9,0x80,0x10,0xa8,0x38,0x3c,0x01,0x57,0x0a,0x24, + 0x95,0xc9,0x77,0x2c,0xfa,0x52,0x8a,0xf7,0x28,0xdd,0xad,0xc0,0x77,0x04,0x94,0x54, + 0x17,0xa2,0x9e,0xf2,0xb0,0xa0,0x2c,0x95,0x91,0x80,0x87,0x1e,0xdd,0x86,0x2a,0x9a, + 0x71,0x1c,0xae,0x4a,0x05,0x41,0x34,0x68,0x22,0x8e,0x7c,0x42,0x00,0xbd,0x36,0x24, + 0x72,0x7e,0x1f,0x78,0x4e,0x4a,0x66,0x69,0x10,0x6d,0x4a,0xd7,0x37,0xc0,0xa6,0x04, + 0xfd,0xb1,0x63,0xda,0xaa,0x3b,0x27,0x74,0xad,0xa8,0xe5,0x42,0xf2,0x40,0x65,0x63, + 0x8d,0x3e,0xc4,0x0d,0x75,0x4e,0xc9,0x4e,0x2e,0xba,0xe9,0x72,0x60,0x25,0xdf,0xe5, + 0x90,0x62,0xe8,0x39,0x09,0x05,0x80,0x26,0xad,0x1f,0xce,0x34,0x71,0xa9,0x6f,0x5a, + 0x1d,0x8a,0x2a,0x9f,0xc2,0xda,0x5f,0x9c,0xb4,0xfe,0xcd,0x69,0x7d,0xfe,0xb8,0xd8, + 0xb9,0x93,0x81,0xd1,0x05,0x98,0xd6,0xd9,0xf2,0xba,0x59,0x12,0x5e,0x57,0xcc,0x85, + 0xce,0x89,0x61,0x2a,0x3b,0x2d,0xbe,0x45,0x56,0x43,0xda,0x22,0x34,0x3d,0x90,0x16, + 0x81,0xda,0x1b,0x7c,0x2f,0x2a,0x4c,0xae,0x0c,0xc1,0xe5,0x4d,0xe7,0xf6,0x8c,0xa1, + 0x34,0x7d,0x88,0x18,0xde,0x61,0x3d,0x58,0x57,0x8d,0x9d,0x5f,0x70,0x44,0x0d,0x8b, + 0x74,0xc5,0x41,0x4c,0xa2,0xc4,0x57,0x6a,0xda,0x70,0x59,0x94,0xe4,0xe3,0x24,0xdf, + 0xdf,0xe8,0x5e,0xa2,0x54,0x51,0x22,0x6c,0x59,0xf7,0x07,0xf9,0x2c,0xa6,0x2b,0xf5, + 0xf2,0x38,0x54,0x03,0xef,0xef,0xe1,0xf0,0x49,0x29,0x8a,0x89,0x58,0x5b,0x8c,0xe0, + 0x98,0x7b,0xe2,0x0a,0x15,0xb5,0x39,0xb4,0x17,0xe4,0x17,0xbb,0x70,0x38,0x2a,0xfb, + 0x84,0x20,0x9b,0x86,0xc6,0x4f,0x5f,0xbf,0x42,0x33,0x86,0xef,0x52,0x20,0xa6,0x9d, + 0x57,0x6d,0x0c,0x71,0x2a,0x0d,0x49,0x9a,0x38,0x28,0xd2,0x46,0x44,0xc6,0x52,0xc6, + 0x6e,0x1c,0x9e,0xb8,0x2e,0xbd,0x88,0xf0,0xa4,0x48,0xb1,0x3c,0xce,0xd3,0xd6,0x5e, + 0xc9,0xe0,0x58,0xd1,0x8c,0x18,0x8f,0x45,0x13,0x12,0x57,0xf6,0xe1,0x18,0x52,0x40, + 0xd4,0x7f,0x67,0x15,0x8b,0x03,0x9b,0x1e,0xff,0xfd,0x6f,0xff,0x41,0x51,0x52,0x7e, + 0x26,0x62,0x9b,0x58,0xa2,0x05,0x59,0x72,0xa7,0x2d,0x32,0x04,0xa6,0xd4,0x7e,0xee, + 0xc1,0xc2,0xd1,0xdf,0xb3,0x58,0xc3,0xbb,0x17,0xac,0x22,0xe4,0x1c,0xc5,0xe5,0xf2, + 0x2d,0x85,0xd4,0xb0,0xc8,0x55,0x25,0x02,0x7e,0x44,0xd0,0x18,0x01,0xcd,0x16,0xe9, + 0x44,0x64,0x44,0x63,0x25,0xee,0x0a,0x0d,0x3f,0xb8,0xad,0x38,0x6c,0x35,0x97,0x01, + 0x8b,0xc0,0xab,0xc6,0x60,0xb2,0x72,0x3c,0x56,0x11,0xbf,0xb7,0x1c,0x73,0x97,0xc9, + 0x9a,0x22,0x0c,0xdd,0xd6,0x4e,0x29,0x3c,0x56,0xb9,0x37,0x52,0xcb,0xc3,0x8d,0xc5, + 0xc5,0x2d,0x95,0x69,0x2b,0x32,0x3c,0x45,0xd1,0xda,0x7a,0x63,0xac,0x58,0x25,0x18, + 0xb3,0x16,0x73,0x59,0x61,0x14,0x37,0x0a,0xa3,0x28,0x42,0x2d,0x6f,0x44,0x48,0xe4, + 0x5d,0xe1,0xb4,0x4a,0xcd,0x55,0x13,0xdc,0x2d,0x9d,0x3f,0x97,0xae,0xfa,0xa5,0x0a, + 0x63,0xe9,0x0d,0xc5,0x30,0xea,0x3d,0x72,0x34,0xa5,0x8e,0xf2,0x33,0x61,0x72,0x1f, + 0x35,0x97,0x4f,0x6c,0x59,0x54,0x1f,0xd6,0xe6,0xe0,0xb0,0x7f,0xd7,0xb7,0x8c,0x46, + 0x4e,0xd0,0x2c,0x25,0xe7,0x79,0x8a,0x40,0xa4,0xf8,0x0f,0x35,0xe8,0x8f,0x6c,0xa4, + 0x22,0x70,0x0d,0x13,0x92,0xc4,0x19,0xfa,0x4d,0x89,0x4b,0x7d,0xc2,0x08,0xf6,0x0c, + 0x87,0xa7,0xd9,0xda,0x4b,0x4c,0xaa,0x2f,0x17,0x78,0x33,0xd5,0x5e,0xbd,0xcf,0x2f, + 0xf0,0xe1,0x80,0x40,0x0b,0x5b,0x48,0x66,0x21,0x8e,0x0e,0xfd,0xc2,0xe6,0x1e,0xe8, + 0x14,0xb8,0x45,0x28,0x2b,0x26,0x6e,0x26,0xdc,0xe1,0x88,0x19,0x9e,0x27,0xec,0x41, + 0x32,0x6e,0xf0,0xd1,0x12,0x34,0x6e,0x0e,0x1f,0xc4,0x5d,0x24,0xe3,0xe9,0x96,0x84, + 0x07,0x36,0xc6,0x38,0x89,0xc8,0xf8,0x65,0xde,0x7d,0xf9,0x9d,0xea,0x92,0xd3,0x0a, + 0xaa,0x81,0x29,0xf1,0xa8,0x62,0x71,0xeb,0x11,0x50,0xb2,0x63,0xdd,0x54,0x0e,0xb1, + 0x95,0x14,0x0a,0xdc,0x4b,0xf3,0x45,0x07,0x8f,0xd6,0x0b,0xd9,0x14,0x51,0xf9,0x74, + 0x63,0x12,0x05,0xfc,0xa3,0xda,0x46,0x91,0x7d,0x72,0xd9,0x30,0x6d,0x1d,0x92,0x33, + 0x0c,0xd5,0x87,0x2d,0xc2,0x21,0xde,0x4b,0x23,0x20,0x57,0xc6,0x5e,0x89,0xc9,0xb1, + 0x07,0x00,0x22,0x6b,0x5e,0xb4,0xaa,0x22,0x2a,0x03,0x9f,0xa6,0x97,0x56,0x3e,0x1a, + 0x8b,0xee,0x4d,0xca,0x1d,0xaf,0x4a,0xd7,0x6b,0x96,0xf7,0x0b,0xbd,0x03,0x76,0x9b, + 0xdf,0xb9,0x49,0x31,0x23,0xe5,0x7a,0x78,0xcd,0xa6,0xd9,0x94,0x6b,0x46,0x29,0xb3, + 0x5a,0x37,0x66,0x4f,0xaa,0x9d,0x76,0x5f,0xa9,0xc1,0x17,0x68,0x56,0x92,0x28,0xca, + 0xc2,0x59,0x29,0x5a,0xa7,0xba,0x5e,0x61,0xab,0x75,0x6f,0x4b,0x47,0xed,0xe3,0xb0, + 0xa7,0xd8,0x6a,0xc2,0xc3,0xc1,0x1e,0xe8,0xfd,0x12,0x42,0x39,0x25,0x51,0x6f,0x44, + 0x11,0xb9,0xdf,0xad,0x05,0x5d,0x7d,0xd8,0x2b,0xee,0x44,0x6c,0xb2,0x26,0x90,0x6f, + 0x52,0x74,0x55,0x8d,0x09,0xca,0xcc,0x3e,0x26,0x73,0x21,0x86,0xc9,0xc9,0x0a,0x0c, + 0xf3,0x6e,0x99,0xfb,0x77,0x41,0x5f,0x78,0x84,0xed,0x86,0xd6,0x14,0x8a,0x57,0x99, + 0xaf,0x22,0xaf,0xca,0x69,0x1d,0x6f,0xe6,0x57,0xd1,0x6e,0xe3,0x45,0xaa,0xd8,0x3f, + 0xde,0x0f,0xbb,0xd9,0xd3,0xff,0x35,0x9a,0x91,0xd1,0x86,0x5c,0x2b,0x94,0xdc,0x2e, + 0xaa,0x97,0xba,0x45,0x41,0x37,0xb4,0x51,0x79,0xe3,0xf5,0xf1,0x52,0x77,0x71,0x97, + 0xe9,0xe1,0x36,0xba,0xb7,0xe2,0x5f,0x3c,0x9c,0x38,0x7a,0xf4,0x7f,0x01,0x36,0xd0, + 0xf3,0xe7,0x28,0xdc,0x00,0x00, +}; diff --git a/src/helpers/esp32/WebConfigServer.cpp b/src/helpers/esp32/WebConfigServer.cpp new file mode 100644 index 00000000..cc7e24cc --- /dev/null +++ b/src/helpers/esp32/WebConfigServer.cpp @@ -0,0 +1,1039 @@ +#include "WebConfigServer.h" + +#ifdef WITH_WEBCONFIG + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WITH_MQTT_BRIDGE + #include + #include + #include +#endif + +#include "WebConfigHtml.h" + +// Placeholder sent instead of stored secrets; POSTs carrying it are dropped +// so an untouched password field never overwrites the stored value. +static const char SECRET_SENTINEL[] = "********"; + +// Keys the web UI may drive through the CLI `set` handlers. Everything else +// is rejected, so a crafted request can't reach arbitrary commands (`erase`, +// `password`, ...) through the batch. +static const char* const ALLOWED_SET_KEYS[] = { + // NodePrefs (radio / node) + "name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay", + "cad", "radio.rxgain", "repeat", "advert.interval", "flood.advert.interval", + "flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect", + // MQTTPrefs (WiFi / MQTT / misc observer) + "wifi.ssid", "wifi.pwd", "wifi.powersave", + "mqtt.origin", "mqtt.iata", "mqtt.status", "mqtt.packets", "mqtt.raw", + "mqtt.tx", "mqtt.rx", "mqtt.interval", "mqtt.ntp", "mqtt.owner", "mqtt.email", + "timezone", "timezone.offset", "snmp", "snmp.community", +}; +static const char* const ALLOWED_SLOT_KEYS[] = { + "preset", "server", "port", "username", "password", "token", "topic", "audience", +}; + +static bool isAllowedSetKey(const char* key, bool has_mqtt) { + if (!key) return false; + for (size_t i = 0; i < sizeof(ALLOWED_SET_KEYS) / sizeof(ALLOWED_SET_KEYS[0]); i++) { + if (strcmp(key, ALLOWED_SET_KEYS[i]) == 0) { + const bool mqtt_only = strncmp(key, "mqtt.", 5) == 0 + || strncmp(key, "timezone", 8) == 0 + || strncmp(key, "snmp", 4) == 0; + return has_mqtt || !mqtt_only; + } + } + // mqtt<1-6>. +#ifdef WITH_MQTT_BRIDGE + if (!has_mqtt) return false; + if (strlen(key) > 6 && strncmp(key, "mqtt", 4) == 0 + && key[4] >= '1' && key[4] <= ('0' + MAX_MQTT_SLOTS) + && key[5] == '.') { + for (size_t i = 0; i < sizeof(ALLOWED_SLOT_KEYS) / sizeof(ALLOWED_SLOT_KEYS[0]); i++) { + if (strcmp(&key[6], ALLOWED_SLOT_KEYS[i]) == 0) return true; + } + } +#else + (void)has_mqtt; +#endif + return false; +} + +static bool isSecretKey(const char* key) { + if (!key) return false; + if (strcmp(key, "wifi.pwd") == 0) return true; + if (strlen(key) > 6 && strncmp(key, "mqtt", 4) == 0 && key[5] == '.' + && (strcmp(&key[6], "password") == 0 || strcmp(&key[6], "token") == 0)) return true; + return false; +} + +// Constant-time-ish comparison so login timing doesn't leak a prefix match. +static bool fixedTimeEquals(const char* a, const char* b, size_t max_len) { + size_t la = strnlen(a, max_len), lb = strnlen(b, max_len); + uint8_t diff = (la == lb) ? 0 : 1; + for (size_t i = 0; i < max_len; i++) { + char ca = (i < la) ? a[i] : 0; + char cb = (i < lb) ? b[i] : 0; + diff |= (uint8_t)(ca ^ cb); + } + return diff == 0; +} + +// RAII lock; tolerates a null mutex (allocation failure) by not locking. +struct WCLock { + SemaphoreHandle_t h; + explicit WCLock(SemaphoreHandle_t s) : h(s) { if (h) xSemaphoreTake(h, portMAX_DELAY); } + ~WCLock() { if (h) xSemaphoreGive(h); } +}; + +WebConfigServer* WebConfigServer::_active = NULL; +static volatile bool webconfig_button_toggle_requested = false; + +WebConfigServer::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) + : _cb(callbacks), _mqtt_prefs(mqtt_prefs), _owns_wifi(owns_wifi), _pub_key(pub_key), + _fw_ver(fw_ver), _role(role), _board_name(board_name) { + _mux = xSemaphoreCreateMutex(); + _active = this; + +#ifdef WITH_MQTT_BRIDGE + MQTTPrefs* obs = static_cast(_mqtt_prefs); + if (obs) { + strncpy(_wifi_ssid, obs->wifi_ssid, sizeof(_wifi_ssid) - 1); + strncpy(_wifi_password, obs->wifi_password, sizeof(_wifi_password) - 1); + _wifi_power_save = obs->wifi_power_save <= 2 ? obs->wifi_power_save : 1; + // Companion builds keep their canonical connection credentials in the + // shared mesh-wifi namespace. A fresh MQTT configuration can therefore + // have an empty MQTTPrefs WiFi field even while the companion is already + // connected; use the canonical copy until the first WebUI save syncs both. + if (_wifi_ssid[0] == 0 && !_owns_wifi) { + loadStandaloneWiFi(_wifi_ssid, sizeof(_wifi_ssid), + _wifi_password, sizeof(_wifi_password), &_wifi_power_save); + } + } else +#endif + { + loadStandaloneWiFi(_wifi_ssid, sizeof(_wifi_ssid), + _wifi_password, sizeof(_wifi_password), &_wifi_power_save); + } +} + +WebConfigServer::~WebConfigServer() { + if (_active == this) _active = NULL; + if (_mux) vSemaphoreDelete(_mux); +} + +bool WebConfigServer::loadEnabled(bool default_value) { + Preferences nvs; + if (!nvs.begin("mesh-webui", true)) return default_value; + bool enabled = nvs.getBool("enabled", default_value); + nvs.end(); + return enabled; +} + +bool WebConfigServer::saveEnabled(bool enabled) { + Preferences nvs; + if (!nvs.begin("mesh-webui", false)) return false; + size_t written = nvs.putBool("enabled", enabled); + nvs.end(); + return written == sizeof(uint8_t); +} + +bool WebConfigServer::loadStandaloneWiFi(char* ssid, size_t ssid_len, + char* password, size_t password_len, + uint8_t* power_save) { + if (!ssid || ssid_len == 0 || !password || password_len == 0) return false; + ssid[0] = 0; + password[0] = 0; + Preferences nvs; + if (!nvs.begin("mesh-wifi", true)) return false; + String stored_ssid = nvs.getString("ssid", ""); + String stored_password = nvs.getString("password", ""); + uint8_t stored_ps = nvs.getUChar("powersave", 1); + nvs.end(); + if (stored_ssid.length() == 0 || stored_ssid.length() >= ssid_len + || stored_password.length() >= password_len) return false; + strncpy(ssid, stored_ssid.c_str(), ssid_len - 1); + ssid[ssid_len - 1] = 0; + strncpy(password, stored_password.c_str(), password_len - 1); + password[password_len - 1] = 0; + if (power_save) *power_save = stored_ps <= 2 ? stored_ps : 1; + return true; +} + +bool WebConfigServer::saveStandaloneWiFi(const char* ssid, const char* password, + uint8_t power_save) { + if (!ssid || !ssid[0] || strlen(ssid) >= 32 + || (password && strlen(password) >= 64) || power_save > 2) { + return false; + } + Preferences nvs; + if (!nvs.begin("mesh-wifi", false)) return false; + const char* pwd = password ? password : ""; + bool ok = nvs.putString("ssid", ssid) == strlen(ssid); + nvs.putString("password", pwd); // empty String legitimately writes zero bytes + ok = ok && nvs.getString("password", "\x01") == pwd; + ok = ok && nvs.putUChar("powersave", power_save) == sizeof(uint8_t); + nvs.end(); + return ok; +} + +void WebConfigServer::requestToggleFromButton() { + webconfig_button_toggle_requested = true; +} + +bool WebConfigServer::takeButtonToggleRequest() { + if (!webconfig_button_toggle_requested) return false; + webconfig_button_toggle_requested = false; + return true; +} + +bool WebConfigServer::isRebootPending() { + WebConfigServer* w = _active; + return w != NULL && w->_reboot_at != 0 && w->_batch_reboot && + w->_batch_state == BATCH_DONE; +} + +bool WebConfigServer::getSetupInfo(char* ssid, size_t ssid_len, char* ip, size_t ip_len) { + WebConfigServer* w = _active; + if (w == NULL || w->_mode != MODE_SETUP || w->_stopping) return false; + if (ssid && ssid_len > 0) { + strncpy(ssid, w->_ap_ssid, ssid_len - 1); + ssid[ssid_len - 1] = 0; + } + if (ip && ip_len > 0) { + snprintf(ip, ip_len, "%s", WiFi.softAPIP().toString().c_str()); + } + return true; +} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +bool WebConfigServer::startSetupMode(char reply[]) { + const bool promote_lan = _mode == MODE_LAN && !_owns_wifi && !_stopping; + if ((_mode != MODE_OFF && !promote_lan) || _stopping) { + strcpy(reply, "Err: webconfig busy"); + return false; + } + // AP_STA (not pure AP) so the WiFi scan for the SSID picker works while + // the AP is up. STA stays unconnected - the bridge won't touch WiFi + // while wifi_ssid is empty, and `start webconfig ap` requires it stopped. + WiFi.mode(WIFI_AP_STA); + snprintf(_ap_ssid, sizeof(_ap_ssid), "MeshCore-Setup-%02X%02X", _pub_key[0], _pub_key[1]); +#ifdef WEBCONFIG_AP_PASSWORD + bool ap_ok = WiFi.softAP(_ap_ssid, WEBCONFIG_AP_PASSWORD); +#else + bool ap_ok = WiFi.softAP(_ap_ssid); +#endif + if (!ap_ok) { + if (!promote_lan) WiFi.mode(WIFI_OFF); + strcpy(reply, "Err: failed to start AP"); + return false; + } + delay(100); // let the AP netif settle before reading its IP + IPAddress ip = WiFi.softAPIP(); + + _dns = new DNSServer(); + _dns->start(53, "*", ip); // captive portal: every name resolves to us + + if (!promote_lan) createServer(); + _mode = MODE_SETUP; + _was_setup_ap = true; + _last_activity = millis(); + WiFi.scanNetworks(true); // pre-populate the SSID picker + + sprintf(reply, "WebConfig AP started: join '%s' then open http://%s/", _ap_ssid, ip.toString().c_str()); + return true; +} + +bool WebConfigServer::startLanMode(char reply[]) { + if (_mode != MODE_OFF || _stopping) { + strcpy(reply, "Err: webconfig busy"); + return false; + } + if (WiFi.status() != WL_CONNECTED) { + strcpy(reply, "Err: WiFi not connected"); + return false; + } + createServer(); + _mode = MODE_LAN; + _last_activity = millis(); + + NodeSnapshot node = {}; + _cb->getNodeSnapshot(node); + int pos = snprintf(reply, 160, "WebConfig started: http://%s/%s", + WiFi.localIP().toString().c_str(), + node.admin_password[0] ? " (admin password login)" : ""); + if (heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) < 60 * 1024) { + sprintf(reply + pos, " WARN: low heap"); + } + return true; +} + +bool WebConfigServer::startAutoMode(char reply[]) { + if (_wifi_ssid[0] == 0) return startSetupMode(reply); + if (WiFi.status() == WL_CONNECTED) return startLanMode(reply); + if (_mode != MODE_OFF || _stopping) { + strcpy(reply, "Err: webconfig busy"); + return false; + } + + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(true); + WiFi.begin(_wifi_ssid, _wifi_password); + wifi_ps_type_t ps_mode = _wifi_power_save == 1 ? WIFI_PS_NONE + : _wifi_power_save == 2 ? WIFI_PS_MAX_MODEM + : WIFI_PS_MIN_MODEM; + esp_wifi_set_ps(ps_mode); + _mode = MODE_CONNECTING; + _connect_deadline = millis() + 15000; + if (_connect_deadline == 0) _connect_deadline = 1; + snprintf(reply, 160, "WebConfig connecting to '%s'; use 'get webui' for its IP", _wifi_ssid); + return true; +} + +void WebConfigServer::createServer() { + _server = new AsyncWebServer(80); + // iOS caches plain-HTTP GETs aggressively (keyed by URL, surviving even a + // device reflash behind the same IP), which poisons /api/config/result and + // friends with stale responses from earlier sessions. Forbid caching on + // every response; the HTML is small enough to refetch per visit. + // DefaultHeaders is process-global and survives a stop/start cycle. Adding + // this on every start would retain duplicate header nodes indefinitely. + static bool cache_header_added = false; + if (!cache_header_added) { + DefaultHeaders::Instance().addHeader("Cache-Control", "no-store"); + cache_header_added = true; + } + registerRoutes(); + _server->begin(); +} + +void WebConfigServer::requestStop() { + if (_mode == MODE_OFF && !_stopping) return; + if (_server) _server->end(); + if (_dns) _dns->stop(); + _mode = MODE_OFF; + _stopping = true; + // Deleting an AsyncWebServer with live connections is a known crash source; + // give in-flight responses a grace period before freeing. + _delete_at = millis() + 2000; + if (_delete_at == 0) _delete_at = 1; +} + +void WebConfigServer::finalizeTeardown() { + delete _server; + _server = NULL; + delete _dns; + _dns = NULL; + const bool was_setup_ap = _was_setup_ap; + if (was_setup_ap) { + WiFi.softAPdisconnect(true); + // Nothing else owns WiFi when we raised the AP: either the node is + // unconfigured, or `start webconfig ap` required the bridge stopped. + if (_wifi_ssid[0] == 0 || _owns_wifi) { + WiFi.mode(WIFI_OFF); + } else { + WiFi.mode(WIFI_STA); + } + _was_setup_ap = false; + } + if (_owns_wifi && !was_setup_ap) { + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + } + _stopping = false; + _delete_at = 0; + _connect_deadline = 0; + _reboot_at = 0; + _batch_state = BATCH_IDLE; + _batch_next = 0; + _batch_reboot_armed = false; + _session_token[0] = 0; + _stats_json[0] = 0; + if (_cb) _cb->onWebConfigStopped(); +} + +void WebConfigServer::tick(uint32_t now) { + if (_stopping) { + if (_delete_at && (int32_t)(now - _delete_at) >= 0) finalizeTeardown(); + return; + } + if (_mode == MODE_OFF) return; + + if (_mode == MODE_CONNECTING) { + if (WiFi.status() == WL_CONNECTED) { + createServer(); + _mode = MODE_LAN; + _connect_deadline = 0; + _last_activity = now; + Serial.printf("WebConfig ready: http://%s/\n", WiFi.localIP().toString().c_str()); + } else if (_connect_deadline && (int32_t)(now - _connect_deadline) >= 0) { + Serial.printf("WebConfig: WiFi '%s' unavailable; opening setup AP\n", _wifi_ssid); + WiFi.disconnect(true); + _mode = MODE_OFF; + _connect_deadline = 0; + char ignored[160]; + startSetupMode(ignored); + Serial.println(ignored); + } + return; + } + + if (_dns) _dns->processNextRequest(); + + if (_batch_state == BATCH_PENDING) drainBatch(now); + + if (_reboot_at && (int32_t)(now - _reboot_at) >= 0) { + Serial.printf("WC: rebooting now (%s)\n", _batch_reboot_armed ? "confirmed" : "fallback"); + _cb->rebootNow(); // does not return + } + + if ((int32_t)(_diag_until - now) > 0 && (now - _diag_last) >= 1000) { + _diag_last = now; + Serial.printf("WC: diag sta=%d heap=%u batch=%d/%d state=%d\n", + (int)WiFi.softAPgetStationNum(), (unsigned)ESP.getFreeHeap(), + (int)_batch_next, (int)_batch_count, (int)_batch_state); + } + + // Refresh the stats snapshot only while a client is actually polling. + if ((int32_t)(_stats_wanted_until - now) > 0 && (now - _stats_built_at) >= 2000) { + WCLock lock(_mux); + _cb->buildStatsJson(_stats_json, sizeof(_stats_json)); + _stats_built_at = now; + } + + // Idle timeout: only the setup AP auto-stops (a deployed node must not be + // left broadcasting an open AP). LAN mode runs until `stop webconfig`. + if (_mode == MODE_SETUP && WiFi.softAPgetStationNum() == 0 && + (now - _last_activity) > WEBCONFIG_AP_IDLE_TIMEOUT_MS) { + requestStop(); + } +} + +void WebConfigServer::drainBatch(uint32_t now) { + // One command per call, spaced out. Each `set` persists prefs with a flash + // write, and flash writes stall the WiFi task (flash cache off); running a + // whole batch back-to-back starves the softAP of beacons long enough for + // clients (iPhones especially) to drop off mid-save. + if (_batch_next == 0) { + WCLock prefs_lock(_mux); + _cb->onConfigBatchStart(); + } else if (_batch_next < _batch_count && (int32_t)(now - _batch_last_cmd) < 25) { + return; // let the WiFi task breathe between flash writes + } + if (_batch_next < _batch_count) { + WCLock prefs_lock(_mux); + BatchEntry& e = _batch[_batch_next++]; + e.reply[0] = 0; + uint32_t t0 = millis(); + const char* value = e.cmd + strlen("set ") + strlen(e.key) + 1; + if ((_mqtt_prefs == NULL || !_owns_wifi) && strcmp(e.key, "wifi.ssid") == 0) { + if (!value[0] || strlen(value) >= sizeof(_wifi_ssid)) { + strcpy(e.reply, "Error: WiFi SSID must be 1-31 characters"); + } else { + strncpy(_wifi_ssid, value, sizeof(_wifi_ssid) - 1); + _wifi_ssid[sizeof(_wifi_ssid) - 1] = 0; + _standalone_wifi_dirty = true; + strcpy(e.reply, "OK"); + } + } else if ((_mqtt_prefs == NULL || !_owns_wifi) && strcmp(e.key, "wifi.pwd") == 0) { + if (strlen(value) >= sizeof(_wifi_password)) { + strcpy(e.reply, "Error: WiFi password must be at most 63 characters"); + } else { + strncpy(_wifi_password, value, sizeof(_wifi_password) - 1); + _wifi_password[sizeof(_wifi_password) - 1] = 0; + _standalone_wifi_dirty = true; + strcpy(e.reply, "OK"); + } + } else if ((_mqtt_prefs == NULL || !_owns_wifi) && strcmp(e.key, "wifi.powersave") == 0) { + if (strcmp(value, "min") == 0) _wifi_power_save = 0; + else if (strcmp(value, "none") == 0) _wifi_power_save = 1; + else if (strcmp(value, "max") == 0) _wifi_power_save = 2; + else strcpy(e.reply, "Error: must be none, min, or max"); + if (e.reply[0] == 0) { + _standalone_wifi_dirty = true; + strcpy(e.reply, "OK"); + } + } else { + _cb->execCommand(e.cmd, e.reply); + // Keep the response snapshot current after MQTT/companion CLI handlers + // persist a WiFi field. The browser can then soft-reload without a stale + // value even before the requested reboot happens. + if (strncmp(e.reply, "OK", 2) == 0 && strcmp(e.key, "wifi.ssid") == 0) { + strncpy(_wifi_ssid, value, sizeof(_wifi_ssid) - 1); + _wifi_ssid[sizeof(_wifi_ssid) - 1] = 0; + } else if (strncmp(e.reply, "OK", 2) == 0 && strcmp(e.key, "wifi.pwd") == 0) { + strncpy(_wifi_password, value, sizeof(_wifi_password) - 1); + _wifi_password[sizeof(_wifi_password) - 1] = 0; + } else if (strncmp(e.reply, "OK", 2) == 0 && strcmp(e.key, "wifi.powersave") == 0) { + _wifi_power_save = strcmp(value, "max") == 0 ? 2 : strcmp(value, "min") == 0 ? 0 : 1; + } + } + if (e.reply[0] == 0) strcpy(e.reply, "OK"); + // A wizard save must not reboot away from a rejected setting before the + // operator can correct it. A reboot-only batch (zero commands) is still + // allowed; otherwise every command must report success. + if (_batch_reboot && strncmp(e.reply, "OK", 2) != 0) _batch_reboot = false; + _batch_last_cmd = millis(); + Serial.printf("WC: cmd %d/%d '%s' took %lums\n", (int)_batch_next, (int)_batch_count, + e.key, (unsigned long)(_batch_last_cmd - t0)); + if (_batch_next < _batch_count) return; // more commands next tick + } + if (_standalone_wifi_dirty) { + _standalone_wifi_dirty = false; + if (!saveStandaloneWiFi(_wifi_ssid, _wifi_password, _wifi_power_save)) { + // Attribute the persistence failure to the last WiFi field so it is + // visible beside a concrete input in the UI. + for (int i = _batch_count - 1; i >= 0; i--) { + if (strncmp(_batch[i].key, "wifi.", 5) == 0) { + strcpy(_batch[i].reply, "Error: failed to save WiFi settings"); + break; + } + } + _batch_reboot = false; + } + } + { + WCLock prefs_lock(_mux); + _cb->onConfigBatchEnd(); + } + WCLock lock(_mux); + _batch_state = BATCH_DONE; + if (_batch_reboot) { + // Fallback only: the real 3 s reboot timer is armed when the client reads + // /api/config/result (handleConfigResult), so the browser gets its + // confirmation before the AP/WiFi drops. This covers a client that + // disconnected and never polls — generous enough for a phone that got + // bounced off the AP mid-save to rejoin and fetch its confirmation. + _reboot_at = now + 30000; + if (_reboot_at == 0) _reboot_at = 1; + } +} + +// --------------------------------------------------------------------------- +// Routes / auth +// --------------------------------------------------------------------------- + +// Diagnostic trace of every request that reaches the server (async_tcp task). +// Distinguishes "client stopped sending" from "server stopped accepting" when +// a save's confirmation polls go missing on hardware. +static void wcLogReq(AsyncWebServerRequest* r) { + Serial.printf("WC: http %s %s\n", r->methodToString(), r->url().c_str()); +} + +void WebConfigServer::registerRoutes() { + _server->on("/", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleRoot(r); }); + _server->on("/api/status", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleStatus(r); }); + _server->on("/api/presets", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handlePresets(r); }); + _server->on("/api/login", HTTP_POST, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleLogin(r); }, + NULL, collectBody); + _server->on("/api/logout", HTTP_POST, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleLogout(r); }); + // NB: plain-string routes match sub-paths too ("/api/config" matches + // "/api/config/result") and handlers run in registration order, so the more + // specific route MUST be registered first or it never fires. This was why + // save confirmations were lost: result polls were answered with config JSON. + _server->on("/api/config/result", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleConfigResult(r); }); + _server->on("/api/config", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleConfigGet(r); }); + _server->on("/api/config", HTTP_POST, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleConfigPost(r); }, + NULL, collectBody); + _server->on("/api/stats", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleStats(r); }); + _server->on("/api/scan", HTTP_GET, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleScan(r); }); + _server->on("/api/reboot", HTTP_POST, [this](AsyncWebServerRequest* r) { wcLogReq(r); handleReboot(r); }); + _server->on("/api/portal/exit", HTTP_POST, [this](AsyncWebServerRequest* r) { wcLogReq(r); handlePortalExit(r); }); + _server->onNotFound([this](AsyncWebServerRequest* r) { wcLogReq(r); handleNotFound(r); }); +} + +// Accumulate a small JSON body into request->_tempObject (freed automatically +// by the request destructor). Oversized bodies are left unbuffered and +// rejected in the completion handler. +void WebConfigServer::collectBody(AsyncWebServerRequest* req, uint8_t* data, size_t len, + size_t index, size_t total) { + if (total == 0 || total > MAX_BODY) return; + if (index == 0) { + req->_tempObject = malloc(total + 1); + if (req->_tempObject) ((char*)req->_tempObject)[total] = 0; + } + if (req->_tempObject) memcpy((uint8_t*)req->_tempObject + index, data, len); +} + +bool WebConfigServer::checkAuth(AsyncWebServerRequest* req) { + _last_activity = millis(); + if (_mode == MODE_SETUP) return true; // physical proximity implied, nothing configured + if (_mode != MODE_LAN) return false; + NodeSnapshot node = {}; + { + WCLock lock(_mux); + _cb->getNodeSnapshot(node); + } + if (node.admin_password[0] == 0) return true; + if (_session_token[0] == 0) return false; + if (!req->hasHeader("Cookie")) return false; + const String& cookies = req->getHeader("Cookie")->value(); + int idx = cookies.indexOf("wcs="); + if (idx < 0 || (int)cookies.length() < idx + 4 + 32) return false; + String token = cookies.substring(idx + 4, idx + 4 + 32); + uint32_t now = millis(); + if ((uint32_t)(now - _session_last_seen) > WEBCONFIG_SESSION_TTL_MS) return false; + if (!fixedTimeEquals(token.c_str(), _session_token, 32)) return false; + _session_last_seen = now; // sliding expiry + return true; +} + +// --------------------------------------------------------------------------- +// Handlers (async_tcp task - no CLI/prefs writes, no radio access) +// --------------------------------------------------------------------------- + +void WebConfigServer::handleRoot(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + _last_activity = millis(); + if (req->hasHeader("If-None-Match") && + req->getHeader("If-None-Match")->value() == WEBCONFIG_HTML_ETAG) { + req->send(304); + return; + } + AsyncWebServerResponse* res = + req->beginResponse(200, "text/html", WEBCONFIG_HTML_GZ, WEBCONFIG_HTML_GZ_LEN); + res->addHeader("Content-Encoding", "gzip"); + res->addHeader("ETag", WEBCONFIG_HTML_ETAG); + req->send(res); +} + +void WebConfigServer::handleStatus(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + bool authed = checkAuth(req); + + NodeSnapshot node = {}; + { + WCLock lock(_mux); + _cb->getNodeSnapshot(node); + } + const bool has_mqtt = _mqtt_prefs != NULL; + + DynamicJsonDocument doc(768); + doc["mode"] = (_mode == MODE_SETUP) ? "setup" : "lan"; + doc["auth"] = authed; + doc["needs_setup"] = (_wifi_ssid[0] == 0); + doc["name"] = (const char*)node.name; + char node_id[17]; + for (int i = 0; i < 8; i++) sprintf(&node_id[i * 2], "%02x", _pub_key[i]); + doc["node_id"] = node_id; + doc["fw"] = _fw_ver; + doc["role"] = _role; + doc["board"] = _board_name; + doc["uptime_s"] = millis() / 1000; +#ifdef WITH_MQTT_BRIDGE + doc["runtime_slots"] = has_mqtt ? RUNTIME_MQTT_SLOTS : 0; + doc["max_slots"] = has_mqtt ? MAX_MQTT_SLOTS : 0; +#else + doc["runtime_slots"] = 0; + doc["max_slots"] = 0; +#endif + doc["mqtt"] = has_mqtt; + doc["capabilities"] = node.capabilities; + + AsyncResponseStream* res = req->beginResponseStream("application/json"); + serializeJson(doc, *res); + req->send(res); +} + +void WebConfigServer::handleLogin(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + _last_activity = millis(); + if (_mode == MODE_SETUP) { // no auth in setup mode + req->send(200, "application/json", "{\"ok\":true}"); + return; + } + NodeSnapshot node = {}; + { + WCLock lock(_mux); + _cb->getNodeSnapshot(node); + } + if (node.admin_password[0] == 0) { + req->send(200, "application/json", "{\"ok\":true}"); + return; + } + uint32_t now = millis(); + if (_login_lock_until && (int32_t)(now - _login_lock_until) < 0) { + req->send(429, "application/json", "{\"error\":\"locked, retry in 30s\"}"); + return; + } + const char* body = (const char*)req->_tempObject; + DynamicJsonDocument doc(256); + if (!body || deserializeJson(doc, body) != DeserializationError::Ok) { + req->send(400, "application/json", "{\"error\":\"bad request\"}"); + return; + } + const char* pwd = doc["password"] | ""; + if (!fixedTimeEquals(pwd, node.admin_password, sizeof(node.admin_password))) { + if (++_login_fails >= 5) { + _login_lock_until = now + 30000; + if (_login_lock_until == 0) _login_lock_until = 1; + _login_fails = 0; + } + req->send(401, "application/json", "{\"error\":\"wrong password\"}"); + return; + } + _login_fails = 0; + _login_lock_until = 0; + for (int i = 0; i < 4; i++) sprintf(&_session_token[i * 8], "%08lx", (unsigned long)esp_random()); + _session_last_seen = now; + + AsyncWebServerResponse* res = req->beginResponse(200, "application/json", "{\"ok\":true}"); + char cookie[80]; + sprintf(cookie, "wcs=%s; HttpOnly; SameSite=Lax; Path=/", _session_token); + res->addHeader("Set-Cookie", cookie); + req->send(res); +} + +void WebConfigServer::handleLogout(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + _session_token[0] = 0; + AsyncWebServerResponse* res = req->beginResponse(200, "application/json", "{\"ok\":true}"); + res->addHeader("Set-Cookie", "wcs=; Max-Age=0; Path=/"); + req->send(res); +} + +void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + + DynamicJsonDocument doc(6144); + { + WCLock lock(_mux); + NodeSnapshot node = {}; + _cb->getNodeSnapshot(node); + + JsonObject radio = doc.createNestedObject("radio"); + // round via double so float error doesn't leak into the JSON + // (910.525f would otherwise serialize as 910.5250244) + radio["freq"] = (double)roundf(node.freq * 1000.0f) / 1000.0; + radio["bw"] = (double)roundf(node.bw * 100.0f) / 100.0; + radio["sf"] = node.sf; + radio["cr"] = node.cr; + radio["tx"] = node.tx_power; + radio["af"] = node.airtime_factor; + radio["rxdelay"] = node.rx_delay; + radio["txdelay"] = node.tx_delay; + radio["cad"] = (bool)node.cad; + radio["rxgain"] = (bool)node.rx_gain; + radio["repeat"] = (bool)node.repeat; + radio["flood_max"] = node.flood_max; + radio["flood_max_advert"] = node.flood_max_advert; + radio["flood_max_unscoped"] = node.flood_max_unscoped; + static const char* const LOOP_MODES[] = { "off", "minimal", "moderate", "strict" }; + radio["loop_detect"] = LOOP_MODES[node.loop_detect <= 3 ? node.loop_detect : 0]; + radio["name"] = (const char*)node.name; + radio["lat"] = node.lat; + radio["lon"] = node.lon; + radio["advert_interval"] = node.advert_interval; + radio["flood_advert_interval"] = node.flood_advert_interval; + radio["capabilities"] = node.capabilities; + + JsonObject wifi = doc.createNestedObject("wifi"); + wifi["ssid"] = (const char*)_wifi_ssid; + wifi["pwd"] = _wifi_password[0] ? SECRET_SENTINEL : ""; + wifi["powersave"] = _wifi_power_save == 0 ? "min" + : _wifi_power_save == 2 ? "max" : "none"; + +#ifdef WITH_MQTT_BRIDGE + MQTTPrefs* obs = static_cast(_mqtt_prefs); + if (obs) { + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["origin"] = (const char*)obs->mqtt_origin; + mqtt["iata"] = (const char*)obs->mqtt_iata; + mqtt["status"] = (bool)obs->mqtt_status_enabled; + mqtt["packets"] = (bool)obs->mqtt_packets_enabled; + mqtt["raw"] = (bool)obs->mqtt_raw_enabled; + mqtt["tx"] = obs->mqtt_tx_enabled == 2 ? "advert" + : obs->mqtt_tx_enabled == 1 ? "on" : "off"; + mqtt["rx"] = (bool)obs->mqtt_rx_enabled; + mqtt["interval"] = obs->mqtt_status_interval / 60000; + mqtt["timezone"] = (const char*)obs->timezone_string; + mqtt["timezone_offset"] = obs->timezone_offset; + mqtt["ntp"] = (const char*)obs->mqtt_ntp_server; + mqtt["owner"] = (const char*)obs->mqtt_owner_public_key; + mqtt["email"] = (const char*)obs->mqtt_email; + mqtt["snmp"] = (bool)obs->snmp_enabled; + mqtt["snmp_community"] = (const char*)obs->snmp_community; + + JsonArray slots = mqtt.createNestedArray("slots"); + for (int i = 0; i < MAX_MQTT_SLOTS; i++) { + JsonObject s = slots.createNestedObject(); + s["preset"] = (const char*)obs->mqtt_slot_preset[i]; + s["server"] = (const char*)obs->mqtt_slot_host[i]; + s["port"] = obs->mqtt_slot_port[i]; + s["username"] = (const char*)obs->mqtt_slot_username[i]; + s["password"] = obs->mqtt_slot_password[i][0] ? SECRET_SENTINEL : ""; + s["token"] = obs->mqtt_slot_token[i][0] ? SECRET_SENTINEL : ""; + s["topic"] = (const char*)obs->mqtt_slot_topic[i]; + s["audience"] = (const char*)obs->mqtt_slot_audience[i]; + } + } +#endif + } + + AsyncResponseStream* res = req->beginResponseStream("application/json"); + serializeJson(doc, *res); + req->send(res); +} + +void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + if (req->contentLength() > MAX_BODY) { + req->send(413, "application/json", "{\"error\":\"body too large\"}"); + return; + } + const char* body = (const char*)req->_tempObject; + DynamicJsonDocument doc(6144); + if (!body || deserializeJson(doc, body) != DeserializationError::Ok) { + req->send(400, "application/json", "{\"error\":\"bad json\"}"); + return; + } + bool reboot_after = doc["reboot"] | false; + JsonObject set = doc["set"]; + + WCLock lock(_mux); + // A DONE batch stays readable until the next POST claims the slot, so a + // client that lost the result response can re-poll instead of failing. + if (_batch_state == BATCH_PENDING) { + req->send(409, "application/json", "{\"error\":\"busy\"}"); + return; + } + + int count = 0; + for (JsonPair kv : set) { + const char* key = kv.key().c_str(); + const char* val = kv.value().as(); + if (!val || !isAllowedSetKey(key, _mqtt_prefs != NULL)) { + char err[96]; + snprintf(err, sizeof(err), "{\"error\":\"bad key\",\"key\":\"%.32s\"}", key); + req->send(400, "application/json", err); + return; + } + if (isSecretKey(key) && strcmp(val, SECRET_SENTINEL) == 0) continue; // unchanged + if (count >= MAX_BATCH) { + req->send(400, "application/json", "{\"error\":\"too many changes\"}"); + return; + } + BatchEntry& e = _batch[count]; + strncpy(e.key, key, sizeof(e.key) - 1); + e.key[sizeof(e.key) - 1] = 0; + // Build "set ", stripping CR/LF so a value can't smuggle in + // a second command. + int pos = snprintf(e.cmd, sizeof(e.cmd), "set %s ", key); + for (const char* p = val; *p && pos < (int)sizeof(e.cmd) - 1; p++) { + if (*p == '\r' || *p == '\n') continue; + e.cmd[pos++] = *p; + } + e.cmd[pos] = 0; + count++; + } + if (count == 0 && !reboot_after) { + req->send(400, "application/json", "{\"error\":\"no changes\"}"); + return; + } + _batch_count = count; + _batch_next = 0; + _batch_reboot = reboot_after; + _batch_reboot_armed = false; + _standalone_wifi_dirty = false; + _batch_state = BATCH_PENDING; // tick() picks it up on the loop task + uint32_t du = millis() + 60000; + if (du == 0) du = 1; + _diag_until = du; + Serial.printf("WC: config POST accepted, %d cmds, reboot=%d\n", count, (int)reboot_after); + + char msg[64]; + snprintf(msg, sizeof(msg), "{\"state\":\"pending\",\"count\":%d}", count); + req->send(202, "application/json", msg); +} + +void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { Serial.println("WC: result read -> 503 (mode off)"); req->send(503); return; } + if (!checkAuth(req)) { Serial.println("WC: result read -> 401"); req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + + // Entry print BEFORE the lock (racy state read is fine for diag): if this + // fires but no branch print follows, the handler is blocked on _mux. + Serial.printf("WC: result entry mode=%d state=%d\n", (int)_mode, (int)_batch_state); + WCLock lock(_mux); + if (_batch_state == BATCH_IDLE) { + Serial.println("WC: result read -> idle"); + req->send(200, "application/json", "{\"state\":\"idle\"}"); + return; + } + if (_batch_state == BATCH_PENDING) { + req->send(200, "application/json", "{\"state\":\"pending\"}"); + return; + } + Serial.printf("WC: result read -> done (reboot=%d armed=%d)\n", + (int)_batch_reboot, (int)_batch_reboot_armed); + DynamicJsonDocument doc(6144); + doc["state"] = "done"; + doc["reboot"] = _batch_reboot; + JsonArray results = doc.createNestedArray("results"); + for (int i = 0; i < _batch_count; i++) { + JsonObject r = results.createNestedObject(); + r["key"] = (const char*)_batch[i].key; + r["reply"] = (const char*)_batch[i].reply; + } + // State stays DONE (re-readable) until the next POST claims the slot. + if (_batch_reboot && !_batch_reboot_armed) { + // Confirmation delivered — reboot 3 s from now (replaces the 30 s + // drain-time fallback) so the UI can show its countdown first. Armed + // once; re-reads must not keep pushing the deadline out. + _batch_reboot_armed = true; + _reboot_at = millis() + 3000; + if (_reboot_at == 0) _reboot_at = 1; + } + + AsyncResponseStream* res = req->beginResponseStream("application/json"); + serializeJson(doc, *res); + req->send(res); +} + +void WebConfigServer::handleStats(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + uint32_t until = millis() + 15000; + if (until == 0) until = 1; + _stats_wanted_until = until; // tick() refreshes the snapshot while polled + + WCLock lock(_mux); + if (_stats_json[0] == 0) { + req->send(200, "application/json", "{\"state\":\"pending\"}"); + return; + } + req->send(200, "application/json", _stats_json); +} + +void WebConfigServer::handleScan(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + + int n = WiFi.scanComplete(); + if (req->hasParam("rescan") && n >= 0) { + WiFi.scanDelete(); + n = WIFI_SCAN_FAILED; + } + if (n == WIFI_SCAN_FAILED) { + WiFi.scanNetworks(true); + req->send(200, "application/json", "{\"state\":\"scanning\"}"); + return; + } + if (n < 0) { // WIFI_SCAN_RUNNING + req->send(200, "application/json", "{\"state\":\"scanning\"}"); + return; + } + DynamicJsonDocument doc(3072); + doc["state"] = "done"; + JsonArray nets = doc.createNestedArray("networks"); + for (int i = 0; i < n && i < 20; i++) { + JsonObject net = nets.createNestedObject(); + net["ssid"] = WiFi.SSID(i); + net["rssi"] = WiFi.RSSI(i); + net["enc"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN; + } + AsyncResponseStream* res = req->beginResponseStream("application/json"); + serializeJson(doc, *res); + req->send(res); +} + +void WebConfigServer::handlePresets(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + _last_activity = millis(); +#ifdef WITH_MQTT_BRIDGE + if (_mqtt_prefs == NULL) { + req->send(200, "application/json", "{\"presets\":[]}"); + return; + } + DynamicJsonDocument doc(3072); + JsonArray arr = doc.createNestedArray("presets"); + for (int i = 0; i < MQTT_PRESET_COUNT; i++) { + const MQTTPresetDef& p = MQTT_PRESETS[i]; + JsonObject o = arr.createNestedObject(); + o["name"] = p.name; + // What the UI must collect for this preset to connect + if (p.topic_style == MQTT_TOPIC_MESHRANK) { + o["needs"] = "token"; + } else if (mqttPresetNeedsSlotCredentials(&p)) { + o["needs"] = "userpass"; + } else { + o["needs"] = "none"; + } + } + AsyncResponseStream* res = req->beginResponseStream("application/json"); + serializeJson(doc, *res); + req->send(res); +#else + req->send(200, "application/json", "{\"presets\":[]}"); +#endif +} + +void WebConfigServer::handleReboot(AsyncWebServerRequest* req) { + if (_mode == MODE_OFF) { req->send(503); return; } + if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; } + _reboot_at = millis() + 1500; + if (_reboot_at == 0) _reboot_at = 1; + req->send(200, "application/json", "{\"ok\":true}"); +} + +void WebConfigServer::handlePortalExit(AsyncWebServerRequest* req) { + // Setup mode only: switch captive probes to native "success" replies so the + // OS sign-in sheet can be dismissed (iOS: "Done") without dropping the WiFi; + // the user then continues at http:/// in their real browser, + // which survives the phone sleeping (the captive sheet does not). + if (_mode != MODE_SETUP) { req->send(404); return; } + _captive_release = true; + _last_activity = millis(); + char body[64]; + snprintf(body, sizeof(body), "{\"ok\":true,\"url\":\"http://%s/\"}", + WiFi.softAPIP().toString().c_str()); + req->send(200, "application/json", body); +} + +void WebConfigServer::handleNotFound(AsyncWebServerRequest* req) { + // Captive-portal probes (/generate_204, /hotspot-detect.html, /ncsi.txt, + // /connecttest.txt, ...) all land here; a redirect to the portal makes the + // phone pop its sign-in sheet. + if (_mode == MODE_SETUP && req->method() == HTTP_GET) { + if (_captive_release) { + // Answer each OS's connectivity check natively so the sheet reports + // success and can be closed. Deliberately does NOT bump _last_activity: + // background probes must not hold the portal open past the idle timeout. + const String& url = req->url(); + if (url.indexOf("generate_204") >= 0 || url.indexOf("gen_204") >= 0) { + req->send(204); // Android + } else if (url.indexOf("hotspot-detect") >= 0 || url.indexOf("success") >= 0) { + req->send(200, "text/html", // Apple CNA + "SuccessSuccess"); + } else if (url.indexOf("ncsi.txt") >= 0) { + req->send(200, "text/plain", "Microsoft NCSI"); // Windows + } else if (url.indexOf("connecttest.txt") >= 0) { + req->send(200, "text/plain", "Microsoft Connect Test"); + } else { + req->send(404); + } + return; + } + _last_activity = millis(); + req->redirect(String("http://") + WiFi.softAPIP().toString() + "/"); + return; + } + req->send(404); +} + +#endif // WITH_WEBCONFIG diff --git a/src/helpers/esp32/WebConfigServer.h b/src/helpers/esp32/WebConfigServer.h new file mode 100644 index 00000000..29bf007c --- /dev/null +++ b/src/helpers/esp32/WebConfigServer.h @@ -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 +#include +#include + +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 diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 7e7b29a5..b25a36b1 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -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"); + } } } diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h index fb850747..eae3add5 100644 --- a/src/helpers/ota/OtaTargets.h +++ b/src/helpers/ota/OtaTargets.h @@ -2,7 +2,7 @@ #include // 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" }, diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 8c472cc9..6c53b307 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -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()) { diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index d87c263a..78e19d43 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -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; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 37aeaadc..ece4e99f 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -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 { diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index b4851346..4cf25c10 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -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); diff --git a/test/test_cli_command_utils/test_cli_command_utils.cpp b/test/test_cli_command_utils/test_cli_command_utils.cpp new file mode 100644 index 00000000..779220b3 --- /dev/null +++ b/test/test_cli_command_utils/test_cli_command_utils.cpp @@ -0,0 +1,46 @@ +#include + +#include + +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(); +} diff --git a/variants/dual_ota_1536k.csv b/variants/dual_ota_1536k.csv new file mode 100644 index 00000000..33173d5d --- /dev/null +++ b/variants/dual_ota_1536k.csv @@ -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, diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini index 15fe761b..5eda8bdf 100644 --- a/variants/ebyte_eora_s3/platformio.ini +++ b/variants/ebyte_eora_s3/platformio.ini @@ -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 diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 0eb2209d..10b622f7 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -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 diff --git a/variants/generic-e22/platformio.ini b/variants/generic-e22/platformio.ini index 9d7a3fe5..47d2ce55 100644 --- a/variants/generic-e22/platformio.ini +++ b/variants/generic-e22/platformio.ini @@ -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} + +<../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} + +<../examples/simple_repeater/*.cpp> diff --git a/variants/generic_espnow/platformio.ini b/variants/generic_espnow/platformio.ini index 928d5315..fcc9e722 100644 --- a/variants/generic_espnow/platformio.ini +++ b/variants/generic_espnow/platformio.ini @@ -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"' diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 2dd55881..4422e226 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -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 diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index e0469a02..0b67d019 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -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; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 310cfe08..ed9301f2 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -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 diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index ebc855e2..c617a509 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -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 diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp index 04213bda..5802ffe9 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp @@ -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 } diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 19d4bacf..5a2642f4 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -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 = diff --git a/variants/heltec_tracker_v2/target.cpp b/variants/heltec_tracker_v2/target.cpp index 18a4c9d5..bd50ac6e 100644 --- a/variants/heltec_tracker_v2/target.cpp +++ b/variants/heltec_tracker_v2/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index 728f57c3..7e7abe31 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -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); diff --git a/variants/heltec_v3/target.cpp b/variants/heltec_v3/target.cpp index 9590acff..372385ea 100644 --- a/variants/heltec_v3/target.cpp +++ b/variants/heltec_v3/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 2396af88..2523780b 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -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] diff --git a/variants/heltec_v4/target.cpp b/variants/heltec_v4/target.cpp index 4497aa11..f0df3503 100644 --- a/variants/heltec_v4/target.cpp +++ b/variants/heltec_v4/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1fb123b2..27fb0ab6 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -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() { diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 20811abb..8acb30e5 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -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 { diff --git a/variants/keepteen_lt1/target.cpp b/variants/keepteen_lt1/target.cpp index 81e55625..72c627b4 100644 --- a/variants/keepteen_lt1/target.cpp +++ b/variants/keepteen_lt1/target.cpp @@ -12,7 +12,7 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index c5d3db26..5cf07b26 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -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 diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index e579e91c..e8ebe99c 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -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 diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp index 3d19bcd5..ad672c3d 100644 --- a/variants/lilygo_tbeam_1w/target.cpp +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -19,7 +19,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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); } - diff --git a/variants/lilygo_tbeam_SX1262/target.cpp b/variants/lilygo_tbeam_SX1262/target.cpp index 81a60213..657e01f1 100644 --- a/variants/lilygo_tbeam_SX1262/target.cpp +++ b/variants/lilygo_tbeam_SX1262/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/lilygo_tbeam_SX1276/target.cpp b/variants/lilygo_tbeam_SX1276/target.cpp index a5db9f6a..59fd0591 100644 --- a/variants/lilygo_tbeam_SX1276/target.cpp +++ b/variants/lilygo_tbeam_SX1276/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 -} \ No newline at end of file +} diff --git a/variants/lilygo_tbeam_supreme_SX1262/target.cpp b/variants/lilygo_tbeam_supreme_SX1262/target.cpp index c7cd5bc5..58d5e79a 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/target.cpp +++ b/variants/lilygo_tbeam_supreme_SX1262/target.cpp @@ -19,7 +19,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/lilygo_tlora_c6/platformio.ini b/variants/lilygo_tlora_c6/platformio.ini index 3acfa761..6e3e6b54 100644 --- a/variants/lilygo_tlora_c6/platformio.ini +++ b/variants/lilygo_tlora_c6/platformio.ini @@ -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 diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 6b2ea21d..121854a8 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -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} + +<../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} + +<../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} + +<../examples/simple_repeater> diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index 94083eb4..7756f996 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -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 diff --git a/variants/meshadventurer/target.cpp b/variants/meshadventurer/target.cpp index fd7a9763..3ec9d5c3 100644 --- a/variants/meshadventurer/target.cpp +++ b/variants/meshadventurer/target.cpp @@ -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 diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp index 9944a38b..ffea2c89 100644 --- a/variants/minewsemi_me25ls01/target.cpp +++ b/variants/minewsemi_me25ls01/target.cpp @@ -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 -} \ No newline at end of file +} diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index f8d42863..4fc0292b 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -9,13 +9,15 @@ #include #include #include -#ifdef DISPLAY_CLASS - #include "NullDisplayDriver.h" -#endif - -#ifdef DISPLAY_CLASS - extern NullDisplayDriver display; -#endif +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" + #include +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; + extern MomentaryButton user_btn; +#endif extern MinewsemiME25LS01Board board; extern WRAPPER_CLASS radio_driver; diff --git a/variants/muziworks_r1_neo/R1NeoBoard.h b/variants/muziworks_r1_neo/R1NeoBoard.h index c56f1f5c..fa49a184 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.h +++ b/variants/muziworks_r1_neo/R1NeoBoard.h @@ -20,6 +20,7 @@ public: #ifdef NRF52_POWER_MANAGEMENT void powerOff() override { + shutdownPeripherals(); initiateShutdown(SHUTDOWN_REASON_USER); } #endif diff --git a/variants/muziworks_r1_neo/target.cpp b/variants/muziworks_r1_neo/target.cpp index 68655c5d..d36ba832 100644 --- a/variants/muziworks_r1_neo/target.cpp +++ b/variants/muziworks_r1_neo/target.cpp @@ -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); diff --git a/variants/nano_g2_ultra/target.cpp b/variants/nano_g2_ultra/target.cpp index af376892..965c1f0c 100644 --- a/variants/nano_g2_ultra/target.cpp +++ b/variants/nano_g2_ultra/target.cpp @@ -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 diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 6a2f3dec..91c203a0 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -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} + + +<../examples/simple_repeater> diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 83495cd8..c45b7efd 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -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} + + @@ -155,4 +156,3 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 - diff --git a/variants/promicro/target.cpp b/variants/promicro/target.cpp index 6c21d66b..065d628f 100644 --- a/variants/promicro/target.cpp +++ b/variants/promicro/target.cpp @@ -12,7 +12,7 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/rak3112/target.cpp b/variants/rak3112/target.cpp index 5c49e187..57829aae 100644 --- a/variants/rak3112/target.cpp +++ b/variants/rak3112/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index 458bccce..284e20ff 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -26,7 +26,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 12533ffa..154adca5 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -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 diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index 5f40096d..a41ba720 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -26,7 +26,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; diff --git a/variants/rak_wismesh_tag/target.cpp b/variants/rak_wismesh_tag/target.cpp index a4f78fd7..4547ef51 100644 --- a/variants/rak_wismesh_tag/target.cpp +++ b/variants/rak_wismesh_tag/target.cpp @@ -22,7 +22,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h index 5a8861c2..b4676d3a 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -52,6 +52,7 @@ public: #endif #ifdef NRF52_POWER_MANAGEMENT + shutdownPeripherals(); initiateShutdown(SHUTDOWN_REASON_USER); #else NRF52Board::powerOff(); diff --git a/variants/tenstar_c3/platformio.ini b/variants/tenstar_c3/platformio.ini index 45e9fc0c..42a22296 100644 --- a/variants/tenstar_c3/platformio.ini +++ b/variants/tenstar_c3/platformio.ini @@ -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 diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index aae83324..1855612b 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -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 diff --git a/variants/thinknode_m3/target.cpp b/variants/thinknode_m3/target.cpp index e113b27e..6d18f310 100644 --- a/variants/thinknode_m3/target.cpp +++ b/variants/thinknode_m3/target.cpp @@ -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 -} \ No newline at end of file +} diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index cab1c1a3..9a88805d 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -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 diff --git a/variants/thinknode_m7/ThinkNodeM7Board.cpp b/variants/thinknode_m7/ThinkNodeM7Board.cpp index 6b1bf860..e2e009f8 100644 --- a/variants/thinknode_m7/ThinkNodeM7Board.cpp +++ b/variants/thinknode_m7/ThinkNodeM7Board.cpp @@ -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"; } diff --git a/variants/thinknode_m7/ThinkNodeM7Board.h b/variants/thinknode_m7/ThinkNodeM7Board.h index f591b5ac..7e705bde 100644 --- a/variants/thinknode_m7/ThinkNodeM7Board.h +++ b/variants/thinknode_m7/ThinkNodeM7Board.h @@ -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); diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 0eddb0e5..ee5cfee3 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -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 diff --git a/variants/xiao_c3/target.cpp b/variants/xiao_c3/target.cpp index 0009417c..1768fe00 100644 --- a/variants/xiao_c3/target.cpp +++ b/variants/xiao_c3/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - 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 } - diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 0d8c2a79..f1ea5477 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -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 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 5ac0423d..6e4e4afc 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -34,6 +34,7 @@ build_src_filter = ${rak4631_hw.build_src_filter} + + +<../variants/xiao_nrf52> + + + debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index 0fe0005c..be9e4c58 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -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 -} \ No newline at end of file +} diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index 0ba27b9e..ae577049 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -14,7 +14,9 @@ #ifdef DISPLAY_CLASS #include + #include 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(); - diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 00000000..1d2d3d27 --- /dev/null +++ b/webui/index.html @@ -0,0 +1,1029 @@ + + + + + +MeshCore Config + + + +
+ + + + +
+

MeshCore

+
connecting…
+
+ +
+
+ + +
+
+

Admin login

+

Enter this node's admin password (the same one used for remote CLI admin).

+
+
+
+ +
+
+
+
+ + +
+
In the WiFi sign-in popup? It closes if your phone sleeps. + Open in your browser to keep your progress.
+
+ +
+
+

Step 1 · WiFi

+

Connect this node to your WiFi network so it can reach the MQTT servers.

+
+
+ + +
+
+ +
Leave blank for an open network. 2.4 GHz networks only.
+
+ +
Shown on the mesh and in MQTT status messages.
+
+ +
+ +
+
+

Step 2 · Radio

+

Nodes only hear each other on identical radio settings. Pick the preset your local mesh uses.

+
+ +
+
+ +
Maximum legal power varies by region — check local rules.
+
Need settings that aren't listed? Choose Keep current settings, finish setup, then fine-tune in the Advanced editor.
+
+
+ + +
+
+ +
+
+

Step 3 · MQTT

+
+ +
How this observer identifies itself in published status/packet messages. Informational only — not used for topics or authentication. Prefilled with the node name.
+
+ +
Nearest airport code, e.g. DEN. Used in topic paths.
+
+ +
Optional — 64-char hex public key of your companion node. Included in auth JWTs so services that support it can let you claim this node.
+
+ +
Optional — also included in auth JWTs for claiming this node on some services.
+

Servers

+
+
+
+ + +
+
+ +
+
+

Step 4 · Review & save

+
+
+
Saving reboots the node into normal operation. To keep the page available later, run set webui on; use get webui to see its address.
+
+ + +
+
+
+ +

Advanced editor

+
+ + +
+
+ + + + +
+ +
+
+

Node

+
+
+
+
+
+
+
+

LoRa radio reboot to apply

+
Frequency, bandwidth, SF and CR must match your mesh exactly — a wrong value takes this node off the air until fixed over serial.
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Advanced

+
+
+
+
+
+
+
CAD (listen before transmit)Channel activity detection +
+
RX boosted gainSX126x receivers only +
+
RepeatForward mesh traffic. Off = listen-only (still observes and publishes). +
+
+
+
0 = off
+
+
0 = off, else 3–168
+
+
+
+

Flooding & loops

+
Hop limits for flood traffic this node forwards. Lower values contain noisy neighbours; too low takes parts of the mesh out of reach.
+
+
+
0–64. Default 64.
+
+
0–64. Default 8.
+
+
+
Applies to packets with no region scope. 0–64.
+
+ +
Drops flood packets that already carry this node's hash. Stricter = fewer repeats tolerated before dropping.
+
+
+ +
+
+

Identity

+
+
How this observer identifies itself in published messages; informational only. Blank = node name.
+
+
+
64-char hex public key of the owner's companion node (optional).
+
+
+
+

Publishing

+
This node only ever uploads to MQTT — nothing from a broker is injected into the mesh.
+
Node statusPeriodic health/stats to the status topic +
+
Publish packetsMaster switch for all packet traffic below +
+
Add raw framesAlso send the full hex frame to the raw topic +
+
Received packetsReport packets heard over the air +
+
+
+ +
Report packets this node sends.
+
+
+
+
+

Servers

+
+
+
+

Time & SNMP

+
+
Enter none to clear.
+
+
+
+
+
SNMP agentRestart required +
+
+
+
+ +
+
+

WiFi connection

+
Changing WiFi restarts the connection — this page will drop and the node reappears on the new network. Find its new IP from your router or the serial console.
+
+
+ + +
+
+
+
+
+
+

Maintenance

+
+ + +
+
+
+ +
+
+

Device

+
+
+
+

Free heap (KB)

+

Noise floor (dBm)

+
+
+

MQTT servers

+
No data yet.
+
+
+
+
+ + +
+ + + +
+ + +
+
Nearby networks + +
+
+
+ +
+ + +
+
+

Continue in your browser

+
    +
  1. Tap Done (or Cancel) in the corner of this window.
    + If asked, choose Use Without Internet / Stay connected.
  2. +
  3. Open Safari or Chrome on this device.
  4. +
  5. Go to http://192.168.4.1
  6. +
+
Stay connected to the MeshCore-Setup WiFi network. Your progress here isn't carried over — the setup restarts in the browser, where it survives the screen locking.
+

Go back

+
+
+ + +
+
+ +

Rebooting…

+

+

+
+
+ + + +