Pārlūkot izejas kodu

Merge upstream/dev into develop

Jared Dohrman 3 mēneši atpakaļ
vecāks
revīzija
e05c9bc09c
37 mainītis faili ar 2059 papildinājumiem un 629 dzēšanām
  1. 1033 0
      docs/cli_commands.md
  2. 29 7
      examples/companion_radio/DataStore.cpp
  3. 62 15
      examples/companion_radio/MyMesh.cpp
  4. 2 1
      examples/companion_radio/MyMesh.h
  5. 2 0
      examples/companion_radio/NodePrefs.h
  6. 76 110
      examples/simple_repeater/MyMesh.cpp
  7. 10 0
      examples/simple_repeater/MyMesh.h
  8. 5 1
      examples/simple_repeater/UITask.cpp
  9. 131 10
      examples/simple_room_server/MyMesh.cpp
  10. 17 0
      examples/simple_room_server/MyMesh.h
  11. 5 1
      examples/simple_room_server/UITask.cpp
  12. 25 1
      examples/simple_sensor/SensorMesh.cpp
  13. 4 0
      examples/simple_sensor/SensorMesh.h
  14. 5 1
      examples/simple_sensor/UITask.cpp
  15. 559 445
      src/helpers/CommonCLI.cpp
  16. 19 3
      src/helpers/CommonCLI.h
  17. 4 0
      src/helpers/ESP32Board.h
  18. 34 17
      src/helpers/RegionMap.cpp
  19. 6 1
      src/helpers/RegionMap.h
  20. 6 0
      src/helpers/sensors/EnvironmentSensorManager.cpp
  21. 1 1
      variants/heltec_v2/platformio.ini
  22. 4 1
      variants/heltec_v3/HeltecV3Board.h
  23. 2 0
      variants/heltec_v4/platformio.ini
  24. 1 0
      variants/heltec_wireless_paper/platformio.ini
  25. 1 0
      variants/lilygo_tbeam_1w/platformio.ini
  26. 1 1
      variants/lilygo_tlora_v2_1/platformio.ini
  27. 1 1
      variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h
  28. 0 1
      variants/minewsemi_me25ls01/platformio.ini
  29. 1 1
      variants/t1000-e/T1000eBoard.h
  30. 1 1
      variants/t1000-e/platformio.ini
  31. 1 1
      variants/t1000-e/variant.cpp
  32. 2 2
      variants/t1000-e/variant.h
  33. 0 1
      variants/thinknode_m3/platformio.ini
  34. 2 1
      variants/waveshare_rp2040_lora/platformio.ini
  35. 0 1
      variants/wio-e5-mini/platformio.ini
  36. 5 1
      variants/xiao_nrf52/XiaoNrf52Board.h
  37. 2 2
      variants/xiao_nrf52/platformio.ini

+ 1033 - 0
docs/cli_commands.md

@@ -0,0 +1,1033 @@
+# CLI Commands
+
+This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
+
+## Navigation
+
+- [Operational](#operational)
+- [Neighbors](#neighbors-repeater-only)
+- [Statistics](#statistics)
+- [Logging](#logging)
+- [Information](#info)
+- [Configuration](#configuration)
+  - [Radio](#radio)
+  - [System](#system)
+  - [Routing](#routing)
+  - [ACL](#acl)
+  - [Region Management](#region-management-v110)
+    - [Region Examples](#region-examples)
+  - [GPS](#gps-when-gps-support-is-compiled-in)
+  - [Sensors](#sensors-when-sensor-support-is-compiled-in)
+  - [Bridge](#bridge-when-bridge-support-is-compiled-in)
+
+---
+
+## Operational
+
+### Reboot the node
+**Usage:** 
+- `reboot`
+
+---
+
+### Reset the clock and reboot
+**Usage:**
+- `clkreboot`
+
+---
+
+### Sync the clock with the remote device
+**Usage:** 
+- `clock sync`
+
+---
+
+### Display current time in UTC
+**Usage:**
+- `clock`
+
+---
+
+### Set the time to a specific timestamp
+**Usage:** 
+- `time <epoch_seconds>`
+
+**Parameters:**
+- `epoch_seconds`: Unix epoch time
+
+---
+
+### Send a flood advert
+**Usage:** 
+- `advert`
+
+---
+
+### Send a zero-hop advert
+**Usage:**
+- `advert.zerohop`
+
+---
+
+### Start an Over-The-Air (OTA) firmware update
+**Usage:**
+- `start ota`
+
+---
+
+### Erase/Factory Reset
+**Usage:**
+- `erase`
+
+**Serial Only:** Yes
+
+**Warning:** _**This is destructive!**_
+
+---
+
+## Neighbors (Repeater Only)
+
+### List nearby neighbors
+**Usage:** 
+- `neighbors`
+
+**Note:** The output of this command is limited to the 8 most recent adverts.
+
+**Note:** Each line is encoded as `{pubkey-prefix}:{timestamp}:{snr*4}`
+
+---
+
+### Remove a neighbor
+**Usage:** 
+- `neighbor.remove <pubkey_prefix>`
+
+**Parameters:** 
+- `pubkey_prefix`: The public key of the node to remove from the neighbors list
+
+---
+
+### Discover zero hop neighbors
+
+**Usage:** 
+- `discover.neighbors`
+
+---
+
+## Statistics
+
+### Clear Stats
+**Usage:** `clear stats`
+
+---
+
+### System Stats - Battery, Uptime, Queue Length and Debug Flags
+**Usage:** 
+- `stats-core`
+
+**Serial Only:** Yes
+
+---
+
+### Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors
+**Usage:** `stats-radio`
+
+**Serial Only:** Yes
+
+---
+
+### Packet stats - Packet counters: Received, Sent
+**Usage:** `stats-packets`
+
+**Serial Only:** Yes
+
+---
+
+## Logging
+
+### Begin capture of rx log to node storage
+**Usage:** `log start`
+
+---
+
+### End capture of rx log to node storage
+**Usage:** `log stop`
+
+---
+
+### Erase captured log
+**Usage:** `log erase`
+
+---
+
+### Print the captured log to the serial terminal
+**Usage:** `log`
+
+**Serial Only:** Yes
+
+---
+
+## Info
+
+### Get the Version
+**Usage:** `ver`
+
+---
+
+### Show the hardware name
+**Usage:** `board`
+
+---
+
+## Configuration
+
+### Radio
+
+#### View or change this node's radio parameters
+**Usage:**
+- `get radio`
+- `set radio <freq>,<bw>,<sf>,<cr>`
+
+**Parameters:**
+- `freq`: Frequency in MHz
+- `bw`: Bandwidth in kHz
+- `sf`: Spreading factor (5-12)
+- `cr`: Coding rate (5-8)
+
+**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR`
+
+**Default:** `869.525,250,11,5`
+
+**Note:** Requires reboot to apply
+
+---
+
+#### View or change this node's transmit power
+**Usage:**
+- `get tx`
+- `set tx <dbm>`
+
+**Parameters:**
+- `dbm`: Power level in dBm (1-22)
+
+**Set by build flag:** `LORA_TX_POWER`
+
+**Default:** Varies by board
+
+**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**
+
+---
+
+#### Change the radio parameters for a set duration
+**Usage:** 
+- `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>`
+
+**Parameters:**
+- `freq`: Frequency in MHz (300-2500)
+- `bw`: Bandwidth in kHz (7.8-500)
+- `sf`: Spreading factor (5-12)
+- `cr`: Coding rate (5-8)
+- `timeout_mins`: Duration in minutes (must be > 0)
+
+**Note:** This is not saved to preferences and will clear on reboot
+
+---
+
+#### View or change this node's frequency
+**Usage:**
+- `get freq`
+- `set freq <frequency>`
+
+**Parameters:**
+- `frequency`: Frequency in MHz
+
+**Default:** `869.525`
+
+**Note:** Requires reboot to apply
+**Serial Only:** `set freq <frequency>`
+
+---
+
+#### View or change this node's rx boosted gain mode (SX12xx only, v1.14.1+)
+**Usage:**
+- `get radio.rxgain`
+- `set radio.rxgain <state>`
+
+**Parameters:**
+  - `state`: `on`|`off`
+
+**Default:** `on`
+
+**Temporary Note:** If you upgraded from an older version to 1.14.1 without erasing flash, this setting is `off` because of [#2118](https://github.com/meshcore-dev/MeshCore/issues/2118)
+
+---
+
+### System
+
+#### View or change this node's name
+**Usage:**
+- `get name`
+- `set name <name>`
+
+**Parameters:**
+- `name`: Node name
+
+**Set by build flag:** `ADVERT_NAME`
+
+**Default:** Varies by board
+
+**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.
+
+---
+
+#### View or change this node's latitude
+**Usage:**
+- `get lat`
+- `set lat <degrees>`
+
+**Set by build flag:** `ADVERT_LAT`
+
+**Default:** `0`
+
+**Parameters:**
+- `degrees`: Latitude in degrees
+
+---
+
+#### View or change this node's longitude
+**Usage:**
+- `get lon`
+- `set lon <degrees>`
+
+**Set by build flag:** `ADVERT_LON`
+
+**Default:** `0`
+
+**Parameters:**
+- `degrees`: Longitude in degrees
+
+---
+
+#### View or change this node's identity (Private Key)
+**Usage:**
+- `get prv.key`
+- `set prv.key <private_key>`
+
+**Parameters:**
+- `private_key`: Private key in hex format (64 hex characters)
+
+**Serial Only:**
+- `get prv.key`: Yes
+- `set prv.key`: No
+
+**Note:** Requires reboot to take effect after setting
+
+---
+
+#### Change this node's admin password
+**Usage:**
+- `password <new_password>`
+
+**Parameters:**
+- `new_password`: New admin password
+
+**Set by build flag:** `ADMIN_PASSWORD`
+
+**Default:** `password`
+
+**Note:** Command reply echoes the updated password for confirmation.
+
+**Note:** Any node using this password will be added to the admin ACL list.
+
+---
+
+#### View or change this node's guest password
+**Usage:**
+- `get guest.password`
+- `set guest.password <password>`
+
+**Parameters:**
+- `password`: Guest password
+
+**Set by build flag:** `ROOM_PASSWORD` (Room Server only)
+
+**Default:** `<blank>`
+
+---
+
+#### View or change this node's owner info
+**Usage:**
+- `get owner.info`
+- `set owner.info <text>`
+
+**Parameters:**
+- `text`: Owner information text
+
+**Default:** `<blank>`
+
+**Note:** `|` characters are translated to newlines
+
+**Note:** Requires firmware 1.12.+
+
+---
+
+#### Fine-tune the battery reading
+**Usage:**
+- `get adc.multiplier`
+- `set adc.multiplier <value>`
+
+**Parameters:**
+- `value`: ADC multiplier (0.0-10.0)
+
+**Default:** `0.0` (value defined by board)
+
+**Note:** Returns "Error: unsupported by this board" if hardware doesn't support it
+
+---
+
+#### View this node's public key
+**Usage:** `get public.key`
+
+---
+
+#### View this node's configured role
+**Usage:** `get role`
+
+---
+
+#### View or change this node's power saving flag (Repeater Only)
+**Usage:**
+- `powersaving`
+- `powersaving on`
+- `powersaving off`
+
+**Parameters:** 
+- `on`: enable power saving
+- `off`: disable power saving
+
+**Default:** `on`
+
+**Note:** When enabled, device enters sleep mode between radio transmissions
+
+---
+
+### Routing
+
+#### View or change this node's repeat flag
+**Usage:**
+- `get repeat`
+- `set repeat <state>`
+
+**Parameters:**
+  - `state`: `on`|`off`
+
+**Default:** `on`
+
+---
+
+#### View or change this node's advert path hash size
+**Usage:**
+- `get path.hash.mode`
+- `set path.hash.mode <value>`
+
+**Parameters:**
+- `value`: Path hash size (0-2)
+  - `0`: 1 Byte hash size (256 unique ids)[64 max flood]
+  - `1`: 2 Byte hash size (65,536 unique ids)[32 max flood]
+  - `2`: 3 Byte hash size (16,777,216 unique ids)[21 max flood]
+  - `3`: DO NOT USE (Reserved) 
+
+**Default:** `0`
+
+**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
+
+**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. 
+
+---
+
+#### View or change this node's loop detection
+**Usage:**
+- `get loop.detect`
+- `set loop.detect <state>`
+
+**Parameters:**
+- `state`: 
+  - `off`: no loop detection is performed
+  - `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte)
+  - `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
+  - `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
+  
+**Default:** `off`
+
+**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14
+
+**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet.  If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. 
+
+---
+
+#### View or change the retransmit delay factor for flood traffic
+**Usage:**
+- `get txdelay`
+- `set txdelay <value>`
+
+**Parameters:**
+- `value`: Transmit delay factor (0-2)
+
+**Default:** `0.5`
+
+---
+
+#### View or change the retransmit delay factor for direct traffic
+**Usage:**
+- `get direct.txdelay`
+- `set direct.txdelay <value>`
+
+**Parameters:**
+- `value`: Direct transmit delay factor (0-2)
+
+**Default:** `0.2`
+
+---
+
+#### [Experimental] View or change the processing delay for received traffic
+**Usage:**
+- `get rxdelay`
+- `set rxdelay <value>`
+
+**Parameters:**
+- `value`: Receive delay base (0-20)
+
+**Default:** `0.0`
+
+---
+
+#### View or change the duty cycle limit
+**Usage:**
+- `get dutycycle`
+- `set dutycycle <value>`
+
+**Parameters:**
+- `value`: Duty cycle percentage (1-100)
+
+**Default:** `50%` (equivalent to airtime factor 1.0)
+
+**Examples:**
+- `set dutycycle 100` — no duty cycle limit
+- `set dutycycle 50` — 50% duty cycle (default)
+- `set dutycycle 10` — 10% duty cycle
+- `set dutycycle 1` — 1% duty cycle (strictest EU requirement)
+
+> **Note:** Added in firmware v1.15.0
+
+---
+
+#### View or change the airtime factor (duty cycle limit)
+> **Deprecated** as of firmware v1.15.0. Use [`get/set dutycycle`](#view-or-change-the-duty-cycle-limit) instead.
+
+**Usage:**
+- `get af`
+- `set af <value>`
+
+**Parameters:**
+- `value`: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example:
+  - `af = 1` → ~50% duty
+  - `af = 2` → ~33% duty
+  - `af = 3` → ~25% duty
+  - `af = 9` → ~10% duty
+  You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).
+
+**Default:** `1.0`
+
+---
+
+#### View or change the local interference threshold
+**Usage:**
+- `get int.thresh`
+- `set int.thresh <value>`
+
+**Parameters:**
+- `value`: Interference threshold value
+
+**Default:** `0.0`
+
+---
+
+#### View or change the AGC Reset Interval
+**Usage:**
+- `get agc.reset.interval`
+- `set agc.reset.interval <value>`
+
+**Parameters:**
+- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
+
+**Default:** `0.0`
+
+---
+
+#### Enable or disable Multi-Acks support
+**Usage:**
+- `get multi.acks`
+- `set multi.acks <state>`
+
+**Parameters:**
+- `state`: `0` (disable) or `1` (enable)
+
+**Default:** `0`
+
+---
+
+#### View or change the flood advert interval
+**Usage:**
+- `get flood.advert.interval`
+- `set flood.advert.interval <hours>`
+
+**Parameters:**
+- `hours`: Interval in hours (3-168)
+
+**Default:** `12` (Repeater) - `0` (Sensor)
+
+---
+
+#### View or change the zero-hop advert interval
+**Usage:**
+- `get advert.interval`
+- `set advert.interval <minutes>`
+
+**Parameters:**
+- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
+
+**Default:** `0`
+
+---
+
+#### Limit the number of hops for a flood message
+**Usage:**
+- `get flood.max`
+- `set flood.max <value>`
+
+**Parameters:**
+- `value`: Maximum flood hop count (0-64)
+
+**Default:** `64`
+
+---
+
+### ACL
+
+#### Add, update or remove permissions for a companion
+**Usage:** 
+- `setperm <pubkey> <permissions>`
+
+**Parameters:**
+- `pubkey`: Companion public key
+- `permissions`: 
+  - `0`: Guest
+  - `1`: Read-only
+  - `2`: Read-write
+  - `3`: Admin
+
+**Note:** Removes the entry when `permissions` is omitted
+
+---
+
+#### View the current ACL
+**Usage:** 
+- `get acl`
+
+**Serial Only:** Yes
+
+---
+
+#### View or change this room server's 'read-only' flag
+**Usage:**
+- `get allow.read.only`
+- `set allow.read.only <state>`
+
+**Parameters:**
+- `state`: `on` (enable) or `off` (disable)
+
+**Default:** `off`
+
+---
+
+### Region Management (v1.10.+)
+
+#### Bulk-load region lists
+**Usage:** 
+- `region load`
+- `region load <name> [flood_flag]`
+
+**Parameters:**
+- `name`: A name of a region. `*` represents the wildcard region
+
+**Note:** `flood_flag`: Optional `F` to allow flooding
+
+**Note:** Indentation creates parent-child relationships (max 8 levels)
+
+**Note:** `region load` with an empty name will not work remotely (it's interactive)
+
+---
+
+#### Save any changes to regions made since reboot
+**Usage:** 
+- `region save`
+
+---
+
+#### Allow a region
+**Usage:** 
+- `region allowf <name>`
+
+**Parameters:** 
+- `name`: Region name (or `*` for wildcard)
+
+**Note:** Setting on wildcard `*` allows packets without region transport codes
+
+---
+
+#### Block a region
+**Usage:** 
+- `region denyf <name>`
+
+**Parameters:** 
+- `name`: Region name (or `*` for wildcard)
+
+**Note:** Setting on wildcard `*` drops packets without region transport codes
+
+---
+
+#### Show information for a region
+**Usage:** 
+- `region get <name>`
+
+**Parameters:**
+- `name`: Region name (or `*` for wildcard)
+
+---
+
+#### View or change the home region for this node
+**Usage:** 
+- `region home`
+- `region home <name>`
+
+**Parameters:**
+- `name`: Region name
+
+---
+
+#### View or change the default scope region for this node
+**Usage:** 
+- `region default`
+- `region default {name|<null>}`
+
+**Parameters:**
+- `name`: Region name,  or <null> to reset/clear
+
+---
+
+#### Create a new region
+**Usage:** 
+- `region put <name> [parent_name]`
+
+**Parameters:**
+- `name`: Region name
+- `parent_name`: Parent region name (optional, defaults to wildcard)
+
+---
+
+#### Remove a region
+**Usage:** 
+- `region remove <name>`
+
+**Parameters:**
+- `name`: Region name
+
+**Note:** Must remove all child regions before the region can be removed 
+
+---
+
+#### View all regions
+**Usage:** 
+- `region list <filter>`
+
+**Serial Only:** Yes
+
+**Parameters:**
+- `filter`: `allowed`|`denied`
+
+**Note:** Requires firmware 1.12.+
+
+---
+
+#### Dump all defined regions and flood permissions
+**Usage:** 
+- `region`
+
+**Serial Only:** For firmware older than 1.12.0
+
+---
+
+### Region Examples
+
+**Example 1: Using F Flag with Named Public Region**
+```
+region load
+#Europe F
+<blank line to end region load>
+region save
+```
+
+**Explanation:**
+- Creates a region named `#Europe` with flooding enabled
+- Packets from this region will be flooded to other nodes
+
+---
+
+**Example 2: Using Wildcard with F Flag**
+```
+region load 
+* F
+<blank line to end region load>
+region save
+```
+
+**Explanation:**
+- Creates a wildcard region `*` with flooding enabled
+- Enables flooding for all regions automatically
+- Applies only to packets without transport codes
+
+---
+
+**Example 3: Using Wildcard Without F Flag**
+```
+region load 
+*
+<blank line to end region load>
+region save
+```
+**Explanation:**
+- Creates a wildcard region `*` without flooding
+- This region exists but doesn't affect packet distribution
+- Used as a default/empty region
+
+---
+
+**Example 4: Nested Public Region with F Flag**
+```
+region load 
+#Europe F
+  #UK
+    #London
+    #Manchester
+  #France
+    #Paris
+    #Lyon
+<blank line to end region load>
+region save
+```
+
+**Explanation:**
+- Creates `#Europe` region with flooding enabled
+- Adds nested child regions (`#UK`, `#France`)
+- All nested regions inherit the flooding flag from parent
+
+---
+
+**Example 5: Wildcard with Nested Public Regions**
+```
+region load 
+* F
+  #NorthAmerica
+    #USA
+      #NewYork
+      #California
+    #Canada
+      #Ontario
+      #Quebec
+<blank line to end region load>
+region save
+```
+
+**Explanation:**
+- Creates wildcard region `*` with flooding enabled
+- Adds nested `#NorthAmerica` hierarchy
+- Enables flooding for all child regions automatically
+- Useful for global networks with specific regional rules
+
+---
+### GPS (When GPS support is compiled in)
+
+#### View or change GPS state
+**Usage:**
+- `gps`
+- `gps <state>`
+
+**Parameters:**
+- `state`: `on`|`off`
+
+**Default:** `off`
+
+**Note:** Output format:
+- `off` when the GPS hardware is disabled
+- `on, {active|deactivated}, {fix|no fix}, {sat count} sats` when the GPS hardware is enabled
+
+---
+
+#### Sync this node's clock with GPS time
+**Usage:** 
+- `gps sync`
+
+---
+
+#### Set this node's location based on the GPS coordinates
+**Usage:** 
+- `gps setloc`
+
+---
+
+#### View or change the GPS advert policy
+**Usage:**
+- `gps advert`
+- `gps advert <policy>`
+
+**Parameters:** 
+- `policy`: `none`|`share`|`prefs` 
+  - `none`: don't include location in adverts
+  - `share`: share gps location (from SensorManager)
+  - `prefs`: location stored in node's lat and lon settings
+
+**Default:** `prefs`
+
+---
+
+### Sensors (When sensor support is compiled in)
+
+#### View the list of sensors on this node
+**Usage:** `sensor list [start]`
+
+**Parameters:**
+- `start`: Optional starting index (defaults to 0)
+
+**Note:** Output format: `<var_name>=<value>\n`
+
+---
+
+#### View or change thevalue of a sensor
+**Usage:** 
+- `sensor get <key>`
+- `sensor set <key> <value>`
+
+**Parameters:**
+- `key`: Sensor setting name
+- `value`: The value to set the sensor to
+
+---
+
+### Bridge (When bridge support is compiled in)
+
+#### View the compiled bridge type
+**Usage:** `get bridge.type`
+
+---
+
+#### View or change the bridge enabled flag
+**Usage:**
+- `get bridge.enabled`
+- `set bridge.enabled <state>`
+
+**Parameters:**
+- `state`: `on`|`off`
+
+**Default:** `off`
+
+---
+
+#### Add a delay to packets routed through this bridge
+**Usage:**
+- `get bridge.delay`
+- `set bridge.delay <ms>`
+
+**Parameters:**
+- `ms`: Delay in milliseconds (0-10000)
+
+**Default:** `500`
+
+---
+
+#### View or change the source of packets bridged to the external interface
+**Usage:**
+- `get bridge.source`
+- `set bridge.source <source>`
+
+**Parameters:**
+- `source`: 
+  - `logRx`: bridges received packets
+  - `logTx`: bridges transmitted packets
+
+**Default:** `logTx`
+
+---
+
+#### View or change the speed of the bridge (RS-232 only)
+**Usage:**
+- `get bridge.baud`
+- `set bridge.baud <rate>`
+
+**Parameters:**
+- `rate`: Baud rate (`9600`, `19200`, `38400`, `57600`, or `115200`)
+
+**Default:** `115200`
+
+---
+
+#### View or change the channel used for bridging (ESPNow only)
+**Usage:**
+- `get bridge.channel`
+- `set bridge.channel <channel>`
+
+**Parameters:**
+- `channel`: Channel number (1-14)
+
+---
+
+#### Set the ESP-Now secret
+**Usage:** 
+- `get bridge.secret`
+- `set bridge.secret <secret>`
+
+**Parameters:**
+- `secret`: ESP-NOW bridge secret, up to 15 characters
+
+**Default:** Varies by board
+
+---
+
+#### View the bootloader version (nRF52 only)
+**Usage:** `get bootloader.ver`
+
+---
+
+#### View power management support
+**Usage:** `get pwrmgt.support`
+
+---
+
+#### View the current power source
+**Usage:** `get pwrmgt.source`
+
+**Note:** Returns an error on boards without power management support.
+
+---
+
+#### View the boot reset and shutdown reasons
+**Usage:** `get pwrmgt.bootreason`
+
+**Note:** Returns an error on boards without power management support.
+
+---
+
+#### View the boot voltage
+**Usage:** `get pwrmgt.bootmv`
+
+**Note:** Returns an error on boards without power management support.
+
+---

+ 29 - 7
examples/companion_radio/DataStore.cpp

@@ -230,17 +230,37 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval));               // 86
     file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config));           // 87
     file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops));       // 88
-    file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
-    if (file.available() >= (int)sizeof(_prefs.wifi_powersave)) {
+    file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain));         // 89
+
+    const int wifi_bytes = sizeof(_prefs.wifi_powersave) + sizeof(_prefs.wifi_ssid) + sizeof(_prefs.wifi_pwd);
+    const int scope_bytes = sizeof(_prefs.default_scope_name) + sizeof(_prefs.default_scope_key);
+    const int remaining = file.available();
+
+    if (remaining >= wifi_bytes + scope_bytes) {
       file.read((uint8_t *)&_prefs.wifi_powersave, sizeof(_prefs.wifi_powersave));         // 90
-    }
-    if (file.available() >= (int)sizeof(_prefs.wifi_ssid)) {
       file.read((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid));                    // 91
       _prefs.wifi_ssid[sizeof(_prefs.wifi_ssid) - 1] = 0;
-    }
-    if (file.available() >= (int)sizeof(_prefs.wifi_pwd)) {
       file.read((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd));                      // 124
       _prefs.wifi_pwd[sizeof(_prefs.wifi_pwd) - 1] = 0;
+      file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name));  // 189
+      _prefs.default_scope_name[sizeof(_prefs.default_scope_name) - 1] = 0;
+      file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key));    // 220
+    } else if (remaining >= scope_bytes) {
+      file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name));  // 90
+      _prefs.default_scope_name[sizeof(_prefs.default_scope_name) - 1] = 0;
+      file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key));    // 121
+    } else {
+      if (file.available() >= (int)sizeof(_prefs.wifi_powersave)) {
+        file.read((uint8_t *)&_prefs.wifi_powersave, sizeof(_prefs.wifi_powersave));       // 90
+      }
+      if (file.available() >= (int)sizeof(_prefs.wifi_ssid)) {
+        file.read((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid));                  // 91
+        _prefs.wifi_ssid[sizeof(_prefs.wifi_ssid) - 1] = 0;
+      }
+      if (file.available() >= (int)sizeof(_prefs.wifi_pwd)) {
+        file.read((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd));                    // 124
+        _prefs.wifi_pwd[sizeof(_prefs.wifi_pwd) - 1] = 0;
+      }
     }
 
     file.close();
@@ -279,10 +299,12 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval));               // 86
     file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config));           // 87
     file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops));       // 88
-    file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
+    file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain));         // 89
     file.write((uint8_t *)&_prefs.wifi_powersave, sizeof(_prefs.wifi_powersave));           // 90
     file.write((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid));                      // 91
     file.write((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd));                        // 124
+    file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name));    // 189
+    file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key));      // 220
 
     file.close();
   }

+ 62 - 15
examples/companion_radio/MyMesh.cpp

@@ -53,7 +53,7 @@
 #define CMD_SEND_BINARY_REQ           50
 #define CMD_FACTORY_RESET             51
 #define CMD_SEND_PATH_DISCOVERY_REQ   52
-#define CMD_SET_FLOOD_SCOPE           54   // v8+
+#define CMD_SET_FLOOD_SCOPE_KEY       54   // v8+
 #define CMD_SEND_CONTROL_DATA         55   // v8+
 #define CMD_GET_STATS                 56   // v8+, second byte is stats type
 #define CMD_SEND_ANON_REQ             57
@@ -62,6 +62,8 @@
 #define CMD_GET_ALLOWED_REPEAT_FREQ   60
 #define CMD_SET_PATH_HASH_MODE        61
 #define CMD_SEND_CHANNEL_DATA         62
+#define CMD_SET_DEFAULT_FLOOD_SCOPE   63
+#define CMD_GET_DEFAULT_FLOOD_SCOPE   64
 
 // Stats sub-types for CMD_GET_STATS
 #define STATS_TYPE_CORE               0
@@ -96,6 +98,7 @@
 #define RESP_CODE_AUTOADD_CONFIG      25
 #define RESP_ALLOWED_REPEAT_FREQ      26
 #define RESP_CODE_CHANNEL_DATA_RECV   27
+#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
 
 #define MAX_CHANNEL_DATA_LENGTH       (MAX_FRAME_SIZE - 9)
 
@@ -548,27 +551,32 @@ bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
   return _prefs.client_repeat != 0;
 }
 
-void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
-  // TODO: dynamic send_scope, depending on recipient and current 'home' Region
-  if (send_scope.isNull()) {
+void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) {
+  if (scope.isNull()) {
     sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
   } else {
     uint16_t codes[2];
-    codes[0] = send_scope.calcTransportCode(pkt);
+    codes[0] = scope.calcTransportCode(pkt);
     codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
     sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
   }
 }
+
+void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
+  // TODO: dynamic send_scope, depending on recipient and current 'home' Region
+  TransportKey default_scope;
+  memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
+
+  auto scope = send_scope.isNull() ? &default_scope : &send_scope;
+  sendFloodScoped(*scope, pkt, delay_millis);
+}
 void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
   // TODO: have per-channel send_scope
-  if (send_scope.isNull()) {
-    sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
-  } else {
-    uint16_t codes[2];
-    codes[0] = send_scope.calcTransportCode(pkt);
-    codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
-    sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
-  }
+  TransportKey default_scope;
+  memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
+
+  auto scope = send_scope.isNull() ? &default_scope : &send_scope;
+  sendFloodScoped(*scope, pkt, delay_millis);
 }
 
 void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
@@ -973,6 +981,17 @@ void MyMesh::begin(bool has_display) {
   strcpy(_prefs.node_name, pub_key_hex);
 #endif
 
+  // if build provides default-scope, init with that
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+  strcpy(_prefs.default_scope_name, DEFAULT_FLOOD_SCOPE_NAME);
+  {
+    TransportKeyStore temp;
+    TransportKey key;
+    temp.getAutoKeyFor(0, "#" DEFAULT_FLOOD_SCOPE_NAME, key);
+    memcpy(_prefs.default_scope_key, key.key, sizeof(key.key));
+  }
+#endif
+
   // load persisted prefs
   _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
 
@@ -1290,7 +1309,9 @@ void MyMesh::handleCmdFrame(size_t len) {
     if (pkt) {
       if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
         unsigned long delay_millis = 0;
-        sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
+        TransportKey default_scope;
+        memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
+        sendFloodScoped(default_scope, pkt, delay_millis);
       } else {
         sendZeroHop(pkt);
       }
@@ -1942,13 +1963,39 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_FILE_IO_ERROR);
     }
-  } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) {
+  } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 0) {
     if (len >= 2 + 16) {
       memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key));  // set curr scope TransportKey
     } else {
       memset(send_scope.key, 0, sizeof(send_scope.key));  // set scope to null
     }
     writeOKFrame();
+  } else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
+    if (len >= 1+31+16) {
+      int n = strlen((char *) &cmd_frame[1]);
+      if (n > 0 && n < 31) {
+        strcpy(_prefs.default_scope_name, (char *) &cmd_frame[1]);
+        memcpy(_prefs.default_scope_key, &cmd_frame[1+31], 16);
+        savePrefs();
+        writeOKFrame();
+      } else {
+        writeErrFrame(ERR_CODE_ILLEGAL_ARG);
+      }
+    } else {
+      memset(_prefs.default_scope_name, 0, sizeof(_prefs.default_scope_name));  // set default scope to null
+      memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key));
+      savePrefs();
+      writeOKFrame();
+    }
+  } else if (cmd_frame[0] == CMD_GET_DEFAULT_FLOOD_SCOPE) {
+    out_frame[0] = RESP_CODE_DEFAULT_FLOOD_SCOPE;
+    if (strlen(_prefs.default_scope_name) > 0) {
+      memcpy(&out_frame[1], _prefs.default_scope_name, 31);
+      memcpy(&out_frame[1+31], _prefs.default_scope_key, 16);
+      _serial->writeFrame(out_frame, 1+31+16);
+    } else {
+      _serial->writeFrame(out_frame, 1);   // no name or key means null
+    }
   } else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) {
     auto resp = createControlData(&cmd_frame[1], len - 1);
     if (resp) {

+ 2 - 1
examples/companion_radio/MyMesh.h

@@ -5,7 +5,7 @@
 #include "AbstractUITask.h"
 
 /*------------ Frame Protocol --------------*/
-#define FIRMWARE_VER_CODE 10
+#define FIRMWARE_VER_CODE 11
 
 #ifndef FIRMWARE_BUILD_DATE
 #define FIRMWARE_BUILD_DATE "20 Mar 2026"
@@ -117,6 +117,7 @@ protected:
   bool filterRecvFloodPacket(mesh::Packet* packet) override;
   bool allowPacketForward(const mesh::Packet* packet) override;
 
+  void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis);
   void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
   void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
 

+ 2 - 0
examples/companion_radio/NodePrefs.h

@@ -35,4 +35,6 @@ struct NodePrefs {  // persisted to file
   uint8_t wifi_powersave;    // 0 = none, 1 = min, 2 = max
   char wifi_ssid[33];
   char wifi_pwd[65];
+  char default_scope_name[31];
+  uint8_t default_scope_key[16];
 };

+ 76 - 110
examples/simple_repeater/MyMesh.cpp

@@ -537,6 +537,19 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[])
   return n >= max_counters[hash_size];
 }
 
+void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
+  if (recv_pkt_region && !recv_pkt_region->isWildcard()) {  // if _request_ packet scope is known, send reply with same scope
+    TransportKey scope;
+    if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) > 0) {
+      sendFloodScoped(scope, packet, delay_millis, path_hash_size);
+    } else {
+      sendFlood(packet, delay_millis, path_hash_size);  // send un-scoped
+    }
+  } else {
+    sendFlood(packet, delay_millis, path_hash_size);  // send un-scoped
+  }
+}
+
 bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
   if (_prefs.disable_fwd) return false;
   if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
@@ -709,10 +722,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-      if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+      if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else if (reply_path_len < 0) {
       mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
-      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+      if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else {
       mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
       uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63);
@@ -785,7 +798,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
         mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
                                               PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-        if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+        if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
       } else {
         mesh::Packet *reply =
             createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
@@ -793,7 +806,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
           if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
             sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
           } else {
-            sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+            sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           }
         }
       }
@@ -824,7 +837,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         mesh::Packet *ack = createAck(ack_hash);
         if (ack) {
           if (client->out_path_len == OUT_PATH_UNKNOWN) {
-            sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
+            sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize());
           } else {
             sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
           }
@@ -852,7 +865,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
         if (reply) {
           if (client->out_path_len == OUT_PATH_UNKNOWN) {
-            sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
+            sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
           } else {
             sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
           }
@@ -962,7 +975,9 @@ void MyMesh::sendNodeDiscoverReq() {
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
+      region_map(key_store), temp_map(key_store),
+      _cli(board, rtc, sensors, region_map, acl, &_prefs, this),
+      telemetry(MAX_PACKET_PAYLOAD - 4),
       discover_limiter(4, 120),  // max 4 every 2 minutes
       anon_limiter(4, 180)   // max 4 every 3 minutes
 #if defined(WITH_RS232_BRIDGE)
@@ -1039,6 +1054,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
 
   pending_discover_tag = 0;
   pending_discover_until = 0;
+
+  memset(default_scope.key, 0, sizeof(default_scope.key));
 }
 
 void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
@@ -1052,6 +1069,26 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
   // TODO: key_store.begin();
   region_map.load(_fs);
 
+  // establish default-scope
+  {
+    RegionEntry* r = region_map.getDefaultRegion();
+    if (r) {
+      region_map.getTransportKeysFor(*r, &default_scope, 1);
+    } else {
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+      r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
+      if (r == NULL) {
+        r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0);  // auto-create the default scope region
+        if (r) { r->flags = 0; }   // Allow-flood
+      }
+      if (r) {
+        region_map.setDefaultRegion(r);
+        region_map.getTransportKeysFor(*r, &default_scope, 1);
+      }
+#endif
+    }
+  }
+
 #if defined(WITH_BRIDGE)
   if (_prefs.bridge_enabled) {
     bridge.begin();
@@ -1119,6 +1156,17 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
   next_history_sample_ms = futureMillis(1000);
 }
 
+void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
+  if (scope.isNull()) {
+    sendFlood(pkt, delay_millis, path_hash_size);
+  } else {
+    uint16_t codes[2];
+    codes[0] = scope.calcTransportCode(pkt);
+    codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
+    sendFlood(pkt, codes, delay_millis, path_hash_size);
+  }
+}
+
 void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
   set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
   pending_freq = freq;
@@ -1146,7 +1194,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
     if (flood) {
-      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
+      sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
     } else {
       sendZeroHop(pkt, delay_millis);
     }
@@ -1257,6 +1305,25 @@ void MyMesh::formatStatsReply(char *reply, size_t reply_size) {
   StatsFormatHelper::formatCoreStats(reply, reply_size, board, *_ms, _err_flags, _mgr);
 }
 
+void MyMesh::startRegionsLoad() {
+  temp_map.resetFrom(region_map);   // rebuild regions in a temp instance
+  memset(load_stack, 0, sizeof(load_stack));
+  load_stack[0] = &temp_map.getWildcard();
+  region_load_active = true;
+}
+
+bool MyMesh::saveRegions() {
+  return region_map.save(_fs);
+}
+
+void MyMesh::onDefaultRegionChanged(const RegionEntry* r) {
+  if (r) {
+    region_map.getTransportKeysFor(*r, &default_scope, 1);
+  } else {
+    memset(default_scope.key, 0, sizeof(default_scope.key));
+  }
+}
+
 void MyMesh::formatRadioStatsReply(char *reply, size_t reply_size) {
   StatsFormatHelper::formatRadioStats(reply, reply_size, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
 }
@@ -1741,107 +1808,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
       Serial.printf("\n");
     }
     reply[0] = 0;
-  } else if (memcmp(command, "region", 6) == 0) {
-    reply[0] = 0;
-
-    const char* parts[4];
-    int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
-    if (n == 1) {
-      region_map.exportTo(reply, 160);
-    } else if (n >= 2 && strcmp(parts[1], "load") == 0) {
-      temp_map.resetFrom(region_map);   // rebuild regions in a temp instance
-      memset(load_stack, 0, sizeof(load_stack));
-      load_stack[0] = &temp_map.getWildcard();
-      region_load_active = true;
-    } else if (n >= 2 && strcmp(parts[1], "save") == 0) {
-      _prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime();   // this node is now 'modified' (for discovery info)
-      savePrefs();
-      bool success = region_map.save(_fs);
-      strcpy(reply, success ? "OK" : "Err - save failed");
-    } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        region->flags &= ~REGION_DENY_FLOOD;
-        strcpy(reply, "OK");
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        region->flags |= REGION_DENY_FLOOD;
-        strcpy(reply, "OK");
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "get") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        auto parent = region_map.findById(region->parent);
-        if (parent && parent->id != 0) {
-          sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
-        } else {
-          sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
-        }
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "home") == 0) {
-      auto home = region_map.findByNamePrefix(parts[2]);
-      if (home) {
-        region_map.setHomeRegion(home);
-        sprintf(reply, " home is now %s", home->name);
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n == 2 && strcmp(parts[1], "home") == 0) {
-      auto home = region_map.getHomeRegion();
-      sprintf(reply, " home is %s", home ? home->name : "*");
-    } else if (n >= 3 && strcmp(parts[1], "put") == 0) {
-      auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_map.getWildcard();
-      if (parent == NULL) {
-        strcpy(reply, "Err - unknown parent");
-      } else {
-        auto region = region_map.putRegion(parts[2], parent->id);
-        if (region == NULL) {
-          strcpy(reply, "Err - unable to put");
-        } else {
-          strcpy(reply, "OK");
-        }
-      }
-    } else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
-      auto region = region_map.findByName(parts[2]);
-      if (region) {
-        if (region_map.removeRegion(*region)) {
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Err - not empty");
-        }
-      } else {
-        strcpy(reply, "Err - not found");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "list") == 0) {
-      uint8_t mask = 0;
-      bool invert = false;
-      
-      if (strcmp(parts[2], "allowed") == 0) {
-        mask = REGION_DENY_FLOOD;
-        invert = false;  // list regions that DON'T have DENY flag
-      } else if (strcmp(parts[2], "denied") == 0) {
-        mask = REGION_DENY_FLOOD;
-        invert = true;   // list regions that DO have DENY flag
-      } else {
-        strcpy(reply, "Err - use 'allowed' or 'denied'");
-        return;
-      }
-      
-      int len = region_map.exportNamesTo(reply, 160, mask, invert);
-      if (len == 0) {
-        strcpy(reply, "-none-");
-      }
-    } else {
-      strcpy(reply, "Err - ??");
-    }
   } else if (memcmp(command, "discover.neighbors", 18) == 0) {
     const char* sub = command + 18;
     while (*sub == ' ') sub++;
@@ -2391,7 +2357,7 @@ void MyMesh::loop() {
   if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
     mesh::Packet *pkt = createSelfAdvert();
     uint32_t delay_millis = 0;
-    if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
+    if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
 
     updateFloodAdvertTimer(); // schedule next flood advert
     updateAdvertTimer();      // also schedule local advert (so they don't overlap)

+ 10 - 0
examples/simple_repeater/MyMesh.h

@@ -109,6 +109,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm
   RegionMap region_map, temp_map;
   RegionEntry* load_stack[8];
   RegionEntry* recv_pkt_region;
+  TransportKey default_scope;
   RateLimiter discover_limiter, anon_limiter;
   uint32_t pending_discover_tag;
   unsigned long pending_discover_until;
@@ -211,6 +212,8 @@ protected:
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
   void onControlDataRecv(mesh::Packet* packet) override;
 
+  void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
+
 public:
   MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
 
@@ -228,6 +231,9 @@ public:
     _cli.savePrefs(_fs);
   }
 
+  void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
+
+  // CommonCLICallbacks
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
   void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -248,11 +254,15 @@ public:
   void formatRadioStatsReply(char *reply, size_t reply_size) override;
   void formatPacketStatsReply(char *reply, size_t reply_size) override;
   void formatMemoryReply(char *reply, size_t reply_size) override;
+  void startRegionsLoad() override;
+  bool saveRegions() override;
+  void onDefaultRegionChanged(const RegionEntry* r) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 
   void saveIdentity(const mesh::LocalIdentity& new_id) override;
   void clearStats() override;
+
   void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
   void runWebCommand(const char* command, char* reply, size_t reply_size) override;
   const char* getWebAdminPassword() const override { return _prefs.password; }

+ 5 - 1
examples/simple_repeater/UITask.cpp

@@ -3,6 +3,10 @@
 #include <WiFi.h>
 #include <helpers/CommonCLI.h>
 
+#ifndef USER_BTN_PRESSED
+#define USER_BTN_PRESSED LOW
+#endif
+
 #define AUTO_OFF_MILLIS      20000  // 20 seconds
 #define BOOT_SCREEN_MILLIS   4000   // 4 seconds
 
@@ -96,7 +100,7 @@ void UITask::loop() {
   if (millis() >= _next_read) {
     int btnState = digitalRead(PIN_USER_BTN);
     if (btnState != _prevBtnState) {
-      if (btnState == LOW) {  // pressed?
+      if (btnState == USER_BTN_PRESSED) {  // pressed?
         if (_display->isOn()) {
           // TODO: any action ?
         } else {

+ 131 - 10
examples/simple_room_server/MyMesh.cpp

@@ -75,7 +75,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
   if (reply) {
     if (client->out_path_len == OUT_PATH_UNKNOWN) {
       unsigned long delay_millis = 0;
-      sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1);
+      sendFloodScoped(default_scope, reply, delay_millis, _prefs.path_hash_mode + 1); // REVISIT
       client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
     } else {
       sendDirect(reply, client->out_path, client->out_path_len);
@@ -286,6 +286,23 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
   return true;
 }
 
+bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
+  // just try to determine region for packet (apply later in allowPacketForward())
+  if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
+    recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
+  } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
+    if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
+      recv_pkt_region = NULL;
+    } else {
+      recv_pkt_region =  &region_map.getWildcard();
+    }
+  } else {
+    recv_pkt_region = NULL;
+  }
+  // do normal processing
+  return false;
+}
+
 void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
                             uint8_t *data, size_t len) {
   if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin
@@ -361,14 +378,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, 13);
-      if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+      if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else {
       mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
       if (reply) {
         if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
           sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
         } else {
-          sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+          sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
         }
       }
     }
@@ -458,7 +475,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
       if (send_ack) {
         if (client->out_path_len == OUT_PATH_UNKNOWN) {
           mesh::Packet *ack = createAck(ack_hash);
-          if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
+          if (ack) sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize());
           delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
         } else {
           uint32_t d = TXT_ACK_DELAY;
@@ -491,7 +508,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
         if (reply) {
           if (client->out_path_len == OUT_PATH_UNKNOWN) {
-            sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+            sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           } else {
             sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
           }
@@ -546,14 +563,14 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
             // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
             mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
                                                   PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-            if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+            if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           } else {
             mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
             if (reply) {
               if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
                 sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
               } else {
-                sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+                sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
               }
             }
           }
@@ -595,12 +612,16 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
+      region_map(key_store), temp_map(key_store),
+      _cli(board, rtc, sensors, region_map, acl, &_prefs, this),
+      telemetry(MAX_PACKET_PAYLOAD - 4)
+{
   last_millis = 0;
   uptime_millis = 0;
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
   _logging = false;
+  region_load_active = false;
   set_radio_at = revert_radio_at = 0;
 
   // defaults
@@ -638,6 +659,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   next_push = 0;
   memset(posts, 0, sizeof(posts));
   _num_posted = _num_post_pushes = 0;
+
+  memset(default_scope.key, 0, sizeof(default_scope.key));
 }
 
 void MyMesh::begin(FILESYSTEM *fs) {
@@ -647,6 +670,27 @@ void MyMesh::begin(FILESYSTEM *fs) {
   _cli.loadPrefs(_fs);
 
   acl.load(_fs, self_id);
+  region_map.load(_fs);
+
+  // establish default-scope
+  {
+    RegionEntry* r = region_map.getDefaultRegion();
+    if (r) {
+      region_map.getTransportKeysFor(*r, &default_scope, 1);
+    } else {
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+      r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
+      if (r == NULL) {
+        r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0);  // auto-create the default scope region
+        if (r) { r->flags = 0; }   // Allow-flood
+      }
+      if (r) {
+        region_map.setDefaultRegion(r);
+        region_map.getTransportKeysFor(*r, &default_scope, 1);
+      }
+#endif
+    }
+  }
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);
@@ -662,6 +706,30 @@ void MyMesh::begin(FILESYSTEM *fs) {
 #endif
 }
 
+void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
+  if (scope.isNull()) {
+    sendFlood(pkt, delay_millis, path_hash_size);
+  } else {
+    uint16_t codes[2];
+    codes[0] = scope.calcTransportCode(pkt);
+    codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
+    sendFlood(pkt, codes, delay_millis, path_hash_size);
+  }
+}
+
+void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
+  if (recv_pkt_region && !recv_pkt_region->isWildcard()) {  // if _request_ packet scope is known, send reply with same scope
+    TransportKey scope;
+    if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) > 0) {
+      sendFloodScoped(scope, packet, delay_millis, path_hash_size);
+    } else {
+      sendFlood(packet, delay_millis, path_hash_size);   // send un-scoped
+    }
+  } else {
+    sendFlood(packet, delay_millis, path_hash_size);   // send un-scoped
+  }
+}
+
 void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
   set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
   pending_freq = freq;
@@ -689,7 +757,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
     if (flood) {
-      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
+      sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
     } else {
       sendZeroHop(pkt, delay_millis);
     }
@@ -746,6 +814,25 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
   store.save("_main", new_id);
 }
 
+void MyMesh::startRegionsLoad() {
+  temp_map.resetFrom(region_map);   // rebuild regions in a temp instance
+  memset(load_stack, 0, sizeof(load_stack));
+  load_stack[0] = &temp_map.getWildcard();
+  region_load_active = true;
+}
+
+bool MyMesh::saveRegions() {
+  return region_map.save(_fs);
+}
+
+void MyMesh::onDefaultRegionChanged(const RegionEntry* r) {
+  if (r) {
+    region_map.getTransportKeysFor(*r, &default_scope, 1);
+  } else {
+    memset(default_scope.key, 0, sizeof(default_scope.key));
+  }
+}
+
 void MyMesh::clearStats() {
   radio_driver.resetStats();
   resetStats();
@@ -770,6 +857,40 @@ void MyMesh::formatMemoryReply(char *reply, size_t reply_size) {
 }
 
 void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
+  if (region_load_active) {
+    if (StrHelper::isBlank(command)) {  // empty/blank line, signal to terminate 'load' operation
+      region_map = temp_map;  // copy over the temp instance as new current map
+      region_load_active = false;
+
+      sprintf(reply, "OK - loaded %d regions", region_map.getCount());
+    } else {
+      char *np = command;
+      while (*np == ' ') np++;   // skip indent
+      int indent = np - command;
+
+      char *ep = np;
+      while (RegionMap::is_name_char(*ep)) ep++;
+      if (*ep) { *ep++ = 0; }  // set null terminator for end of name
+
+      while (*ep && *ep != 'F') ep++;  // look for (optional) flags
+
+      if (indent > 0 && indent < 8 && strlen(np) > 0) {
+        auto parent = load_stack[indent - 1];
+        if (parent) {
+          auto old = region_map.findByName(np);
+          auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0);  // carry-over the current ID (if name already exists)
+          if (nw) {
+            nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD);   // carry-over flags from curr
+
+            load_stack[indent] = nw;  // keep pointers to parent regions, to resolve parent_id's
+          }
+        }
+      }
+      reply[0] = 0;
+    }
+    return;
+  }
+
   while (*command == ' ')
     command++; // skip leading spaces
 
@@ -871,7 +992,7 @@ void MyMesh::loop() {
   if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
     mesh::Packet *pkt = createSelfAdvert();
     uint32_t delay_millis = 0;
-    if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
+    if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
 
     updateFloodAdvertTimer(); // schedule next flood advert
     updateAdvertTimer();      // also schedule local advert (so they don't overlap)

+ 17 - 0
examples/simple_room_server/MyMesh.h

@@ -20,6 +20,7 @@
 #include <helpers/CommonCLI.h>
 #include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 #include <RTClib.h>
 #include <target.h>
 
@@ -93,7 +94,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   uint64_t uptime_millis;
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
+  bool region_load_active;
   NodePrefs _prefs;
+  TransportKeyStore key_store;
+  RegionMap region_map, temp_map;
   ClientACL acl;
   CommonCLI _cli;
   unsigned long dirty_contacts_expiry;
@@ -104,6 +108,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   int next_post_idx;
   PostInfo posts[MAX_UNSYNCED_POSTS];   // cyclic queue
   CayenneLPP telemetry;
+  RegionEntry* load_stack[8];
+  RegionEntry* recv_pkt_region;
+  TransportKey default_scope;
   unsigned long set_radio_at, revert_radio_at;
   float pending_freq;
   float pending_bw;
@@ -144,6 +151,8 @@ protected:
     return _prefs.multi_acks;
   }
 
+  bool filterRecvFloodPacket(mesh::Packet* pkt) override;
+
   bool allowPacketForward(const mesh::Packet* packet) override;
   void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
   int searchPeersByHash(const uint8_t* hash) override ;
@@ -158,6 +167,8 @@ protected:
   }
 #endif
 
+  void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
+
 public:
   MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
 
@@ -175,6 +186,9 @@ public:
     _cli.savePrefs(_fs);
   }
 
+  void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
+
+  // CommonCLICallbacks
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
   void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -197,6 +211,9 @@ public:
   void formatRadioStatsReply(char *reply, size_t reply_size) override;
   void formatPacketStatsReply(char *reply, size_t reply_size) override;
   void formatMemoryReply(char *reply, size_t reply_size) override;
+  void startRegionsLoad() override;
+  bool saveRegions() override;
+  void onDefaultRegionChanged(const RegionEntry* r) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 

+ 5 - 1
examples/simple_room_server/UITask.cpp

@@ -2,6 +2,10 @@
 #include <Arduino.h>
 #include <helpers/CommonCLI.h>
 
+#ifndef USER_BTN_PRESSED
+#define USER_BTN_PRESSED LOW
+#endif
+
 #define AUTO_OFF_MILLIS      20000  // 20 seconds
 #define BOOT_SCREEN_MILLIS   4000   // 4 seconds
 
@@ -85,7 +89,7 @@ void UITask::loop() {
   if (millis() >= _next_read) {
     int btnState = digitalRead(PIN_USER_BTN);
     if (btnState != _prevBtnState) {
-      if (btnState == LOW) {  // pressed?
+      if (btnState == USER_BTN_PRESSED) {  // pressed?
         if (_display->isOn()) {
           // TODO: any action ?
         } else {

+ 25 - 1
examples/simple_sensor/SensorMesh.cpp

@@ -696,7 +696,9 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
 
 SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
      : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
+      region_map(key_store),
+      _cli(board, rtc, sensors, region_map, acl, &_prefs, this),
+      telemetry(MAX_PACKET_PAYLOAD - 4)
 {
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
@@ -730,6 +732,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   _prefs.gps_interval = 0;
   _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
   _prefs.battery_reporting_enabled = 1;
+  memset(default_scope.key, 0, sizeof(default_scope.key));
 }
 
 void SensorMesh::begin(FILESYSTEM* fs) {
@@ -739,6 +742,27 @@ void SensorMesh::begin(FILESYSTEM* fs) {
   _cli.loadPrefs(_fs);
 
   acl.load(_fs, self_id);
+  region_map.load(_fs);
+
+  // establish default-scope
+  {
+    RegionEntry* r = region_map.getDefaultRegion();
+    if (r) {
+      region_map.getTransportKeysFor(*r, &default_scope, 1);
+    } else {
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+      r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
+      if (r == NULL) {
+        r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0);  // auto-create the default scope region
+        if (r) { r->flags = 0; }   // Allow-flood
+      }
+      if (r) {
+        region_map.setDefaultRegion(r);
+        region_map.getTransportKeysFor(*r, &default_scope, 1);
+      }
+#endif
+    }
+  }
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);

+ 4 - 0
examples/simple_sensor/SensorMesh.h

@@ -22,6 +22,7 @@
 #include <helpers/CommonCLI.h>
 #include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 #include <RTClib.h>
 #include <target.h>
 
@@ -139,6 +140,9 @@ private:
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
   unsigned long dirty_contacts_expiry;
   CayenneLPP telemetry;
+  TransportKeyStore key_store;
+  RegionMap region_map;
+  TransportKey default_scope;
   uint32_t last_read_time;
   int matching_peer_indexes[MAX_SEARCH_RESULTS];
   int num_alert_tasks;

+ 5 - 1
examples/simple_sensor/UITask.cpp

@@ -2,6 +2,10 @@
 #include <Arduino.h>
 #include <helpers/CommonCLI.h>
 
+#ifndef USER_BTN_PRESSED
+#define USER_BTN_PRESSED LOW
+#endif
+
 #define AUTO_OFF_MILLIS      20000  // 20 seconds
 #define BOOT_SCREEN_MILLIS   4000   // 4 seconds
 
@@ -85,7 +89,7 @@ void UITask::loop() {
   if (millis() >= _next_read) {
     int btnState = digitalRead(PIN_USER_BTN);
     if (btnState != _prevBtnState) {
-      if (btnState == LOW) {  // pressed?
+      if (btnState == USER_BTN_PRESSED) {  // pressed?
         if (_display->isOn()) {
           // TODO: any action ?
         } else {

+ 559 - 445
src/helpers/CommonCLI.cpp

@@ -214,7 +214,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
   }
 }
 
-void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
+void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
     if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
       _board->powerOff();  // doesn't return
     } else if (memcmp(command, "reboot", 6) == 0) {
@@ -302,452 +302,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     } else if (memcmp(command, "clear stats", 11) == 0) {
       _callbacks->clearStats();
       strcpy(reply, "(OK - stats reset)");
-    /*
-     * GET commands
-     */
     } else if (memcmp(command, "get ", 4) == 0) {
-      const char* config = &command[4];
-      if (memcmp(config, "dutycycle", 9) == 0) {
-        float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
-        int dc_int = (int)dc;
-        int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
-        sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
-      } else if (memcmp(config, "af", 2) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
-      } else if (memcmp(config, "int.thresh", 10) == 0) {
-        sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
-      } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
-      } else if (memcmp(config, "multi.acks", 10) == 0) {
-        sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
-      } else if (memcmp(config, "allow.read.only", 15) == 0) {
-        sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
-      } else if (memcmp(config, "flood.advert.interval", 21) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
-      } else if (memcmp(config, "advert.interval", 15) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
-      } else if (memcmp(config, "guest.password", 14) == 0) {
-        sprintf(reply, "> %s", _prefs->guest_password);
-      } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) {  // from serial command line only
-        uint8_t prv_key[PRV_KEY_SIZE];
-        int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
-        mesh::Utils::toHex(tmp, prv_key, len);
-        sprintf(reply, "> %s", tmp);
-      } else if (memcmp(config, "name", 4) == 0) {
-        sprintf(reply, "> %s", _prefs->node_name);
-      } else if (memcmp(config, "repeat", 6) == 0) {
-        sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
-      } else if (memcmp(config, "lat", 3) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
-      } else if (memcmp(config, "lon", 3) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
-#if defined(USE_SX1262) || defined(USE_SX1268)
-      } else if (memcmp(config, "radio.rxgain", 12) == 0) {
-        sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
-#endif
-      } else if (memcmp(config, "radio", 5) == 0) {
-        char freq[16], bw[16];
-        strcpy(freq, StrHelper::ftoa(_prefs->freq));
-        strcpy(bw, StrHelper::ftoa3(_prefs->bw));
-        sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
-      } else if (memcmp(config, "rxdelay", 7) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
-      } else if (memcmp(config, "txdelay", 7) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
-      } else if (memcmp(config, "flood.max", 9) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
-      } else if (memcmp(config, "direct.txdelay", 14) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
-      } else if (memcmp(config, "owner.info", 10) == 0) {
-        *reply++ = '>';
-        *reply++ = ' ';
-        const char* sp = _prefs->owner_info;
-        while (*sp) {
-          *reply++ = (*sp == '\n') ? '|' : *sp;    // translate newline back to orig '|'
-          sp++;
-        }
-        *reply = 0;  // set null terminator
-      } else if (memcmp(config, "path.hash.mode", 14) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
-      } else if (memcmp(config, "loop.detect", 11) == 0) {
-        if (_prefs->loop_detect == LOOP_DETECT_OFF) {
-          strcpy(reply, "> off");
-        } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
-          strcpy(reply, "> minimal");
-        } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
-          strcpy(reply, "> moderate");
-        } else {
-          strcpy(reply, "> strict");
-        }
-      } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
-        sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
-      } else if (memcmp(config, "freq", 4) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
-      } else if (memcmp(config, "public.key", 10) == 0) {
-        strcpy(reply, "> ");
-        mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
-      } else if (memcmp(config, "role", 4) == 0) {
-        sprintf(reply, "> %s", _callbacks->getRole());
-      } else if (memcmp(config, "bridge.type", 11) == 0) {
-        sprintf(reply, "> %s",
-#ifdef WITH_RS232_BRIDGE
-                "rs232"
-#elif WITH_ESPNOW_BRIDGE
-                "espnow"
-#else
-                "none"
-#endif
-        );
-#ifdef WITH_BRIDGE
-      } else if (memcmp(config, "bridge.enabled", 14) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
-      } else if (memcmp(config, "bridge.delay", 12) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
-      } else if (memcmp(config, "bridge.source", 13) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
-#endif
-#ifdef WITH_RS232_BRIDGE
-      } else if (memcmp(config, "bridge.baud", 11) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
-#endif
-#ifdef WITH_ESPNOW_BRIDGE
-      } else if (memcmp(config, "bridge.channel", 14) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
-      } else if (memcmp(config, "bridge.secret", 13) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_secret);
-#endif
-      } else if (memcmp(config, "bootloader.ver", 14) == 0) {
-      #ifdef NRF52_PLATFORM
-          char ver[32];
-          if (_board->getBootloaderVersion(ver, sizeof(ver))) {
-              sprintf(reply, "> %s", ver);
-          } else {
-              strcpy(reply, "> unknown");
-          }
-      #else
-          strcpy(reply, "ERROR: unsupported");
-      #endif
-      } else if (memcmp(config, "adc.multiplier", 14) == 0) {
-        float adc_mult = _board->getAdcMultiplier();
-        if (adc_mult == 0.0f) {
-          strcpy(reply, "Error: unsupported by this board");
-        } else {
-          sprintf(reply, "> %.3f", adc_mult);
-        }
-      } else if (memcmp(config, "battery.reporting", 17) == 0) {
-        if (_board->supportsBatteryReporting()) {
-          sprintf(reply, "> %s", _board->isBatteryReportingEnabled() ? "on" : "off");
-        } else {
-          strcpy(reply, "Error: unsupported by this board");
-        }
-      // Power management commands
-      } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        strcpy(reply, "> supported");
-#else
-        strcpy(reply, "> unsupported");
-#endif
-      } else if (memcmp(config, "pwrmgt.source", 13) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        sprintf(reply, "> Reset: %s; Shutdown: %s",
-          _board->getResetReasonString(_board->getResetReason()),
-          _board->getShutdownReasonString(_board->getShutdownReason()));
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        sprintf(reply, "> %u mV", _board->getBootVoltage());
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else {
-        sprintf(reply, "??: %s", config);
-      }
-    /*
-     * SET commands
-     */
+      handleGetCmd(sender_timestamp, command, reply);
     } else if (memcmp(command, "set ", 4) == 0) {
-      const char* config = &command[4];
-      if (memcmp(config, "dutycycle ", 10) == 0) {
-        float dc = atof(&config[10]);
-        if (dc < 1 || dc > 100) {
-          strcpy(reply, "ERROR: dutycycle must be 1-100");
-        } else {
-          _prefs->airtime_factor = (100.0f / dc) - 1.0f;
-          savePrefs();
-          float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
-          int a_int = (int)actual;
-          int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
-          sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
-        }
-      } else if (memcmp(config, "af ", 3) == 0) {
-        _prefs->airtime_factor = atof(&config[3]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "int.thresh ", 11) == 0) {
-        _prefs->interference_threshold = atoi(&config[11]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
-        _prefs->agc_reset_interval = atoi(&config[19]) / 4;
-        savePrefs();
-        sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
-      } else if (memcmp(config, "multi.acks ", 11) == 0) {
-        _prefs->multi_acks = atoi(&config[11]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "allow.read.only ", 16) == 0) {
-        _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
-        int hours = _atoi(&config[22]);
-        if ((hours > 0 && hours < 3) || (hours > 168)) {
-          strcpy(reply, "Error: interval range is 3-168 hours");
-        } else {
-          _prefs->flood_advert_interval = (uint8_t)(hours);
-          _callbacks->updateFloodAdvertTimer();
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "advert.interval ", 16) == 0) {
-        int mins = _atoi(&config[16]);
-        if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) {
-          sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
-        } else {
-          _prefs->advert_interval = (uint8_t)(mins / 2);
-          _callbacks->updateAdvertTimer();
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "guest.password ", 15) == 0) {
-        StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "prv.key ", 8) == 0) {
-        uint8_t prv_key[PRV_KEY_SIZE];
-        bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
-        // only allow rekey if key is valid
-        if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
-          mesh::LocalIdentity new_id;
-          new_id.readFrom(prv_key, PRV_KEY_SIZE);
-          _callbacks->saveIdentity(new_id);
-          strcpy(reply, "OK, reboot to apply! New pubkey: ");
-          mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
-        } else {
-          strcpy(reply, "Error, bad key");
-        }
-      } else if (memcmp(config, "name ", 5) == 0) {
-        if (isValidName(&config[5])) {
-          StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, bad chars");
-        }
-      } else if (memcmp(config, "repeat ", 7) == 0) {
-        _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
-        savePrefs();
-        strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
-#if defined(USE_SX1262) || defined(USE_SX1268)
-      } else if (memcmp(config, "radio.rxgain ", 13) == 0) {
-        _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0;
-        strcpy(reply, "OK");
-        savePrefs();
-        _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
-#endif
-      } else if (memcmp(config, "radio ", 6) == 0) {
-        strcpy(tmp, &config[6]);
-        const char *parts[4];
-        int num = mesh::Utils::parseTextParts(tmp, parts, 4);
-        float freq  = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
-        float bw    = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
-        uint8_t sf  = num > 2 ? atoi(parts[2]) : 0;
-        uint8_t cr  = num > 3 ? atoi(parts[3]) : 0;
-        if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
-          _prefs->sf = sf;
-          _prefs->cr = cr;
-          _prefs->freq = freq;
-          _prefs->bw = bw;
-          _callbacks->savePrefs();
-          strcpy(reply, "OK - reboot to apply");
-        } else {
-          strcpy(reply, "Error, invalid radio params");
-        }
-      } else if (memcmp(config, "lat ", 4) == 0) {
-        _prefs->node_lat = atof(&config[4]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "lon ", 4) == 0) {
-        _prefs->node_lon = atof(&config[4]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "rxdelay ", 8) == 0) {
-        float db = atof(&config[8]);
-        if (db >= 0) {
-          _prefs->rx_delay_base = db;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "txdelay ", 8) == 0) {
-        float f = atof(&config[8]);
-        if (f >= 0) {
-          _prefs->tx_delay_factor = f;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "flood.max ", 10) == 0) {
-        uint8_t m = atoi(&config[10]);
-        if (m <= 64) {
-          _prefs->flood_max = m;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, max 64");
-        }
-      } else if (memcmp(config, "direct.txdelay ", 15) == 0) {
-        float f = atof(&config[15]);
-        if (f >= 0) {
-          _prefs->direct_tx_delay_factor = f;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "owner.info ", 11) == 0) {
-        config += 11;
-        char *dp = _prefs->owner_info;
-        while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
-          *dp++ = (*config == '|') ? '\n' : *config;    // translate '|' to newline chars
-          config++;
-        }
-        *dp = 0;
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "path.hash.mode ", 15) == 0) {
-        config += 15;
-        uint8_t mode = atoi(config);
-        if (mode < 3) {
-          _prefs->path_hash_mode = mode;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, must be 0,1, or 2");
-        }
-      } else if (memcmp(config, "loop.detect ", 12) == 0) {
-        config += 12;
-        uint8_t mode;
-        if (memcmp(config, "off", 3) == 0) {
-          mode = LOOP_DETECT_OFF;
-        } else if (memcmp(config, "minimal", 7) == 0) {
-          mode = LOOP_DETECT_MINIMAL;
-        } else if (memcmp(config, "moderate", 8) == 0) {
-          mode = LOOP_DETECT_MODERATE;
-        } else if (memcmp(config, "strict", 6) == 0) {
-          mode = LOOP_DETECT_STRICT;
-        } else {
-          mode = 0xFF;
-          strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
-        }
-        if (mode != 0xFF) {
-          _prefs->loop_detect = mode;
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "tx ", 3) == 0) {
-        _prefs->tx_power_dbm = atoi(&config[3]);
-        savePrefs();
-        _callbacks->setTxPower(_prefs->tx_power_dbm);
-        strcpy(reply, "OK");
-      } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
-        _prefs->freq = atof(&config[5]);
-        savePrefs();
-        strcpy(reply, "OK - reboot to apply");
-#ifdef WITH_BRIDGE
-      } else if (memcmp(config, "bridge.enabled ", 15) == 0) {
-        _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
-        _callbacks->setBridgeState(_prefs->bridge_enabled);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "bridge.delay ", 13) == 0) {
-        int delay = _atoi(&config[13]);
-        if (delay >= 0 && delay <= 10000) {
-          _prefs->bridge_delay = (uint16_t)delay;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error: delay must be between 0-10000 ms");
-        }
-      } else if (memcmp(config, "bridge.source ", 14) == 0) {
-        _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
-        savePrefs();
-        strcpy(reply, "OK");
-#endif
-#ifdef WITH_RS232_BRIDGE
-      } else if (memcmp(config, "bridge.baud ", 12) == 0) {
-        uint32_t baud = atoi(&config[12]);
-        if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
-          _prefs->bridge_baud = (uint32_t)baud;
-          _callbacks->restartBridge();
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
-        }
-#endif
-#ifdef WITH_ESPNOW_BRIDGE
-      } else if (memcmp(config, "bridge.channel ", 15) == 0) {
-        int ch = atoi(&config[15]);
-        if (ch > 0 && ch < 15) {
-          _prefs->bridge_channel = (uint8_t)ch;
-          _callbacks->restartBridge();
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error: channel must be between 1-14");
-        }
-      } else if (memcmp(config, "bridge.secret ", 14) == 0) {
-        StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
-        _callbacks->restartBridge();
-        savePrefs();
-        strcpy(reply, "OK");
-#endif
-      } else if (memcmp(config, "adc.multiplier ", 15) == 0) {
-        _prefs->adc_multiplier = atof(&config[15]);
-        if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
-          savePrefs();
-          if (_prefs->adc_multiplier == 0.0f) {
-            strcpy(reply, "OK - using default board multiplier");
-          } else {
-            sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
-          }
-        } else {
-          _prefs->adc_multiplier = 0.0f;
-          strcpy(reply, "Error: unsupported by this board");
-        };
-      } else if (memcmp(config, "battery.reporting ", 18) == 0) {
-        bool enabled = memcmp(&config[18], "on", 2) == 0;
-        if (_board->setBatteryReporting(enabled)) {
-          _prefs->battery_reporting_enabled = enabled ? 1 : 0;
-          savePrefs();
-          sprintf(reply, "OK - battery reporting %s", enabled ? "on" : "off");
-        } else {
-          strcpy(reply, "Error: unsupported by this board");
-        }
-      } else {
-        sprintf(reply, "unknown config: %s", config);
-      }
+      handleSetCmd(sender_timestamp, command, reply);
     } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
       bool s = _callbacks->formatFileSystem();
       sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
@@ -799,6 +357,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
           *(dp-1) = 0; // remove last CR
         }
       }
+    } else if (memcmp(command, "region", 6) == 0) {
+      handleRegionCmd(command, reply);
 #if ENV_INCLUDE_GPS == 1
     } else if (memcmp(command, "gps on", 6) == 0) {
       if (_sensors->setSettingValue("gps", "1")) {
@@ -916,3 +476,557 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       strcpy(reply, "Unknown command");
     }
 }
+
+void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
+  const char* config = &command[4];
+  if (memcmp(config, "dutycycle ", 10) == 0) {
+    float dc = atof(&config[10]);
+    if (dc < 1 || dc > 100) {
+      strcpy(reply, "ERROR: dutycycle must be 1-100");
+    } else {
+      _prefs->airtime_factor = (100.0f / dc) - 1.0f;
+      savePrefs();
+      float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
+      int a_int = (int)actual;
+      int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
+      sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
+    }
+  } else if (memcmp(config, "af ", 3) == 0) {
+    _prefs->airtime_factor = atof(&config[3]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "int.thresh ", 11) == 0) {
+    _prefs->interference_threshold = atoi(&config[11]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
+    _prefs->agc_reset_interval = atoi(&config[19]) / 4;
+    savePrefs();
+    sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
+  } else if (memcmp(config, "multi.acks ", 11) == 0) {
+    _prefs->multi_acks = atoi(&config[11]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "allow.read.only ", 16) == 0) {
+    _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
+    int hours = _atoi(&config[22]);
+    if ((hours > 0 && hours < 3) || (hours > 168)) {
+      strcpy(reply, "Error: interval range is 3-168 hours");
+    } else {
+      _prefs->flood_advert_interval = (uint8_t)(hours);
+      _callbacks->updateFloodAdvertTimer();
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "advert.interval ", 16) == 0) {
+    int mins = _atoi(&config[16]);
+    if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) {
+      sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
+    } else {
+      _prefs->advert_interval = (uint8_t)(mins / 2);
+      _callbacks->updateAdvertTimer();
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "guest.password ", 15) == 0) {
+    StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "prv.key ", 8) == 0) {
+    uint8_t prv_key[PRV_KEY_SIZE];
+    bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
+    // only allow rekey if key is valid
+    if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
+      mesh::LocalIdentity new_id;
+      new_id.readFrom(prv_key, PRV_KEY_SIZE);
+      _callbacks->saveIdentity(new_id);
+      strcpy(reply, "OK, reboot to apply! New pubkey: ");
+      mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
+    } else {
+      strcpy(reply, "Error, bad key");
+    }
+  } else if (memcmp(config, "name ", 5) == 0) {
+    if (isValidName(&config[5])) {
+      StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, bad chars");
+    }
+  } else if (memcmp(config, "repeat ", 7) == 0) {
+    _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
+    savePrefs();
+    strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
+#if defined(USE_SX1262) || defined(USE_SX1268)
+  } else if (memcmp(config, "radio.rxgain ", 13) == 0) {
+    _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0;
+    strcpy(reply, "OK");
+    savePrefs();
+    _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
+#endif
+  } else if (memcmp(config, "radio ", 6) == 0) {
+    strcpy(tmp, &config[6]);
+    const char *parts[4];
+    int num = mesh::Utils::parseTextParts(tmp, parts, 4);
+    float freq  = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
+    float bw    = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
+    uint8_t sf  = num > 2 ? atoi(parts[2]) : 0;
+    uint8_t cr  = num > 3 ? atoi(parts[3]) : 0;
+    if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
+      _prefs->sf = sf;
+      _prefs->cr = cr;
+      _prefs->freq = freq;
+      _prefs->bw = bw;
+      _callbacks->savePrefs();
+      strcpy(reply, "OK - reboot to apply");
+    } else {
+      strcpy(reply, "Error, invalid radio params");
+    }
+  } else if (memcmp(config, "lat ", 4) == 0) {
+    _prefs->node_lat = atof(&config[4]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "lon ", 4) == 0) {
+    _prefs->node_lon = atof(&config[4]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "rxdelay ", 8) == 0) {
+    float db = atof(&config[8]);
+    if (db >= 0) {
+      _prefs->rx_delay_base = db;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "txdelay ", 8) == 0) {
+    float f = atof(&config[8]);
+    if (f >= 0) {
+      _prefs->tx_delay_factor = f;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "flood.max ", 10) == 0) {
+    uint8_t m = atoi(&config[10]);
+    if (m <= 64) {
+      _prefs->flood_max = m;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, max 64");
+    }
+  } else if (memcmp(config, "direct.txdelay ", 15) == 0) {
+    float f = atof(&config[15]);
+    if (f >= 0) {
+      _prefs->direct_tx_delay_factor = f;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "owner.info ", 11) == 0) {
+    config += 11;
+    char *dp = _prefs->owner_info;
+    while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
+      *dp++ = (*config == '|') ? '\n' : *config;    // translate '|' to newline chars
+      config++;
+    }
+    *dp = 0;
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "path.hash.mode ", 15) == 0) {
+    config += 15;
+    uint8_t mode = atoi(config);
+    if (mode < 3) {
+      _prefs->path_hash_mode = mode;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, must be 0,1, or 2");
+    }
+  } else if (memcmp(config, "loop.detect ", 12) == 0) {
+    config += 12;
+    uint8_t mode;
+    if (memcmp(config, "off", 3) == 0) {
+      mode = LOOP_DETECT_OFF;
+    } else if (memcmp(config, "minimal", 7) == 0) {
+      mode = LOOP_DETECT_MINIMAL;
+    } else if (memcmp(config, "moderate", 8) == 0) {
+      mode = LOOP_DETECT_MODERATE;
+    } else if (memcmp(config, "strict", 6) == 0) {
+      mode = LOOP_DETECT_STRICT;
+    } else {
+      mode = 0xFF;
+      strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
+    }
+    if (mode != 0xFF) {
+      _prefs->loop_detect = mode;
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "tx ", 3) == 0) {
+    _prefs->tx_power_dbm = atoi(&config[3]);
+    savePrefs();
+    _callbacks->setTxPower(_prefs->tx_power_dbm);
+    strcpy(reply, "OK");
+  } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
+    _prefs->freq = atof(&config[5]);
+    savePrefs();
+    strcpy(reply, "OK - reboot to apply");
+#ifdef WITH_BRIDGE
+  } else if (memcmp(config, "bridge.enabled ", 15) == 0) {
+    _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
+    _callbacks->setBridgeState(_prefs->bridge_enabled);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "bridge.delay ", 13) == 0) {
+    int delay = _atoi(&config[13]);
+    if (delay >= 0 && delay <= 10000) {
+      _prefs->bridge_delay = (uint16_t)delay;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error: delay must be between 0-10000 ms");
+    }
+  } else if (memcmp(config, "bridge.source ", 14) == 0) {
+    _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
+    savePrefs();
+    strcpy(reply, "OK");
+#endif
+#ifdef WITH_RS232_BRIDGE
+  } else if (memcmp(config, "bridge.baud ", 12) == 0) {
+    uint32_t baud = atoi(&config[12]);
+    if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
+      _prefs->bridge_baud = (uint32_t)baud;
+      _callbacks->restartBridge();
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
+    }
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+  } else if (memcmp(config, "bridge.channel ", 15) == 0) {
+    int ch = atoi(&config[15]);
+    if (ch > 0 && ch < 15) {
+      _prefs->bridge_channel = (uint8_t)ch;
+      _callbacks->restartBridge();
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error: channel must be between 1-14");
+    }
+  } else if (memcmp(config, "bridge.secret ", 14) == 0) {
+    StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
+    _callbacks->restartBridge();
+    savePrefs();
+    strcpy(reply, "OK");
+#endif
+  } else if (memcmp(config, "adc.multiplier ", 15) == 0) {
+    _prefs->adc_multiplier = atof(&config[15]);
+    if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
+      savePrefs();
+      if (_prefs->adc_multiplier == 0.0f) {
+        strcpy(reply, "OK - using default board multiplier");
+      } else {
+        sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
+      }
+    } else {
+      _prefs->adc_multiplier = 0.0f;
+      strcpy(reply, "Error: unsupported by this board");
+    };
+  } else {
+    sprintf(reply, "unknown config: %s", config);
+  }
+}
+
+void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
+  const char* config = &command[4];
+  if (memcmp(config, "dutycycle", 9) == 0) {
+    float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
+    int dc_int = (int)dc;
+    int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
+    sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
+  } else if (memcmp(config, "af", 2) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
+  } else if (memcmp(config, "int.thresh", 10) == 0) {
+    sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
+  } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
+  } else if (memcmp(config, "multi.acks", 10) == 0) {
+    sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
+  } else if (memcmp(config, "allow.read.only", 15) == 0) {
+    sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
+  } else if (memcmp(config, "flood.advert.interval", 21) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
+  } else if (memcmp(config, "advert.interval", 15) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
+  } else if (memcmp(config, "guest.password", 14) == 0) {
+    sprintf(reply, "> %s", _prefs->guest_password);
+  } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) {  // from serial command line only
+    uint8_t prv_key[PRV_KEY_SIZE];
+    int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
+    mesh::Utils::toHex(tmp, prv_key, len);
+    sprintf(reply, "> %s", tmp);
+  } else if (memcmp(config, "name", 4) == 0) {
+    sprintf(reply, "> %s", _prefs->node_name);
+  } else if (memcmp(config, "repeat", 6) == 0) {
+    sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
+  } else if (memcmp(config, "lat", 3) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
+  } else if (memcmp(config, "lon", 3) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
+#if defined(USE_SX1262) || defined(USE_SX1268)
+  } else if (memcmp(config, "radio.rxgain", 12) == 0) {
+    sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
+#endif
+  } else if (memcmp(config, "radio", 5) == 0) {
+    char freq[16], bw[16];
+    strcpy(freq, StrHelper::ftoa(_prefs->freq));
+    strcpy(bw, StrHelper::ftoa3(_prefs->bw));
+    sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
+  } else if (memcmp(config, "rxdelay", 7) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
+  } else if (memcmp(config, "txdelay", 7) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
+  } else if (memcmp(config, "flood.max", 9) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
+  } else if (memcmp(config, "direct.txdelay", 14) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
+  } else if (memcmp(config, "owner.info", 10) == 0) {
+    *reply++ = '>';
+    *reply++ = ' ';
+    const char* sp = _prefs->owner_info;
+    while (*sp) {
+      *reply++ = (*sp == '\n') ? '|' : *sp;    // translate newline back to orig '|'
+      sp++;
+    }
+    *reply = 0;  // set null terminator
+  } else if (memcmp(config, "path.hash.mode", 14) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
+  } else if (memcmp(config, "loop.detect", 11) == 0) {
+    if (_prefs->loop_detect == LOOP_DETECT_OFF) {
+      strcpy(reply, "> off");
+    } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
+      strcpy(reply, "> minimal");
+    } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
+      strcpy(reply, "> moderate");
+    } else {
+      strcpy(reply, "> strict");
+    }
+  } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
+    sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
+  } else if (memcmp(config, "freq", 4) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
+  } else if (memcmp(config, "public.key", 10) == 0) {
+    strcpy(reply, "> ");
+    mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
+  } else if (memcmp(config, "role", 4) == 0) {
+    sprintf(reply, "> %s", _callbacks->getRole());
+  } else if (memcmp(config, "bridge.type", 11) == 0) {
+    sprintf(reply, "> %s",
+#ifdef WITH_RS232_BRIDGE
+            "rs232"
+#elif WITH_ESPNOW_BRIDGE
+            "espnow"
+#else
+            "none"
+#endif
+    );
+#ifdef WITH_BRIDGE
+  } else if (memcmp(config, "bridge.enabled", 14) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
+  } else if (memcmp(config, "bridge.delay", 12) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
+  } else if (memcmp(config, "bridge.source", 13) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
+#endif
+#ifdef WITH_RS232_BRIDGE
+  } else if (memcmp(config, "bridge.baud", 11) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+  } else if (memcmp(config, "bridge.channel", 14) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
+  } else if (memcmp(config, "bridge.secret", 13) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_secret);
+#endif
+  } else if (memcmp(config, "bootloader.ver", 14) == 0) {
+  #ifdef NRF52_PLATFORM
+      char ver[32];
+      if (_board->getBootloaderVersion(ver, sizeof(ver))) {
+          sprintf(reply, "> %s", ver);
+      } else {
+          strcpy(reply, "> unknown");
+      }
+  #else
+      strcpy(reply, "ERROR: unsupported");
+  #endif
+  } else if (memcmp(config, "adc.multiplier", 14) == 0) {
+    float adc_mult = _board->getAdcMultiplier();
+    if (adc_mult == 0.0f) {
+      strcpy(reply, "Error: unsupported by this board");
+    } else {
+      sprintf(reply, "> %.3f", adc_mult);
+    }
+  // Power management commands
+  } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    strcpy(reply, "> supported");
+#else
+    strcpy(reply, "> unsupported");
+#endif
+  } else if (memcmp(config, "pwrmgt.source", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    sprintf(reply, "> Reset: %s; Shutdown: %s",
+      _board->getResetReasonString(_board->getResetReason()),
+      _board->getShutdownReasonString(_board->getShutdownReason()));
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    sprintf(reply, "> %u mV", _board->getBootVoltage());
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else {
+    sprintf(reply, "??: %s", config);
+  }
+}
+
+void CommonCLI::handleRegionCmd(char* command, char* reply) {
+  reply[0] = 0;
+
+  const char* parts[4];
+  int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
+  if (n == 1) {
+    _region_map->exportTo(reply, 160);
+  } else if (n >= 2 && strcmp(parts[1], "load") == 0) {
+    _callbacks->startRegionsLoad();
+  } else if (n >= 2 && strcmp(parts[1], "save") == 0) {
+    _prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime();   // this node is now 'modified' (for discovery info)
+    savePrefs();
+    bool success = _callbacks->saveRegions();
+    strcpy(reply, success ? "OK" : "Err - save failed");
+  } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      region->flags &= ~REGION_DENY_FLOOD;
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      region->flags |= REGION_DENY_FLOOD;
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "get") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      auto parent = _region_map->findById(region->parent);
+      if (parent && parent->id != 0) {
+        sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+      } else {
+        sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+      }
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "home") == 0) {
+    auto home = _region_map->findByNamePrefix(parts[2]);
+    if (home) {
+      _region_map->setHomeRegion(home);
+      sprintf(reply, " home is now %s", home->name);
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n == 2 && strcmp(parts[1], "home") == 0) {
+    auto home = _region_map->getHomeRegion();
+    sprintf(reply, " home is %s", home ? home->name : "*");
+  } else if (n >= 3 && strcmp(parts[1], "default") == 0) {
+    if (strcmp(parts[2], "<null>") == 0) {
+      _region_map->setDefaultRegion(NULL);
+      _callbacks->onDefaultRegionChanged(NULL);
+      _callbacks->saveRegions();  // persist in one atomic step
+      sprintf(reply, " default scope is now <null>");
+    } else {
+      auto def = _region_map->findByNamePrefix(parts[2]);
+      if (def == NULL) {
+        def = _region_map->putRegion(parts[2], 0);  // auto-create the default region
+      }
+      if (def) {
+        def->flags = 0;   // make sure allow flood enabled
+        _region_map->setDefaultRegion(def);
+        _callbacks->onDefaultRegionChanged(def);
+        _callbacks->saveRegions();  // persist in one atomic step
+        sprintf(reply, " default scope is now %s", def->name);
+      } else {
+        strcpy(reply, "Err - region table full");
+      }
+    }
+  } else if (n == 2 && strcmp(parts[1], "default") == 0) {
+    auto def = _region_map->getDefaultRegion();
+    sprintf(reply, " default scope is %s", def ? def->name : "<null>");
+  } else if (n >= 3 && strcmp(parts[1], "put") == 0) {
+    auto parent = n >= 4 ? _region_map->findByNamePrefix(parts[3]) : &(_region_map->getWildcard());
+    if (parent == NULL) {
+      strcpy(reply, "Err - unknown parent");
+    } else {
+      auto region = _region_map->putRegion(parts[2], parent->id);
+      if (region == NULL) {
+        strcpy(reply, "Err - unable to put");
+      } else {
+        region->flags = 0;   // New default: enable flood
+        strcpy(reply, "OK - (flood allowed)");
+      }
+    }
+  } else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
+    auto region = _region_map->findByName(parts[2]);
+    if (region) {
+      if (_region_map->removeRegion(*region)) {
+        strcpy(reply, "OK");
+      } else {
+        strcpy(reply, "Err - not empty");
+      }
+    } else {
+      strcpy(reply, "Err - not found");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "list") == 0) {
+    uint8_t mask = 0;
+    bool invert = false;
+
+    if (strcmp(parts[2], "allowed") == 0) {
+      mask = REGION_DENY_FLOOD;
+      invert = false;  // list regions that DON'T have DENY flag
+    } else if (strcmp(parts[2], "denied") == 0) {
+      mask = REGION_DENY_FLOOD;
+      invert = true;   // list regions that DO have DENY flag
+    } else {
+      strcpy(reply, "Err - use 'allowed' or 'denied'");
+      return;
+    }
+
+    int len = _region_map->exportNamesTo(reply, 160, mask, invert);
+    if (len == 0) {
+      strcpy(reply, "-none-");
+    }
+  } else {
+    strcpy(reply, "Err - ??");
+  }
+}

+ 19 - 3
src/helpers/CommonCLI.h

@@ -4,6 +4,7 @@
 #include <helpers/IdentityStore.h>
 #include <helpers/SensorManager.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 
 #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
 #define WITH_BRIDGE
@@ -90,6 +91,16 @@ public:
   virtual void clearStats() = 0;
   virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
 
+  virtual void startRegionsLoad() {
+    // no op by default
+  }
+  virtual bool saveRegions() {
+    return false;
+  }
+  virtual void onDefaultRegionChanged(const RegionEntry* r) {
+    // no op by default
+  }
+
   virtual void setBridgeState(bool enable) {
     // no op by default
   };
@@ -109,6 +120,7 @@ class CommonCLI {
   CommonCLICallbacks* _callbacks;
   mesh::MainBoard* _board;
   SensorManager* _sensors;
+  RegionMap* _region_map;
   ClientACL* _acl;
   char tmp[PRV_KEY_SIZE*2 + 4];
 
@@ -116,12 +128,16 @@ class CommonCLI {
   void savePrefs();
   void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
 
+  void handleRegionCmd(char* command, char* reply);
+  void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply);
+  void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);
+
 public:
-  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
-      : _board(&board), _rtc(&rtc), _sensors(&sensors), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
+  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
+      : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
 
   void loadPrefs(FILESYSTEM* _fs);
   void savePrefs(FILESYSTEM* _fs);
-  void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
+  void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
   uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
 };

+ 4 - 0
src/helpers/ESP32Board.h

@@ -3,6 +3,10 @@
 #include <MeshCore.h>
 #include <Arduino.h>
 
+#ifndef USER_BTN_PRESSED
+#define USER_BTN_PRESSED LOW
+#endif
+
 #if defined(ESP_PLATFORM)
 
 #include <rom/rtc.h>

+ 34 - 17
src/helpers/RegionMap.cpp

@@ -42,7 +42,8 @@ private:
 
 
 RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
-  next_id = 1; num_regions = 0; home_id = 0;
+  next_id = 1; num_regions = 0;
+  default_id = home_id = 0;
   wildcard.id = wildcard.parent = 0;
   wildcard.flags = 0;  // default behaviour, allow flood and direct
   strcpy(wildcard.name, "*");
@@ -79,9 +80,11 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
     if (file) {
       uint8_t pad[128];
 
-      num_regions = 0; next_id = 1; home_id = 0;
+      num_regions = 0; next_id = 1;
+      default_id = home_id = 0;
 
-      bool success = file.read(pad, 5) == 5;  // reserved header
+      bool success = file.read(pad, 3) == 3;  // reserved header
+      success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
       success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
       success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
       success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
@@ -117,7 +120,8 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
     uint8_t pad[128];
     memset(pad, 0, sizeof(pad));
 
-    bool success = file.write(pad, 5) == 5;  // reserved header
+    bool success = file.write(pad, 3) == 3;  // reserved header
+    success = success && file.write((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
     success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
     success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
     success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
@@ -164,24 +168,29 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t
   return region;
 }
 
+int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) {
+  int num;
+  if (src.name[0] == '$') {   // private region
+    num = _store->loadKeysFor(src.id, dest, max_num);
+  } else if (src.name[0] == '#') {   // auto hashtag region
+    _store->getAutoKeyFor(src.id, src.name, dest[0]);
+    num = 1;
+  } else {   // new: implicit auto hashtag region
+    char tmp[sizeof(src.name)];
+    tmp[0] = '#';
+    strcpy(&tmp[1], src.name);
+    _store->getAutoKeyFor(src.id, tmp, dest[0]);
+    num = 1;
+  }
+  return num;
+}
+
 RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
   for (int i = 0; i < num_regions; i++) {
     auto region = &regions[i];
     if ((region->flags & mask) == 0) {   // does region allow this? (per 'mask' param)
       TransportKey keys[4];
-      int num;
-      if (region->name[0] == '$') {   // private region
-        num = _store->loadKeysFor(region->id, keys, 4);
-      } else if (region->name[0] == '#') {   // auto hashtag region
-        _store->getAutoKeyFor(region->id, region->name, keys[0]);
-        num = 1;
-      } else {   // new: implicit auto hashtag region
-        char tmp[sizeof(region->name)];
-        tmp[0] = '#';
-        strcpy(&tmp[1], region->name);
-        _store->getAutoKeyFor(region->id, tmp, keys[0]);
-        num = 1;
-      }
+      int num = getTransportKeysFor(*region, keys, 4);
       for (int j = 0; j < num; j++) {
         uint16_t code = keys[j].calcTransportCode(packet);
         if (packet->transport_codes[0] == code) {   // a match!!
@@ -237,6 +246,14 @@ void RegionMap::setHomeRegion(const RegionEntry* home) {
   home_id = home ? home->id : 0;
 }
 
+RegionEntry* RegionMap::getDefaultRegion() {
+  return default_id == 0 ? NULL : findById(default_id);
+}
+
+void RegionMap::setDefaultRegion(const RegionEntry* def) {
+  default_id = def ? def->id : 0;
+}
+
 bool RegionMap::removeRegion(const RegionEntry& region) {
   if (region.id == 0) return false;  // failed (cannot remove the wildcard Region)
 

+ 6 - 1
src/helpers/RegionMap.h

@@ -16,11 +16,13 @@ struct RegionEntry {
   uint16_t parent;
   uint8_t flags;
   char name[31];
+
+  bool isWildcard() const { return id == 0; }
 };
 
 class RegionMap {
   TransportKeyStore* _store;
-  uint16_t next_id, home_id;
+  uint16_t next_id, home_id, default_id;
   uint16_t num_regions;
   RegionEntry regions[MAX_REGION_ENTRIES];
   RegionEntry wildcard;
@@ -43,6 +45,8 @@ public:
   RegionEntry* findById(uint16_t id);
   RegionEntry* getHomeRegion();   // NOTE: can be NULL
   void setHomeRegion(const RegionEntry* home);
+  RegionEntry* getDefaultRegion();   // NOTE: can be NULL
+  void setDefaultRegion(const RegionEntry* def);
   bool removeRegion(const RegionEntry& region);
   bool clear();
   void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
@@ -50,6 +54,7 @@ public:
   const RegionEntry* getByIdx(int i) const { return &regions[i]; }
   const RegionEntry* getRoot() const { return &wildcard; }
   int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
+  int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);
 
   void    exportTo(Stream& out) const;
   size_t  exportTo(char *dest, size_t max_len) const;

+ 6 - 0
src/helpers/sensors/EnvironmentSensorManager.cpp

@@ -62,9 +62,15 @@ LPS22HBClass LPS22HB(*TELEM_WIRE);
 #endif
 
 #if ENV_INCLUDE_INA3221
+#ifndef TELEM_INA3221_ADDRESS
 #define TELEM_INA3221_ADDRESS   0x42      // INA3221 3 channel current sensor I2C address
+#endif
+#ifndef TELEM_INA3221_SHUNT_VALUE
 #define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
+#endif
+#ifndef TELEM_INA3221_NUM_CHANNELS
 #define TELEM_INA3221_NUM_CHANNELS 3
+#endif
 #include <Adafruit_INA3221.h>
 static Adafruit_INA3221 INA3221;
 #endif

+ 1 - 1
variants/heltec_v2/platformio.ini

@@ -178,7 +178,7 @@ build_flags =
   ${Heltec_lora32_v2.build_flags}
   -I examples/companion_radio/ui-new
   -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=160
+  -D MAX_CONTACTS=100
   -D MAX_GROUP_CHANNELS=8
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'

+ 4 - 1
variants/heltec_v3/HeltecV3Board.h

@@ -11,6 +11,9 @@
 #ifndef PIN_ADC_CTRL              // set in platformio.ini for Heltec Wireless Tracker (2)
   #define  PIN_ADC_CTRL    37
 #endif
+#ifndef ADC_MULTIPLIER            //default ADC multiplier
+  #define ADC_MULTIPLIER 5.42
+#endif
 #define  PIN_ADC_CTRL_ACTIVE    LOW
 #define  PIN_ADC_CTRL_INACTIVE  HIGH
 
@@ -95,7 +98,7 @@ public:
 
     digitalWrite(PIN_ADC_CTRL, !adc_active_state);
 
-    uint16_t mv = (5.42 * (3.3 / 1024.0) * raw) * 1000;
+    uint16_t mv = (ADC_MULTIPLIER * (3.3 / 1024.0) * raw) * 1000;
 
     // Heltec V3 USB-powered boards without a battery can bias or float the
     // VBAT sense rail, which shows up as an impossible LiPo voltage.

+ 2 - 0
variants/heltec_v4/platformio.ini

@@ -207,6 +207,7 @@ build_flags =
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
   -D DISPLAY_CLASS=SSD1306Display
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'
@@ -370,6 +371,7 @@ build_flags =
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
   -D DISPLAY_CLASS=ST7789LCDDisplay
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'

+ 1 - 0
variants/heltec_wireless_paper/platformio.ini

@@ -22,6 +22,7 @@ build_flags =
   ;-D PIN_BOARD_SCL=18               ; same GPIO as P_LORA_TX_LED
   -D PIN_USER_BTN=0
   -D PIN_VEXT_EN=45
+  -D ADC_MULTIPLIER=8.4
   -D PIN_VBAT_READ=20
   -D PIN_ADC_CTRL=19
   -D SX126X_DIO2_AS_RF_SWITCH=true

+ 1 - 0
variants/lilygo_tbeam_1w/platformio.ini

@@ -154,6 +154,7 @@ build_flags =
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'
   -D WIFI_PWD='"mypwd"'

+ 1 - 1
variants/lilygo_tlora_v2_1/platformio.ini

@@ -131,7 +131,7 @@ extends = LilyGo_TLora_V2_1_1_6
 build_flags =
   ${LilyGo_TLora_V2_1_1_6.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=160
+  -D MAX_CONTACTS=100
   -D MAX_GROUP_CHANNELS=8
   -D WIFI_SSID='"ssid"'
   -D WIFI_PWD='"password"'

+ 1 - 1
variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h

@@ -63,7 +63,7 @@ public:
     digitalWrite(LED_PIN, LOW);
     #endif
     #ifdef BUTTON_PIN
-    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_HIGH);
+    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
     #endif
     sd_power_system_off();
   }

+ 0 - 1
variants/minewsemi_me25ls01/platformio.ini

@@ -21,7 +21,6 @@ build_flags = ${nrf52840_me25ls01.build_flags}
   -I variants/minewsemi_me25ls01
   -D me25ls01
   -D PIN_USER_BTN=27
-  -D USER_BTN_PRESSED=HIGH
   -D PIN_STATUS_LED=39
   -D P_LORA_TX_LED=22
   -D RADIO_CLASS=CustomLR1110

+ 1 - 1
variants/t1000-e/T1000eBoard.h

@@ -43,7 +43,7 @@ public:
     uint8_t v = digitalRead(BUTTON_PIN);
     if (v != btn_prev_state) {
       btn_prev_state = v;
-      return (v == LOW) ? 1 : -1;
+      return (v == USER_BTN_PRESSED) ? 1 : -1;
     }
   #endif
     return 0;

+ 1 - 1
variants/t1000-e/platformio.ini

@@ -22,7 +22,7 @@ build_flags = ${nrf52_base.build_flags}
   -D P_LORA_NSS=12 ; P0.12
   -D P_LORA_DIO_1=33 ; P1.1
   -D P_LORA_MISO=40 ; P1.8
-  -D P_LORA_MOSI=41 ; P0.9
+  -D P_LORA_MOSI=41 ; P1.9
   -D P_LORA_RESET=42 ; P1.10
   -D LR11X0_DIO_AS_RF_SWITCH=true
   -D LR11X0_DIO3_TCXO_VOLTAGE=1.6

+ 1 - 1
variants/t1000-e/variant.cpp

@@ -55,7 +55,7 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] =
   42, // P1.10, LORA_RESET
   43, // P1.11, GPS_EN
   44, // P1.12, GPS_SLEEP_INT
-  45, // P1.13
+  45, // P1.13, FLASH_ENABLE
   46, // P1.14, GPS_RESETB
   47, // P1.15, PIN_GPS_RESET
   255,  // NRFX_SPIM_PIN_NOT_USED

+ 2 - 2
variants/t1000-e/variant.h

@@ -97,7 +97,7 @@
 #define LORA_BUSY               (7)             // P0.7
 #define LORA_SCLK               (PIN_SPI_SCK)   // P0.11
 #define LORA_MISO               (PIN_SPI_MISO)  // P1.8
-#define LORA_MOSI               (PIN_SPI_MOSI)  // P0.9
+#define LORA_MOSI               (PIN_SPI_MOSI)  // P1.9
  
 #define LR11X0_DIO_AS_RF_SWITCH    true
 #define LR11X0_DIO3_TCXO_VOLTAGE   1.6
@@ -133,4 +133,4 @@
 // Buzzer
 
 #define BUZZER_EN               (37)            // P1.5
-#define BUZZER_PIN              (25)            // P0.25
+#define BUZZER_PIN              (25)            // P0.25

+ 0 - 1
variants/thinknode_m3/platformio.ini

@@ -10,7 +10,6 @@ build_flags = ${nrf52_base.build_flags}
   -I src/helpers/ui
   -D THINKNODE_M3
   -D PIN_USER_BTN=12
-  -D USER_BTN_PRESSED=LOW
   -D PIN_STATUS_LED=35
   -D RADIO_CLASS=CustomLR1110
   -D WRAPPER_CLASS=CustomLR1110Wrapper

+ 2 - 1
variants/waveshare_rp2040_lora/platformio.ini

@@ -20,7 +20,8 @@ build_flags = ${rp2040_base.build_flags}
   -D P_LORA_MOSI=15
   -D P_LORA_TX_LED=25
   -D SX126X_DIO2_AS_RF_SWITCH=true
-  -D SX126X_DIO3_TCXO_VOLTAGE=0
+  -D SX126X_RXEN=17
+  -D SX126X_DIO3_TCXO_VOLTAGE=1.8
   -D SX126X_RX_BOOSTED_GAIN=1
   -D LORA_TX_POWER=22
 ; Debug options

+ 0 - 1
variants/wio-e5-mini/platformio.ini

@@ -9,7 +9,6 @@ build_flags = ${stm32_base.build_flags}
   -D RX_BOOSTED_GAIN=true
   -D P_LORA_TX_LED=LED_RED
   -D PIN_USER_BTN=USER_BTN
-  -D USER_BTN_PRESSED=LOW
   -I variants/wio-e5-mini
 build_src_filter = ${stm32_base.build_src_filter}
   +<../variants/wio-e5-mini>

+ 5 - 1
variants/xiao_nrf52/XiaoNrf52Board.h

@@ -4,6 +4,10 @@
 #include <Arduino.h>
 #include <helpers/NRF52Board.h>
 
+#ifndef USER_BTN_PRESSED
+#define USER_BTN_PRESSED LOW
+#endif
+
 #ifdef XIAO_NRF52
 
 class XiaoNrf52Board : public NRF52BoardDCDC {
@@ -35,7 +39,7 @@ public:
     // set led on and wait for button release before poweroff
     digitalWrite(PIN_LED, LOW);
 #ifdef PIN_USER_BTN
-    while(digitalRead(PIN_USER_BTN) == LOW);
+    while(digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED);
 #endif
     digitalWrite(LED_GREEN, HIGH);
     digitalWrite(LED_BLUE, HIGH);

+ 2 - 2
variants/xiao_nrf52/platformio.ini

@@ -72,11 +72,11 @@ build_flags =
   -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
   -D QSPIFLASH=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Xiao_nrf52.build_src_filter}
-  +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-orig/*.cpp>
 lib_deps =
@@ -117,4 +117,4 @@ build_flags =
 build_src_filter = ${Xiao_nrf52.build_src_filter}
   +<../examples/kiss_modem/*.cpp>
 lib_deps =
-  ${Xiao_nrf52.lib_deps}
+  ${Xiao_nrf52.lib_deps}