Ver código fonte

Merge branch 'dev' into nrf-dcdc

fdlamotte 6 meses atrás
pai
commit
3f3978c7d3
92 arquivos alterados com 3339 adições e 393 exclusões
  1. 74 0
      boards/meshtiny.json
  2. 72 0
      boards/rak3401.json
  3. 24 0
      build.sh
  4. 213 0
      docs/nrf52_power_management.md
  5. 52 1
      docs/payloads.md
  6. 2 0
      examples/companion_radio/DataStore.cpp
  7. 120 26
      examples/companion_radio/MyMesh.cpp
  8. 4 0
      examples/companion_radio/MyMesh.h
  9. 1 0
      examples/companion_radio/NodePrefs.h
  10. 2 6
      examples/companion_radio/main.cpp
  11. 116 16
      examples/simple_repeater/MyMesh.cpp
  12. 9 3
      examples/simple_repeater/MyMesh.h
  13. 4 2
      examples/simple_repeater/main.cpp
  14. 10 7
      examples/simple_room_server/MyMesh.cpp
  15. 2 2
      examples/simple_room_server/MyMesh.h
  16. 4 2
      examples/simple_room_server/main.cpp
  17. 2 0
      examples/simple_secure_chat/main.cpp
  18. 9 6
      examples/simple_sensor/SensorMesh.cpp
  19. 2 2
      examples/simple_sensor/SensorMesh.h
  20. 4 2
      examples/simple_sensor/main.cpp
  21. 1 0
      platformio.ini
  22. 44 0
      src/Identity.cpp
  23. 7 0
      src/Identity.h
  24. 8 0
      src/MeshCore.h
  25. 97 37
      src/helpers/BaseChatMesh.cpp
  26. 8 0
      src/helpers/BaseChatMesh.h
  27. 16 3
      src/helpers/ClientACL.cpp
  28. 3 1
      src/helpers/ClientACL.h
  29. 79 13
      src/helpers/CommonCLI.cpp
  30. 6 3
      src/helpers/CommonCLI.h
  31. 217 0
      src/helpers/NRF52Board.cpp
  32. 43 0
      src/helpers/NRF52Board.h
  33. 108 16
      src/helpers/RegionMap.cpp
  34. 9 4
      src/helpers/RegionMap.h
  35. 3 2
      src/helpers/StatsFormatHelper.h
  36. 12 2
      src/helpers/esp32/SerialBLEInterface.cpp
  37. 7 1
      src/helpers/esp32/SerialBLEInterface.h
  38. 13 3
      src/helpers/nrf52/SerialBLEInterface.cpp
  39. 8 1
      src/helpers/nrf52/SerialBLEInterface.h
  40. 1 0
      src/helpers/radiolib/RadioLibWrappers.cpp
  41. 3 2
      src/helpers/radiolib/RadioLibWrappers.h
  42. 6 5
      src/helpers/sensors/EnvironmentSensorManager.cpp
  43. 12 2
      src/helpers/ui/SSD1306Display.cpp
  44. 8 1
      src/helpers/ui/SSD1306Display.h
  45. 4 1
      src/helpers/ui/ST7789LCDDisplay.cpp
  46. 2 2
      src/helpers/ui/ST7789LCDDisplay.h
  47. 34 0
      variants/heltec_t114/T114Board.cpp
  48. 9 4
      variants/heltec_t114/T114Board.h
  49. 1 0
      variants/heltec_t114/platformio.ini
  50. 10 2
      variants/heltec_t114/variant.h
  51. 1 1
      variants/heltec_v3/platformio.ini
  52. 5 1
      variants/heltec_v4/HeltecV4Board.cpp
  53. 230 36
      variants/heltec_v4/platformio.ini
  54. 1 1
      variants/heltec_v4/target.cpp
  55. 5 1
      variants/heltec_v4/target.h
  56. 4 4
      variants/heltec_wireless_paper/platformio.ini
  57. 24 24
      variants/ikoka_handheld_nrf/platformio.ini
  58. 36 68
      variants/ikoka_nano_nrf/platformio.ini
  59. 31 54
      variants/ikoka_stick_nrf/platformio.ini
  60. 0 2
      variants/lilygo_t3s3_sx1276/platformio.ini
  61. 44 0
      variants/meshtiny/MeshtinyBoard.cpp
  62. 66 0
      variants/meshtiny/MeshtinyBoard.h
  63. 68 0
      variants/meshtiny/platformio.ini
  64. 47 0
      variants/meshtiny/target.cpp
  65. 33 0
      variants/meshtiny/target.h
  66. 51 0
      variants/meshtiny/variant.cpp
  67. 98 0
      variants/meshtiny/variant.h
  68. 96 0
      variants/rak3112/RAK3112Board.h
  69. 223 0
      variants/rak3112/platformio.ini
  70. 60 0
      variants/rak3112/target.cpp
  71. 30 0
      variants/rak3112/target.h
  72. 32 0
      variants/rak3401/RAK3401Board.cpp
  73. 70 0
      variants/rak3401/RAK3401Board.h
  74. 127 0
      variants/rak3401/platformio.ini
  75. 58 0
      variants/rak3401/target.cpp
  76. 30 0
      variants/rak3401/target.h
  77. 52 0
      variants/rak3401/variant.cpp
  78. 199 0
      variants/rak3401/variant.h
  79. 27 0
      variants/rak4631/RAK4631Board.cpp
  80. 5 0
      variants/rak4631/RAK4631Board.h
  81. 1 0
      variants/rak4631/platformio.ini
  82. 8 0
      variants/rak4631/variant.h
  83. 2 1
      variants/station_g2/platformio.ini
  84. 1 0
      variants/t1000-e/platformio.ini
  85. 1 0
      variants/thinknode_m1/platformio.ini
  86. 1 1
      variants/thinknode_m1/target.cpp
  87. 1 0
      variants/thinknode_m1/target.h
  88. 1 0
      variants/wio-tracker-l1/platformio.ini
  89. 43 4
      variants/xiao_nrf52/XiaoNrf52Board.cpp
  90. 6 15
      variants/xiao_nrf52/XiaoNrf52Board.h
  91. 1 0
      variants/xiao_nrf52/platformio.ini
  92. 15 0
      variants/xiao_nrf52/variant.h

+ 74 - 0
boards/meshtiny.json

@@ -0,0 +1,74 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "nrf52840_s140_v6.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
+    "f_cpu": "64000000L",
+    "hwids": [
+      [
+        "0x239A",
+        "0x8029"
+      ],
+      [
+        "0x239A",
+        "0x0029"
+      ],
+      [
+        "0x239A",
+        "0x002A"
+      ],
+      [
+        "0x239A",
+        "0x802A"
+      ]
+    ],
+    "usb_product": "Meshtiny",
+    "mcu": "nrf52840",
+    "variant": "meshtiny",
+    "bsp": {
+      "name": "adafruit"
+    },
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "6.1.1",
+      "sd_fwid": "0x00B6"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    }
+  },
+  "connectivity": [
+    "bluetooth"
+  ],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "svd_path": "nrf52840.svd",
+    "openocd_target": "nrf52840-mdk-rs"
+  },
+  "frameworks": [
+    "arduino",
+    "freertos"
+  ],
+  "name": "Meshtiny",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "speed": 115200,
+    "protocol": "nrfutil",
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "stlink"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://shop.mtoolstec.com/product/meshtiny",
+  "vendor": "MTools Tec"
+}

+ 72 - 0
boards/rak3401.json

@@ -0,0 +1,72 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "nrf52840_s140_v6.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
+    "f_cpu": "64000000L",
+    "hwids": [
+      [
+        "0x239A",
+        "0x8029"
+      ],
+      [
+        "0x239A",
+        "0x0029"
+      ],
+      [
+        "0x239A",
+        "0x002A"
+      ],
+      [
+        "0x239A",
+        "0x802A"
+      ]
+    ],
+    "usb_product": "WisCore RAK3401 Board",
+    "mcu": "nrf52840",
+    "variant": "WisCore_RAK3401_Board",
+    "bsp": {
+      "name": "adafruit"
+    },
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "6.1.1",
+      "sd_fwid": "0x00B6"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    }
+  },
+  "connectivity": [
+    "bluetooth"
+  ],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "svd_path": "nrf52840.svd"
+  },
+  "frameworks": [
+    "arduino"
+  ],
+  "name": "WisCore RAK3401 Board",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "speed": 115200,
+    "protocol": "nrfutil",
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "stlink"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://www.rakwireless.com",
+  "vendor": "RAKwireless"
+}

+ 24 - 0
build.sh

@@ -29,6 +29,20 @@ $ sh build.sh build-repeater-firmwares
 
 Build all chat room server firmwares
 $ sh build.sh build-room-server-firmwares
+
+Environment Variables:
+  DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.)
+                   If not set, debug flags from variant platformio.ini files are used.
+
+Examples:
+Build without debug logging:
+$ export FIRMWARE_VERSION=v1.0.0
+$ export DISABLE_DEBUG=1
+$ sh build.sh build-firmware RAK_4631_repeater
+
+Build with debug logging (default, uses flags from variant files):
+$ export FIRMWARE_VERSION=v1.0.0
+$ sh build.sh build-firmware RAK_4631_repeater
 EOF
 }
 
@@ -68,6 +82,13 @@ get_pio_envs_ending_with_string() {
   done
 }
 
+# disable all debug logging flags if DISABLE_DEBUG=1 is set
+disable_debug_flags() {
+  if [ "$DISABLE_DEBUG" == "1" ]; then
+    export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -UCFG_DEBUG -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
+  fi
+}
+
 # build firmware for the provided pio env in $1
 build_firmware() {
 
@@ -94,6 +115,9 @@ build_firmware() {
   # add firmware version info to end of existing platformio build flags in environment vars
   export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
 
+  # disable debug flags if requested
+  disable_debug_flags
+
   # build firmware target
   pio run -e $1
 

+ 213 - 0
docs/nrf52_power_management.md

@@ -0,0 +1,213 @@
+# nRF52 Power Management
+
+## Overview
+
+The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
+
+## Features
+
+### Boot Voltage Protection
+- Checks battery voltage immediately after boot and before mesh operations commence
+- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
+- Prevents boot loops when battery is critically low
+- Skipped when external power (USB VBUS) is detected
+
+### Voltage Wake (LPCOMP + VBUS)
+- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
+- Enables USB VBUS detection so external power can wake the device
+- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
+
+### Early Boot Register Capture
+- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
+- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
+- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
+
+### Shutdown Reason Tracking
+Shutdown reason codes (stored in GPREGRET2):
+| Code | Name | Description |
+|------|------|-------------|
+| 0x00 | NONE | Normal boot / no previous shutdown |
+| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
+| 0x55 | USER | User requested powerOff() |
+| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
+
+## Supported Boards
+
+| Board | Implemented | LPCOMP wake | VBUS wake |
+|-------|-------------|-------------|-----------|
+| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
+| RAK4631 (`rak4631`) | Yes | Yes | Yes |
+| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes |
+| Promicro nRF52840 | No | No | No |
+| RAK WisMesh Tag | No | No | No |
+| Heltec Mesh Solar | No | No | No |
+| LilyGo T-Echo / T-Echo Lite | No | No | No |
+| SenseCAP Solar | No | No | No |
+| WIO Tracker L1 / L1 E-Ink | No | No | No |
+| WIO WM1110 | No | No | No |
+| Mesh Pocket | No | No | No |
+| Nano G2 Ultra | No | No | No |
+| ThinkNode M1/M3/M6 | No | No | No |
+| T1000-E | No | No | No |
+| Ikoka Nano/Stick/Handheld (nRF) | No | No | No |
+| Keepteen LT1 | No | No | No |
+| Minewsemi ME25LS01 | No | No | No |
+
+Notes:
+- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
+- User power-off on Heltec T114 does not enable LPCOMP wake.
+- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
+
+## Technical Details
+
+### Architecture
+
+The power management functionality is integrated into the `NRF52Board` base class in `src/helpers/NRF52Board.cpp`. Board variants provide hardware-specific configuration via a `PowerMgtConfig` struct and override `initiateShutdown(uint8_t reason)` to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
+
+### Early Boot Capture
+
+A static constructor with priority 101 in `NRF52Board.cpp` captures the RESETREAS and GPREGRET2 registers before:
+- SystemInit() (priority 102) - which clears RESETREAS
+- Static C++ constructors (default priority 65535)
+
+This ensures we capture the true reset reason before any initialisation code runs.
+
+### Board Implementation
+
+To enable power management on a board variant:
+
+1. **Enable in platformio.ini**:
+   ```ini
+   -D NRF52_POWER_MANAGEMENT
+   ```
+
+2. **Define configuration in variant.h**:
+   ```c
+   #define PWRMGT_VOLTAGE_BOOTLOCK    3300   // Won't boot below this voltage (mV)
+   #define PWRMGT_LPCOMP_AIN          7      // AIN channel for voltage sensing
+   #define PWRMGT_LPCOMP_REFSEL       2      // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
+   ```
+
+3. **Implement in board .cpp file**:
+   ```cpp
+   #ifdef NRF52_POWER_MANAGEMENT
+   const PowerMgtConfig power_config = {
+     .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+     .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+     .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+   };
+
+   void MyBoard::initiateShutdown(uint8_t reason) {
+     // Board-specific shutdown preparation (e.g., disable peripherals)
+     bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+                           reason == SHUTDOWN_REASON_BOOT_PROTECT);
+
+     if (enable_lpcomp) {
+       configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+     }
+
+     enterSystemOff(reason);
+   }
+   #endif
+
+   void MyBoard::begin() {
+     NRF52Board::begin();  // or NRF52BoardDCDC::begin()
+     // ... board setup ...
+
+   #ifdef NRF52_POWER_MANAGEMENT
+     checkBootVoltage(&power_config);
+   #endif
+   }
+   ```
+
+   For user-initiated shutdowns, `powerOff()` remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
+
+4. **Declare override in board .h file**:
+   ```cpp
+   #ifdef NRF52_POWER_MANAGEMENT
+     void initiateShutdown(uint8_t reason) override;
+   #endif
+   ```
+
+### Voltage Wake Configuration
+
+The LPCOMP (Low Power Comparator) is configured to:
+- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
+- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
+- Detect UP events (voltage rising above threshold)
+- Use 50mV hysteresis for noise immunity
+- Wake the device from SYSTEMOFF when triggered
+
+VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
+
+**LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**:
+| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
+|--------|----------|------------------------------------|--------------------------------------|
+| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
+| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
+| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
+| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
+| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
+| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
+| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
+| 7 | ARef | - | - |
+| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
+| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
+| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
+| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
+| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
+| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
+| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
+| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
+
+**Important**: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
+`VBAT_threshold ≈ (VDD * fraction) * divider_scale`, where `divider_scale = (Rtop + Rbottom) / Rbottom` (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
+
+### SoftDevice Compatibility
+
+The power management code checks whether SoftDevice is enabled and uses the appropriate API:
+- When SD enabled: `sd_power_*` functions
+- When SD disabled: Direct register access (NRF_POWER->*)
+
+This ensures compatibility regardless of BLE stack state.
+
+## CLI Commands
+
+Power management status can be queried via the CLI:
+
+| Command | Description |
+|---------|-------------|
+| `get pwrmgt.support` | Returns "supported" or "unsupported" |
+| `get pwrmgt.source` | Returns current power source - "battery" or "external" (5V/USB power) |
+| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
+| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
+
+On boards without power management enabled, all commands except `get pwrmgt.support` return:
+```
+ERROR: Power management not supported
+```
+
+## Debug Output
+
+When `MESH_DEBUG=1` is enabled, the power management module outputs:
+```
+DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
+DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
+DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
+```
+
+## Phase 2 (Planned)
+
+- Runtime voltage monitoring
+- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
+- Configurable thresholds
+- Load shedding callbacks for power reduction
+- Deep sleep integration
+- Scheduled wake-up
+- Extended sleep with periodic monitoring
+
+## References
+
+- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
+- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
+- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group__nrf__sdm__api.html)

+ 52 - 1
docs/payloads.md

@@ -103,7 +103,9 @@ Request type
 | `0x02` | keepalive            | (deprecated) |
 | `0x03` | get telemetry data   | TODO |
 | `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
-| `0x05` | get access list      | get node's approved access list       |
+| `0x05` | get access list      | get node's approved access list            |
+| `0x06` | get neighbors        | get repeater node's neighbors              |
+| `0x07` | get owner info       | get repeater firmware-ver/name/owner info  |
 
 ### Get stats
 
@@ -132,6 +134,27 @@ Gets information about the node, possibly including the following:
 
 Request data about sensors on the node, including battery level.
 
+### Get Telemetry
+
+TODO
+
+### Get Min/Max/Ave  (Sensor nodes)
+
+TODO
+
+### Get Access List
+
+TODO
+
+### Get Neighors
+
+TODO
+
+### Get Owner Info
+
+TODO
+
+
 ## Response
 
 | Field   | Size (bytes)    | Description |
@@ -179,6 +202,34 @@ txt_type
 | timestamp      | 4               | sender time (unix timestamp)                                                  |
 | password       | rest of message | password for repeater/sensor                                                  |
 
+## Repeater - Regions request
+
+| Field          | Size (bytes)    | Description                                                                   |
+|----------------|-----------------|-------------------------------------------------------------------------------|
+| timestamp      | 4               | sender time (unix timestamp)                                                  |
+| req type       | 1               | 0x01 (request sub type)                                                       |
+| reply path len | 1               | path len for reply                                                       |
+| reply path     | (variable)      | reply path                                                       |
+
+## Repeater - Owner info request
+
+| Field          | Size (bytes)    | Description                                                                   |
+|----------------|-----------------|-------------------------------------------------------------------------------|
+| timestamp      | 4               | sender time (unix timestamp)                                                  |
+| req type       | 1               | 0x02 (request sub type)                                                       |
+| reply path len | 1               | path len for reply                                                       |
+| reply path     | (variable)      | reply path                                                       |
+
+## Repeater - Clock and status request
+
+| Field          | Size (bytes)    | Description                                                                   |
+|----------------|-----------------|-------------------------------------------------------------------------------|
+| timestamp      | 4               | sender time (unix timestamp)                                                  |
+| req type       | 1               | 0x03 (request sub type)                                                       |
+| reply path len | 1               | path len for reply                                                       |
+| reply path     | (variable)      | reply path                                                       |
+
+
 # Group text message / datagram
 
 | Field        | Size (bytes)    | Description                                |

+ 2 - 0
examples/companion_radio/DataStore.cpp

@@ -227,6 +227,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));               // 84
     file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));                 // 85
     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.close();
   }
@@ -261,6 +262,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));               // 84
     file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));                 // 85
     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.close();
   }

+ 120 - 26
examples/companion_radio/MyMesh.cpp

@@ -53,6 +53,9 @@
 #define CMD_SET_FLOOD_SCOPE           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
+#define CMD_SET_AUTOADD_CONFIG        58
+#define CMD_GET_AUTOADD_CONFIG        59
 
 // Stats sub-types for CMD_GET_STATS
 #define STATS_TYPE_CORE               0
@@ -84,6 +87,7 @@
 #define RESP_CODE_ADVERT_PATH         22
 #define RESP_CODE_TUNING_PARAMS       23
 #define RESP_CODE_STATS               24   // v8+, second byte is stats type
+#define RESP_CODE_AUTOADD_CONFIG      25
 
 #define SEND_TIMEOUT_BASE_MILLIS        500
 #define FLOOD_SEND_TIMEOUT_FACTOR       16.0f
@@ -109,6 +113,8 @@
 #define PUSH_CODE_BINARY_RESPONSE       0x8C
 #define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
 #define PUSH_CODE_CONTROL_DATA          0x8E   // v8+
+#define PUSH_CODE_CONTACT_DELETED       0x8F // used to notify client app of deleted contact when overwriting oldest
+#define PUSH_CODE_CONTACTS_FULL         0x90 // used to notify client app that contacts storage is full
 
 #define ERR_CODE_UNSUPPORTED_CMD        1
 #define ERR_CODE_NOT_FOUND              2
@@ -119,6 +125,15 @@
 
 #define MAX_SIGN_DATA_LEN               (8 * 1024) // 8K
 
+// Auto-add config bitmask
+// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full
+// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01
+#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0)  // 0x01 - overwrite oldest non-favourite when full
+#define AUTO_ADD_CHAT             (1 << 1)  // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
+#define AUTO_ADD_REPEATER         (1 << 2)  // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
+#define AUTO_ADD_ROOM_SERVER      (1 << 3)  // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
+#define AUTO_ADD_SENSOR           (1 << 4)  // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
+
 void MyMesh::writeOKFrame() {
   uint8_t buf[1];
   buf[0] = RESP_CODE_OK;
@@ -261,20 +276,64 @@ bool MyMesh::isAutoAddEnabled() const {
   return (_prefs.manual_add_contacts & 1) == 0;
 }
 
+bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const {
+  if ((_prefs.manual_add_contacts & 1) == 0) {
+    return true;
+  }
+  
+  uint8_t type_bit = 0;
+  switch (contact_type) {
+    case ADV_TYPE_CHAT:
+      type_bit = AUTO_ADD_CHAT;
+      break;
+    case ADV_TYPE_REPEATER:
+      type_bit = AUTO_ADD_REPEATER;
+      break;
+    case ADV_TYPE_ROOM:
+      type_bit = AUTO_ADD_ROOM_SERVER;
+      break;
+    case ADV_TYPE_SENSOR:
+      type_bit = AUTO_ADD_SENSOR;
+      break;
+    default:
+      return false;  // Unknown type, don't auto-add
+  }
+  
+  return (_prefs.autoadd_config & type_bit) != 0;
+}
+
+bool MyMesh::shouldOverwriteWhenFull() const {
+  return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
+}
+
+void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
+  if (_serial->isConnected()) {
+    out_frame[0] = PUSH_CODE_CONTACT_DELETED;
+    memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
+    _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
+  }
+}
+
+void MyMesh::onContactsFull() {
+  if (_serial->isConnected()) {
+    out_frame[0] = PUSH_CODE_CONTACTS_FULL;
+    _serial->writeFrame(out_frame, 1);
+  }
+}
+
 void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
   if (_serial->isConnected()) {
-    if (!isAutoAddEnabled() && is_new) {
+    if (is_new) {
       writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact);
     } else {
       out_frame[0] = PUSH_CODE_ADVERT;
       memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
       _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
     }
-  } else {
+  } 
 #ifdef DISPLAY_CLASS
-    if (_ui) _ui->notify(UIEventType::newContactMessage);
+  if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled
 #endif
-  }
 
   // add inbound-path to mem cache
   if (path && path_len <= sizeof(AdvertPath::path)) {  // check path is valid
@@ -298,7 +357,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
     memcpy(p->path, path, p->path_len);
   }
 
-  dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
+  if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
 }
 
 static int sort_by_recent(const void *a, const void *b) {
@@ -381,9 +440,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
   bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
   if (should_display && _ui) {
     _ui->newMsg(path_len, from.name, text, offline_queue_len);
-    if (!_serial->isConnected()) {
-      _ui->notify(UIEventType::contactMessage);
-    }
+    if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
   }
 #endif
 }
@@ -468,11 +525,8 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
     uint8_t frame[1];
     frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
     _serial->writeFrame(frame, 1);
-  } else {
-#ifdef DISPLAY_CLASS
-    if (_ui) _ui->notify(UIEventType::channelMessage);
-#endif
   }
+
 #ifdef DISPLAY_CLASS
   // Get the channel name from the channel index
   const char *channel_name = "Unknown";
@@ -480,7 +534,10 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
   if (getChannel(channel_idx, channel_details)) {
     channel_name = channel_details.name;
   }
-  if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
+  if (_ui) {
+    _ui->newMsg(path_len, channel_name, text, offline_queue_len);
+    if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled
+  }
 #endif
 }
 
@@ -739,6 +796,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
   _prefs.bw = LORA_BW;
   _prefs.cr = LORA_CR;
   _prefs.tx_power_dbm = LORA_TX_POWER;
+  _prefs.buzzer_quiet = 0;
   _prefs.gps_enabled = 0;       // GPS disabled by default
   _prefs.gps_interval = 0;      // No automatic GPS updates by default
   //_prefs.rx_delay_base = 10.0f;  enable once new algo fixed
@@ -757,14 +815,14 @@ void MyMesh::begin(bool has_display) {
     _store->saveMainIdentity(self_id);
   }
 
+// if name is provided as a build flag, use that as default node name instead
+#ifdef ADVERT_NAME
+  strcpy(_prefs.node_name, ADVERT_NAME);
+#else
   // use hex of first 4 bytes of identity public key as default node name
   char pub_key_hex[10];
   mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4);
   strcpy(_prefs.node_name, pub_key_hex);
-
-// if name is provided as a build flag, use that as default node name instead
-#ifdef ADVERT_NAME
-  strcpy(_prefs.node_name, ADVERT_NAME);
 #endif
 
   // load persisted prefs
@@ -778,6 +836,7 @@ void MyMesh::begin(bool has_display) {
   _prefs.sf = constrain(_prefs.sf, 5, 12);
   _prefs.cr = constrain(_prefs.cr, 5, 8);
   _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
+  _prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1);  // Ensure boolean 0 or 1
   _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1);  // Ensure boolean 0 or 1
   _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400);  // Max 24 hours
 
@@ -802,6 +861,7 @@ void MyMesh::begin(bool has_display) {
 
   resetContacts();
   _store->loadContacts(this);
+  bootstrapRTCfromContacts();
   addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
   _store->loadChannels(this);
 
@@ -1234,16 +1294,20 @@ void MyMesh::handleCmdFrame(size_t len) {
 #endif
   } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) {
 #if ENABLE_PRIVATE_KEY_IMPORT
-    mesh::LocalIdentity identity;
-    identity.readFrom(&cmd_frame[1], 64);
-    if (_store->saveMainIdentity(identity)) {
-      self_id = identity;
-      writeOKFrame();
-      // re-load contacts, to invalidate ecdh shared_secrets
-      resetContacts();
-      _store->loadContacts(this);
+    if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) {
+        writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key
     } else {
-      writeErrFrame(ERR_CODE_FILE_IO_ERROR);
+        mesh::LocalIdentity identity;
+        identity.readFrom(&cmd_frame[1], 64);
+        if (_store->saveMainIdentity(identity)) {
+          self_id = identity;
+          writeOKFrame();
+          // re-load contacts, to invalidate ecdh shared_secrets
+          resetContacts();
+          _store->loadContacts(this);
+        } else {
+          writeErrFrame(ERR_CODE_FILE_IO_ERROR);
+        }
     }
 #else
     writeDisabledFrame();
@@ -1286,6 +1350,27 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
     }
+  } else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) {
+    uint8_t *pub_key = &cmd_frame[1];
+    ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
+    uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE];
+    if (recipient) {
+      uint32_t tag, est_timeout;
+      int result = sendAnonReq(*recipient, data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
+      if (result == MSG_SEND_FAILED) {
+        writeErrFrame(ERR_CODE_TABLE_FULL);
+      } else {
+        clearPendingReqs();
+        pending_req = tag; // match this to onContactResponse()
+        out_frame[0] = RESP_CODE_SENT;
+        out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
+        memcpy(&out_frame[2], &tag, 4);
+        memcpy(&out_frame[6], &est_timeout, 4);
+        _serial->writeFrame(out_frame, 10);
+      }
+    } else {
+      writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
+    }
   } else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) {
     uint8_t *pub_key = &cmd_frame[1];
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
@@ -1641,6 +1726,15 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_TABLE_FULL);
     }
+  } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
+    _prefs.autoadd_config = cmd_frame[1];
+    savePrefs();
+    writeOKFrame();  
+  } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
+    int i = 0;
+    out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
+    out_frame[i++] = _prefs.autoadd_config;
+    _serial->writeFrame(out_frame, i);
   } else {
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

+ 4 - 0
examples/companion_radio/MyMesh.h

@@ -114,6 +114,10 @@ protected:
 
   void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
   bool isAutoAddEnabled() const override;
+  bool shouldAutoAddContactType(uint8_t type) const override;
+  bool shouldOverwriteWhenFull() const override;
+  void onContactsFull() override;
+  void onContactOverwrite(const uint8_t* pub_key) override;
   bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
   void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
   void onContactPathUpdated(const ContactInfo &contact) override;

+ 1 - 0
examples/companion_radio/NodePrefs.h

@@ -27,4 +27,5 @@ struct NodePrefs {  // persisted to file
   uint8_t  buzzer_quiet;
   uint8_t  gps_enabled;      // GPS enabled flag (0=disabled, 1=enabled)
   uint32_t gps_interval;     // GPS read interval in seconds
+  uint8_t autoadd_config;    // bitmask for auto-add contacts config
 };

+ 2 - 6
examples/companion_radio/main.cpp

@@ -151,9 +151,7 @@ void setup() {
   );
 
 #ifdef BLE_PIN_CODE
-  char dev_name[32+16];
-  sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
-  serial_interface.begin(dev_name, the_mesh.getBLEPin());
+  serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
 #else
   serial_interface.begin(Serial);
 #endif
@@ -199,9 +197,7 @@ void setup() {
   WiFi.begin(WIFI_SSID, WIFI_PWD);
   serial_interface.begin(TCP_PORT);
 #elif defined(BLE_PIN_CODE)
-  char dev_name[32+16];
-  sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
-  serial_interface.begin(dev_name, the_mesh.getBLEPin());
+  serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
 #elif defined(SERIAL_RX)
   companion_serial.setPins(SERIAL_RX, SERIAL_TX);
   companion_serial.begin(115200);

+ 116 - 16
examples/simple_repeater/MyMesh.cpp

@@ -41,16 +41,21 @@
   #define TXT_ACK_DELAY 200
 #endif
 
-#define FIRMWARE_VER_LEVEL       1
+#define FIRMWARE_VER_LEVEL       2
 
 #define REQ_TYPE_GET_STATUS         0x01 // same as _GET_STATS
 #define REQ_TYPE_KEEP_ALIVE         0x02
 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03
 #define REQ_TYPE_GET_ACCESS_LIST    0x05
 #define REQ_TYPE_GET_NEIGHBOURS     0x06
+#define REQ_TYPE_GET_OWNER_INFO     0x07     // FIRMWARE_VER_LEVEL >= 2
 
 #define RESP_SERVER_LOGIN_OK        0 // response to ANON_REQ
 
+#define ANON_REQ_TYPE_REGIONS      0x01
+#define ANON_REQ_TYPE_OWNER        0x02
+#define ANON_REQ_TYPE_BASIC        0x03   // just remote clock
+
 #define CLI_REPLY_DELAY_MILLIS      600
 
 #define LAZY_CONTACTS_WRITE_DELAY    5000
@@ -139,6 +144,64 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
   return 13;  // reply length
 }
 
+uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
+  if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
+    // request data has: {reply-path-len}{reply-path}
+    reply_path_len = *data++ & 0x3F;
+    memcpy(reply_path, data, reply_path_len);
+    // data += reply_path_len;
+
+    memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
+    uint32_t now = getRTCClock()->getCurrentTime();
+    memcpy(&reply_data[4], &now, 4);     // include our clock (for easy clock sync, and packet hash uniqueness)
+
+    return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD);   // reply length
+  }
+  return 0;
+}
+
+uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
+  if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
+    // request data has: {reply-path-len}{reply-path}
+    reply_path_len = *data++ & 0x3F;
+    memcpy(reply_path, data, reply_path_len);
+    // data += reply_path_len;
+
+    memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
+    uint32_t now = getRTCClock()->getCurrentTime();
+    memcpy(&reply_data[4], &now, 4);     // include our clock (for easy clock sync, and packet hash uniqueness)
+    sprintf((char *) &reply_data[8], "%s\n%s", _prefs.node_name, _prefs.owner_info);
+
+    return 8 + strlen((char *) &reply_data[8]);   // reply length
+  }
+  return 0;
+}
+
+uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
+  if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
+    // request data has: {reply-path-len}{reply-path}
+    reply_path_len = *data++ & 0x3F;
+    memcpy(reply_path, data, reply_path_len);
+    // data += reply_path_len;
+
+    memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
+    uint32_t now = getRTCClock()->getCurrentTime();
+    memcpy(&reply_data[4], &now, 4);     // include our clock (for easy clock sync, and packet hash uniqueness)
+    reply_data[8] = 0;  // features
+#ifdef WITH_RS232_BRIDGE
+    reply_data[8] |= 0x01;  // is bridge, type UART
+#elif WITH_ESPNOW_BRIDGE
+    reply_data[8] |= 0x03;  // is bridge, type ESP-NOW
+#endif
+    if (_prefs.disable_fwd) {   // is this repeater currently disabled
+      reply_data[8] |= 0x80;  // is disabled
+    }
+    // TODO:  add some kind of moving-window utilisation metric, so can query 'how busy' is this repeater
+    return 9;   // reply length
+  }
+  return 0;
+}
+
 int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
   // uint32_t now = getRTCClock()->getCurrentTimeUnique();
   // memcpy(reply_data, &now, 4);   // response packets always prefixed with timestamp
@@ -163,7 +226,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
     stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
     stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
     stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
-
+    stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
     memcpy(&reply_data[4], &stats, sizeof(stats));
 
     return 4 + sizeof(stats); //  reply_len
@@ -296,6 +359,9 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
 
       return reply_offset;
     }
+  } else if (payload[0] == REQ_TYPE_GET_OWNER_INFO) {
+    sprintf((char *) &reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info);
+    return 4 + strlen((char *) &reply_data[4]);
   }
   return 0; // unknown command
 }
@@ -448,12 +514,18 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
 
     data[len] = 0;  // ensure null terminator
     uint8_t reply_len;
+
+    reply_path_len = -1;
     if (data[4] == 0 || data[4] >= ' ') {   // is password, ie. a login request
       reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
-    //} else if (data[4] == ANON_REQ_TYPE_*) {   // future type codes
-      // TODO
+    } else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) {
+      reply_len = handleAnonRegionsReq(sender, timestamp, &data[5]);
+    } else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) {
+      reply_len = handleAnonOwnerReq(sender, timestamp, &data[5]);
+    } else if (data[4] == ANON_REQ_TYPE_BASIC && packet->isRouteDirect()) {
+      reply_len = handleAnonClockReq(sender, timestamp, &data[5]);
     } else {
-      reply_len = 0;  // unknown request type
+      reply_len = 0;  // unknown/invalid request type
     }
 
     if (reply_len == 0) return;   // invalid request
@@ -463,9 +535,12 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       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);
-    } else {
+    } 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);
+    } else {
+      mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
+      if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
     }
   }
 }
@@ -637,7 +712,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
 
 void MyMesh::onControlDataRecv(mesh::Packet* packet) {
   uint8_t type = packet->payload[0] & 0xF0;    // just test upper 4 bits
-  if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) {
+  if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
+      && !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime())
+  ) {
     int i = 1;
     uint8_t  filter = packet->payload[i++];
     uint32_t tag;
@@ -667,8 +744,9 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
 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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
-      discover_limiter(4, 120)  // max 4 every 2 minutes
+      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
+      discover_limiter(4, 120),  // max 4 every 2 minutes
+      anon_limiter(4, 180)   // max 4 every 3 minutes
 #if defined(WITH_RS232_BRIDGE)
       , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
 #endif
@@ -730,7 +808,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
   _fs = fs;
   // load persisted prefs
   _cli.loadPrefs(_fs);
-  acl.load(_fs);
+  acl.load(_fs, self_id);
   // TODO: key_store.begin();
   region_map.load(_fs);
 
@@ -776,10 +854,14 @@ bool MyMesh::formatFileSystem() {
 #endif
 }
 
-void MyMesh::sendSelfAdvertisement(int delay_millis) {
+void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
-    sendFlood(pkt, delay_millis);
+    if (flood) {
+      sendFlood(pkt, delay_millis);
+    } else {
+      sendZeroHop(pkt, delay_millis);
+    }
   } else {
     MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
   }
@@ -890,7 +972,6 @@ void MyMesh::formatPacketStatsReply(char *reply) {
 }
 
 void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
-  self_id = new_id;
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
   IdentityStore store(*_fs, "");
 #elif defined(ESP32)
@@ -900,7 +981,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
 #else
 #error "need to define saveIdentity()"
 #endif
-  store.save("_main", self_id);
+  store.save("_main", new_id);
 }
 
 void MyMesh::clearStats() {
@@ -991,8 +1072,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
 
     const char* parts[4];
     int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
-    if (n == 1 && sender_timestamp == 0) {
-      region_map.exportTo(Serial);
+    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));
@@ -1065,6 +1146,25 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
       } 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 - ??");
     }

+ 9 - 3
examples/simple_repeater/MyMesh.h

@@ -54,6 +54,7 @@ struct RepeaterStats {
   int16_t  last_snr;   // x 4
   uint16_t n_direct_dups, n_flood_dups;
   uint32_t total_rx_air_time_secs;
+  uint32_t n_recv_errors;
 };
 
 #ifndef MAX_CLIENTS
@@ -86,14 +87,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
   NodePrefs _prefs;
+  ClientACL  acl;
   CommonCLI _cli;
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
-  ClientACL  acl;
+  uint8_t reply_path[MAX_PATH_SIZE];
+  int8_t  reply_path_len;
   TransportKeyStore key_store;
   RegionMap region_map, temp_map;
   RegionEntry* load_stack[8];
   RegionEntry* recv_pkt_region;
-  RateLimiter discover_limiter;
+  RateLimiter discover_limiter, anon_limiter;
   bool region_load_active;
   unsigned long dirty_contacts_expiry;
 #if MAX_NEIGHBOURS
@@ -114,6 +117,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
 
   void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
   uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
+  uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
+  uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
+  uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
   int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
   mesh::Packet* createSelfAdvert();
 
@@ -181,7 +187,7 @@ public:
 
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
-  void sendSelfAdvertisement(int delay_millis) override;
+  void sendSelfAdvertisement(int delay_millis, bool flood) override;
   void updateAdvertTimer() override;
   void updateFloodAdvertTimer() override;
 

+ 4 - 2
examples/simple_repeater/main.cpp

@@ -87,8 +87,10 @@ void setup() {
   ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
 #endif
 
-  // send out initial Advertisement to the mesh
-  the_mesh.sendSelfAdvertisement(16000);
+  // send out initial zero hop Advertisement to the mesh
+#if ENABLE_ADVERT_ON_BOOT == 1
+  the_mesh.sendSelfAdvertisement(16000, false);
+#endif
 }
 
 void loop() {

+ 10 - 7
examples/simple_room_server/MyMesh.cpp

@@ -587,7 +587,7 @@ 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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
+      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
   last_millis = 0;
   uptime_millis = 0;
   next_local_advert = next_flood_advert = 0;
@@ -637,7 +637,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
   // load persisted prefs
   _cli.loadPrefs(_fs);
 
-  acl.load(_fs);
+  acl.load(_fs, self_id);
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);
@@ -675,10 +675,14 @@ bool MyMesh::formatFileSystem() {
 #endif
 }
 
-void MyMesh::sendSelfAdvertisement(int delay_millis) {
+void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
-    sendFlood(pkt, delay_millis);
+    if (flood) {
+      sendFlood(pkt, delay_millis);
+    } else {
+      sendZeroHop(pkt, delay_millis);
+    }
   } else {
     MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
   }
@@ -720,7 +724,6 @@ void MyMesh::setTxPower(uint8_t power_dbm) {
 }
 
 void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
-  self_id = new_id;
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
   IdentityStore store(*_fs, "");
 #elif defined(ESP32)
@@ -730,7 +733,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
 #else
 #error "need to define saveIdentity()"
 #endif
-  store.save("_main", self_id);
+  store.save("_main", new_id);
 }
 
 void MyMesh::clearStats() {
@@ -815,7 +818,7 @@ void MyMesh::loop() {
       if (c->extra.room.pending_ack && millisHasNowPassed(c->extra.room.ack_timeout)) {
         c->extra.room.push_failures++;
         c->extra.room.pending_ack = 0; // reset  (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
-        MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
+        MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->extra.room.push_failures);
       }
     }
     // check next Round-Robin client, and sync next new post

+ 2 - 2
examples/simple_room_server/MyMesh.h

@@ -94,8 +94,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
   NodePrefs _prefs;
-  CommonCLI _cli;
   ClientACL acl;
+  CommonCLI _cli;
   unsigned long dirty_contacts_expiry;
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
   unsigned long next_push;
@@ -177,7 +177,7 @@ public:
 
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
-  void sendSelfAdvertisement(int delay_millis) override;
+  void sendSelfAdvertisement(int delay_millis, bool flood) override;
   void updateAdvertTimer() override;
   void updateFloodAdvertTimer() override;
 

+ 4 - 2
examples/simple_room_server/main.cpp

@@ -76,8 +76,10 @@ void setup() {
   ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
 #endif
 
-  // send out initial Advertisement to the mesh
-  the_mesh.sendSelfAdvertisement(16000);
+  // send out initial zero hop Advertisement to the mesh
+#if ENABLE_ADVERT_ON_BOOT == 1
+  the_mesh.sendSelfAdvertisement(16000, false);
+#endif
 }
 
 void loop() {

+ 2 - 0
examples/simple_secure_chat/main.cpp

@@ -582,7 +582,9 @@ void setup() {
   the_mesh.showWelcome();
 
   // send out initial Advertisement to the mesh
+#if ENABLE_ADVERT_ON_BOOT == 1
   the_mesh.sendSelfAdvert(1200);   // add slight delay
+#endif
 }
 
 void loop() {

+ 9 - 6
examples/simple_sensor/SensorMesh.cpp

@@ -695,7 +695,7 @@ 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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
+      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
 {
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
@@ -736,7 +736,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
   // load persisted prefs
   _cli.loadPrefs(_fs);
 
-  acl.load(_fs);
+  acl.load(_fs, self_id);
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);
@@ -765,7 +765,6 @@ bool SensorMesh::formatFileSystem() {
 }
 
 void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
-  self_id = new_id;
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
   IdentityStore store(*_fs, "");
 #elif defined(ESP32)
@@ -775,7 +774,7 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
 #else
   #error "need to define saveIdentity()"
 #endif
-  store.save("_main", self_id);
+  store.save("_main", new_id);
 }
 
 void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
@@ -788,10 +787,14 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t
   revert_radio_at = futureMillis(2000 + timeout_mins*60*1000);   // schedule when to revert radio params
 }
 
-void SensorMesh::sendSelfAdvertisement(int delay_millis) {
+void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet* pkt = createSelfAdvert();
   if (pkt) {
-    sendFlood(pkt, delay_millis);
+    if (flood) {
+      sendFlood(pkt, delay_millis);
+    } else {
+      sendZeroHop(pkt, delay_millis);
+    }
   } else {
     MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
   }

+ 2 - 2
examples/simple_sensor/SensorMesh.h

@@ -60,7 +60,7 @@ public:
   NodePrefs* getNodePrefs() { return &_prefs; }
   void savePrefs() override { _cli.savePrefs(_fs); }
   bool formatFileSystem() override;
-  void sendSelfAdvertisement(int delay_millis) override;
+  void sendSelfAdvertisement(int delay_millis, bool flood) override;
   void updateAdvertTimer() override;
   void updateFloodAdvertTimer() override;
   void setLoggingOn(bool enable) override {  }
@@ -133,9 +133,9 @@ private:
   FILESYSTEM* _fs;
   unsigned long next_local_advert, next_flood_advert;
   NodePrefs _prefs;
+  ClientACL  acl;
   CommonCLI _cli;
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
-  ClientACL  acl;
   unsigned long dirty_contacts_expiry;
   CayenneLPP telemetry;
   uint32_t last_read_time;

+ 4 - 2
examples/simple_sensor/main.cpp

@@ -110,8 +110,10 @@ void setup() {
   ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
 #endif
 
-  // send out initial Advertisement to the mesh
-  the_mesh.sendSelfAdvertisement(16000);
+  // send out initial zero hop Advertisement to the mesh
+#if ENABLE_ADVERT_ON_BOOT == 1
+  the_mesh.sendSelfAdvertisement(16000, false);
+#endif
 }
 
 void loop() {

+ 1 - 0
platformio.ini

@@ -27,6 +27,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
   -D LORA_FREQ=869.525
   -D LORA_BW=250
   -D LORA_SF=11
+  -D ENABLE_ADVERT_ON_BOOT=1
   -D ENABLE_PRIVATE_KEY_IMPORT=1   ; NOTE: comment these out for more secure firmware
   -D ENABLE_PRIVATE_KEY_EXPORT=1
   -D RADIOLIB_EXCLUDE_CC1101=1

+ 44 - 0
src/Identity.cpp

@@ -48,6 +48,50 @@ LocalIdentity::LocalIdentity(RNG* rng) {
   ed25519_create_keypair(pub_key, prv_key, seed);
 }
 
+bool LocalIdentity::validatePrivateKey(const uint8_t prv[64]) {
+    uint8_t pub[32];
+    ed25519_derive_pub(pub, prv); // derive public key from given private key
+
+    // disallow 00 or FF prefixed public keys
+    if (pub[0] == 0x00 || pub[0] == 0xFF) return false;
+
+    // known good test client keypair
+    const uint8_t test_client_prv[64] = {
+      0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70,
+      0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde,
+      0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e,
+      0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60,
+      0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39,
+      0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61,
+      0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5,
+      0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71
+    };
+    const uint8_t test_client_pub[32] = {
+      0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2,
+      0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d,
+      0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27,
+      0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10
+    };
+
+    uint8_t ss1[32], ss2[32];
+
+    // shared secret we calculte from test client pubkey and given private key
+    ed25519_key_exchange(ss1, test_client_pub, prv);
+
+    // shared secret they calculate from our derived public key and test client private key
+    ed25519_key_exchange(ss2, pub, test_client_prv);
+
+    // check that both shared secrets match
+    if (memcmp(ss1, ss2, 32) != 0) return false;
+
+    // reject all-zero shared secret
+    for (int i = 0; i < 32; i++) {
+        if (ss1[i] != 0) return true;
+    }
+
+    return false;
+}
+
 bool LocalIdentity::readFrom(Stream& s) {
   bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
   success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE);

+ 7 - 0
src/Identity.h

@@ -76,6 +76,13 @@ public:
   */
   void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
 
+  /**
+   * \brief  Validates that a given private key can be used for ECDH / shared-secret operations.
+   * \param  prv IN - the private key to validate (must be PRV_KEY_SIZE bytes)
+   * \returns true, if the private key is valid for login.
+  */
+  static bool validatePrivateKey(const uint8_t prv[64]);
+
   bool readFrom(Stream& s);
   bool writeTo(Stream& s) const;
   void printTo(Stream& s) const;

+ 8 - 0
src/MeshCore.h

@@ -56,6 +56,14 @@ public:
   virtual void setGpio(uint32_t values) {}
   virtual uint8_t getStartupReason() const = 0;
   virtual bool startOTAUpdate(const char* id, char reply[]) { return false; }   // not supported
+
+  // Power management interface (boards with power management override these)
+  virtual bool isExternalPowered() { return false; }
+  virtual uint16_t getBootVoltage() { return 0; }
+  virtual uint32_t getResetReason() const { return 0; }
+  virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
+  virtual uint8_t getShutdownReason() const { return 0; }
+  virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
 };
 
 /**

+ 97 - 37
src/helpers/BaseChatMesh.cpp

@@ -55,6 +55,54 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
   }
 }
 
+void BaseChatMesh::bootstrapRTCfromContacts() {
+  uint32_t latest = 0;
+  for (int i = 0; i < num_contacts; i++) {
+    if (contacts[i].lastmod > latest) {
+      latest = contacts[i].lastmod;
+    }
+  }
+  if (latest != 0) {
+    getRTCClock()->setCurrentTime(latest + 1);
+  }
+}
+
+ContactInfo* BaseChatMesh::allocateContactSlot() {
+  if (num_contacts < MAX_CONTACTS) {
+    return &contacts[num_contacts++];
+  } else if (shouldOverwriteWhenFull()) {
+    // Find oldest non-favourite contact by oldest lastmod timestamp
+    int oldest_idx = -1;
+    uint32_t oldest_lastmod = 0xFFFFFFFF;
+    for (int i = 0; i < num_contacts; i++) {
+      bool is_favourite = (contacts[i].flags & 0x01) != 0;
+      if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
+        oldest_lastmod = contacts[i].lastmod;
+        oldest_idx = i;
+      }
+    }
+    if (oldest_idx >= 0) {
+      onContactOverwrite(contacts[oldest_idx].id.pub_key);
+      return &contacts[oldest_idx];
+    }
+  }
+  return NULL; // no space, no overwrite or all contacts are all favourites
+}
+
+void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
+  memset(&ci, 0, sizeof(ci));
+  ci.id = id;
+  ci.out_path_len = -1;  // initially out_path is unknown
+  StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
+  ci.type = parser.getType();
+  if (parser.hasLatLon()) {
+    ci.gps_lat = parser.getIntLat();
+    ci.gps_lon = parser.getIntLon();
+  }
+  ci.last_advert_timestamp = timestamp;
+  ci.lastmod = getRTCClock()->getCurrentTime();
+}
+
 void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
   AdvertDataParser parser(app_data, app_data_len);
   if (!(parser.isValid() && parser.hasName())) {
@@ -85,50 +133,38 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
   }
   putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
 
-  bool is_new = false;
+  bool is_new = false; // true = not in contacts[], false = exists in contacts[]
   if (from == NULL) {
-    if (!isAutoAddEnabled()) {
+    if (!shouldAutoAddContactType(parser.getType())) {
       ContactInfo ci;
-      memset(&ci, 0, sizeof(ci));
-      ci.id = id;
-      ci.out_path_len = -1;  // initially out_path is unknown
-      StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
-      ci.type = parser.getType();
-      if (parser.hasLatLon()) {
-        ci.gps_lat = parser.getIntLat();
-        ci.gps_lon = parser.getIntLon();
-      }
-      ci.last_advert_timestamp = timestamp;
-      ci.lastmod = getRTCClock()->getCurrentTime();
+      populateContactFromAdvert(ci, id, parser, timestamp);
       onDiscoveredContact(ci, true, packet->path_len, packet->path);       // let UI know
       return;
     }
 
-    is_new = true;
-    if (num_contacts < MAX_CONTACTS) {
-      from = &contacts[num_contacts++];
-      from->id = id;
-      from->out_path_len = -1;  // initially out_path is unknown
-      from->gps_lat = 0;   // initially unknown GPS loc
-      from->gps_lon = 0;
-      from->sync_since = 0;
-
-      from->shared_secret_valid = false;  // ecdh shared_secret will be calculated later on demand  
-    } else {
-      MESH_DEBUG_PRINTLN("onAdvertRecv: contacts table is full!");
+    from = allocateContactSlot();
+    if (from == NULL) {
+      ContactInfo ci;
+      populateContactFromAdvert(ci, id, parser, timestamp);
+      onDiscoveredContact(ci, true, packet->path_len, packet->path);
+      onContactsFull();
+      MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
       return;
     }
+    
+    populateContactFromAdvert(*from, id, parser, timestamp);
+    from->sync_since = 0;
+    from->shared_secret_valid = false;
   }
-
   // update
-  StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
-  from->type = parser.getType();
-  if (parser.hasLatLon()) {
-    from->gps_lat = parser.getIntLat();
-    from->gps_lon = parser.getIntLon();
-  }
-  from->last_advert_timestamp = timestamp;
-  from->lastmod = getRTCClock()->getCurrentTime();
+    StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
+    from->type = parser.getType();
+    if (parser.hasLatLon()) {
+      from->gps_lat = parser.getIntLat();
+      from->gps_lon = parser.getIntLon();
+    }
+    from->last_advert_timestamp = timestamp;
+    from->lastmod = getRTCClock()->getCurrentTime();
 
   onDiscoveredContact(*from, is_new, packet->path_len, packet->path);       // let UI know
 }
@@ -477,6 +513,31 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
   return MSG_SEND_FAILED;
 }
 
+int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout) {
+  mesh::Packet* pkt;
+  {
+    uint8_t temp[MAX_PACKET_PAYLOAD];
+    tag = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &tag, 4);   // tag to match later (also extra blob to help make packet_hash unique)
+    memcpy(&temp[4], data, len);
+
+    pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
+  }
+  if (pkt) {
+    uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
+    if (recipient.out_path_len < 0) {
+      sendFloodScoped(recipient, pkt);
+      est_timeout = calcFloodTimeoutMillisFor(t);
+      return MSG_SEND_SENT_FLOOD;
+    } else {
+      sendDirect(pkt, recipient.out_path, recipient.out_path_len);
+      est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
+      return MSG_SEND_SENT_DIRECT;
+    }
+  }
+  return MSG_SEND_FAILED;
+}
+
 int  BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
   if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
 
@@ -697,10 +758,9 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre
 }
 
 bool BaseChatMesh::addContact(const ContactInfo& contact) {
-  if (num_contacts < MAX_CONTACTS) {
-    auto dest = &contacts[num_contacts++];
+  ContactInfo* dest = allocateContactSlot();
+  if (dest) {
     *dest = contact;
-
     dest->shared_secret_valid = false; // mark shared_secret as needing calculation
     return true;  // success
   }

+ 8 - 0
src/helpers/BaseChatMesh.h

@@ -88,10 +88,17 @@ protected:
     memset(connections, 0, sizeof(connections));
   }
 
+  void bootstrapRTCfromContacts();
   void resetContacts() { num_contacts = 0; }
+  void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
+  ContactInfo* allocateContactSlot(); // helper to find slot for new contact
 
   // 'UI' concepts, for sub-classes to implement
   virtual bool isAutoAddEnabled() const { return true; }
+  virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
+  virtual void onContactsFull() {};
+  virtual bool shouldOverwriteWhenFull() const { return false; }
+  virtual void onContactOverwrite(const uint8_t* pub_key) {};
   virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
   virtual ContactInfo* processAck(const uint8_t *data) = 0;
   virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
@@ -141,6 +148,7 @@ public:
   int  sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
   bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
   int  sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
+  int  sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout);
   int  sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
   int  sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
   bool shareContactZeroHop(const ContactInfo& contact);

+ 16 - 3
src/helpers/ClientACL.cpp

@@ -11,7 +11,8 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
   #endif
 }
 
-void ClientACL::load(FILESYSTEM* _fs) {
+void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
+  _fs = fs;
   num_clients = 0;
   if (_fs->exists("/s_contacts")) {
   #if defined(RP2040_PLATFORM)
@@ -34,11 +35,12 @@ void ClientACL::load(FILESYSTEM* _fs) {
         success = success && (file.read(unused, 2) == 2);
         success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
         success = success && (file.read(c.out_path, 64) == 64);
-        success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
+        success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
 
         if (!success) break; // EOF
 
         c.id = mesh::Identity(pub_key);
+        self_id.calcSharedSecret(c.shared_secret, pub_key);  // recalculate shared secrets in case our private key changed
         if (num_clients < MAX_CLIENTS) {
           clients[num_clients++] = c;
         } else {
@@ -50,7 +52,8 @@ void ClientACL::load(FILESYSTEM* _fs) {
   }
 }
 
-void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
+void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
+  _fs = fs;
   File file = openWrite(_fs, "/s_contacts");
   if (file) {
     uint8_t unused[2];
@@ -74,6 +77,16 @@ void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
   }
 }
 
+bool ClientACL::clear() {
+  if (!_fs) return false; // no filesystem, nothing to clear
+  if (_fs->exists("/s_contacts")) {
+    _fs->remove("/s_contacts");
+  }
+  memset(clients, 0, sizeof(clients));
+  num_clients = 0;
+  return true;
+}
+
 ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
   for (int i = 0; i < num_clients; i++) {
     if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i];  // already known

+ 3 - 1
src/helpers/ClientACL.h

@@ -36,6 +36,7 @@ struct ClientInfo {
 #endif
 
 class ClientACL {
+  FILESYSTEM* _fs;
   ClientInfo clients[MAX_CLIENTS];
   int num_clients;
 
@@ -44,8 +45,9 @@ public:
     memset(clients, 0, sizeof(clients));
     num_clients = 0;
   }
-  void load(FILESYSTEM* _fs);
+  void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id);
   void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
+  bool clear();
 
   ClientInfo* getClient(const uint8_t* pubkey, int key_len);
   ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);

+ 79 - 13
src/helpers/CommonCLI.cpp

@@ -14,6 +14,14 @@ static uint32_t _atoi(const char* sp) {
   return n;
 }
 
+static bool isValidName(const char *n) {
+  while (*n) {
+    if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
+    n++;
+  }
+  return true;
+}
+
 void CommonCLI::loadPrefs(FILESYSTEM* fs) {
   if (fs->exists("/com_prefs")) {
     loadPrefsInt(fs, "/com_prefs");   // new filename
@@ -72,7 +80,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy));          // 161
     file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
     file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
-    // 170
+    file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info));  // 170
+    // 290
 
     // sanitise bad pref values
     _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -155,7 +164,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy));           // 161
     file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
     file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier));                 // 166
-    // 170
+    file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info));  // 170
+    // 290
 
     file.close();
   }
@@ -186,8 +196,13 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
 void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
     if (memcmp(command, "reboot", 6) == 0) {
       _board->reboot();  // doesn't return
+    } else if (memcmp(command, "clkreboot", 9) == 0) {
+      // Reset clock
+      getRTCClock()->setCurrentTime(1715770351);  // 15 May 2024, 8:50pm
+      _board->reboot();  // doesn't return
     } else if (memcmp(command, "advert", 6) == 0) {
-      _callbacks->sendSelfAdvertisement(1500);  // longer delay, give CLI response time to be sent first
+      // send flood advert
+      _callbacks->sendSelfAdvertisement(1500, true);  // longer delay, give CLI response time to be sent first
       strcpy(reply, "OK - Advert sent");
     } else if (memcmp(command, "clock sync", 10) == 0) {
       uint32_t curr = getRTCClock()->getCurrentTime();
@@ -301,6 +316,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         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, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
         sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm);
       } else if (memcmp(config, "freq", 4) == 0) {
@@ -345,6 +369,33 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         } 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);
       }
@@ -375,8 +426,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         strcpy(reply, "OK");
       } else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
         int hours = _atoi(&config[22]);
-        if ((hours > 0 && hours < 3) || (hours > 48)) {
-          strcpy(reply, "Error: interval range is 3-48 hours");
+        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();
@@ -397,22 +448,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
         savePrefs();
         strcpy(reply, "OK");
-      } else if (sender_timestamp == 0 &&
-                 memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
+      } 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]);
-        if (success) {
+        // 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");
+          strcpy(reply, "OK, reboot to apply! New pubkey: ");
+          mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
         } else {
-          strcpy(reply, "Error, invalid key");
+          strcpy(reply, "Error, bad key");
         }
       } else if (memcmp(config, "name ", 5) == 0) {
-        StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
-        savePrefs();
-        strcpy(reply, "OK");
+        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();
@@ -479,6 +535,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         } 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, "tx ", 3) == 0) {
         _prefs->tx_power_dbm = atoi(&config[3]);
         savePrefs();

+ 6 - 3
src/helpers/CommonCLI.h

@@ -3,6 +3,7 @@
 #include "Mesh.h"
 #include <helpers/IdentityStore.h>
 #include <helpers/SensorManager.h>
+#include <helpers/ClientACL.h>
 
 #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
 #define WITH_BRIDGE
@@ -50,6 +51,7 @@ struct NodePrefs { // persisted to file
   uint8_t advert_loc_policy;
   uint32_t discovery_mod_timestamp;
   float adc_multiplier;
+  char owner_info[120];
 };
 
 class CommonCLICallbacks {
@@ -59,7 +61,7 @@ public:
   virtual const char* getBuildDate() = 0;
   virtual const char* getRole() = 0;
   virtual bool formatFileSystem() = 0;
-  virtual void sendSelfAdvertisement(int delay_millis) = 0;
+  virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0;
   virtual void updateAdvertTimer() = 0;
   virtual void updateFloodAdvertTimer() = 0;
   virtual void setLoggingOn(bool enable) = 0;
@@ -93,6 +95,7 @@ class CommonCLI {
   CommonCLICallbacks* _callbacks;
   mesh::MainBoard* _board;
   SensorManager* _sensors;
+  ClientACL* _acl;
   char tmp[PRV_KEY_SIZE*2 + 4];
 
   mesh::RTCClock* getRTCClock() { return _rtc; }
@@ -100,8 +103,8 @@ class CommonCLI {
   void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
 
 public:
-  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, NodePrefs* prefs, CommonCLICallbacks* callbacks)
-      : _board(&board), _rtc(&rtc), _sensors(&sensors), _prefs(prefs), _callbacks(callbacks) { }
+  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) { }
 
   void loadPrefs(FILESYSTEM* _fs);
   void savePrefs(FILESYSTEM* _fs);

+ 217 - 0
src/helpers/NRF52Board.cpp

@@ -2,6 +2,7 @@
 #include "NRF52Board.h"
 
 #include <bluefruit.h>
+#include <nrf_soc.h>
 
 static BLEDfu bledfu;
 
@@ -21,6 +22,222 @@ void NRF52Board::begin() {
   startup_reason = BD_STARTUP_NORMAL;
 }
 
+#ifdef NRF52_POWER_MANAGEMENT
+#include "nrf.h"
+
+// Power Management global variables
+uint32_t g_nrf52_reset_reason = 0;     // Reset/Startup reason
+uint8_t g_nrf52_shutdown_reason = 0;   // Shutdown reason
+
+// Early constructor - runs before SystemInit() clears the registers
+// Priority 101 ensures this runs before SystemInit (102) and before
+// any C++ static constructors (default 65535)
+static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
+  g_nrf52_reset_reason = NRF_POWER->RESETREAS;
+  g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
+}
+
+void NRF52Board::initPowerMgr() {
+  // Copy early-captured register values
+  reset_reason = g_nrf52_reset_reason;
+  shutdown_reason = g_nrf52_shutdown_reason;
+  boot_voltage_mv = 0;  // Will be set by checkBootVoltage()
+
+  // Clear registers for next boot
+  // Note: At this point SoftDevice may or may not be enabled
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_reset_reason_clr(0xFFFFFFFF);
+    sd_power_gpregret_clr(1, 0xFF);
+  } else {
+    NRF_POWER->RESETREAS = 0xFFFFFFFF;  // Write 1s to clear
+    NRF_POWER->GPREGRET2 = 0;
+  }
+
+  // Log reset/shutdown info
+  if (shutdown_reason != SHUTDOWN_REASON_NONE) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
+      getResetReasonString(reset_reason), (unsigned long)reset_reason,
+      getShutdownReasonString(shutdown_reason), shutdown_reason);
+  } else {
+    MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
+      getResetReasonString(reset_reason), (unsigned long)reset_reason);
+  }
+}
+
+bool NRF52Board::isExternalPowered() {
+  // Check if SoftDevice is enabled before using its API
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+
+  if (sd_enabled) {
+    uint32_t usb_status;
+    sd_power_usbregstatus_get(&usb_status);
+    return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
+  } else {
+    return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
+  }
+}
+
+const char* NRF52Board::getResetReasonString(uint32_t reason) {
+  if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
+  if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
+  if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
+  if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
+  #ifdef POWER_RESETREAS_LPCOMP_Msk
+    if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
+  #endif
+  #ifdef POWER_RESETREAS_VBUS_Msk
+    if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
+  #endif
+  #ifdef POWER_RESETREAS_OFF_Msk
+    if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
+  #endif
+  #ifdef POWER_RESETREAS_DIF_Msk
+    if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
+  #endif
+  return "Cold Boot";
+}
+
+const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
+  switch (reason) {
+    case SHUTDOWN_REASON_LOW_VOLTAGE:  return "Low Voltage";
+    case SHUTDOWN_REASON_USER:         return "User Request";
+    case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
+  }
+  return "Unknown";
+}
+
+bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
+  initPowerMgr();
+
+  // Read boot voltage
+  boot_voltage_mv = getBattMilliVolts();
+  
+  if (config->voltage_bootlock == 0) return true;  // Protection disabled
+
+  // Skip check if externally powered
+  if (isExternalPowered()) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
+    boot_voltage_mv = getBattMilliVolts();
+    return true;
+  }
+
+  MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
+      boot_voltage_mv, config->voltage_bootlock);
+
+  // Only trigger shutdown if reading is valid (>1000mV) AND below threshold
+  // This prevents spurious shutdowns on ADC glitches or uninitialized reads
+  if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
+
+    initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
+    return false;  // Should never reach this
+  }
+
+  return true;
+}
+
+void NRF52Board::initiateShutdown(uint8_t reason) {
+  enterSystemOff(reason);
+}
+
+void NRF52Board::enterSystemOff(uint8_t reason) {
+  MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
+
+  // Record shutdown reason in GPREGRET2
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_gpregret_clr(1, 0xFF);
+    sd_power_gpregret_set(1, reason);
+  } else {
+    NRF_POWER->GPREGRET2 = reason;
+  }
+
+  // Flush serial buffers
+  Serial.flush();
+  delay(100);
+
+  // Enter SYSTEMOFF
+  if (sd_enabled) {
+    uint32_t err = sd_power_system_off();
+    if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) {  //SoftDevice not enabled
+      sd_enabled = 0;
+    }
+  }
+
+  if (!sd_enabled) {
+    // SoftDevice not available; write directly to POWER->SYSTEMOFF
+    NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
+  }
+
+  // If we get here, something went wrong. Reset to recover.
+  NVIC_SystemReset();
+}
+
+void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
+  // LPCOMP is not managed by SoftDevice - direct register access required
+  // Halt and disable before reconfiguration
+  NRF_LPCOMP->TASKS_STOP = 1;
+  NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
+
+  // Select analog input (AIN0-7 maps to PSEL 0-7)
+  NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
+
+  // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
+  NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
+
+  // Detect UP events (voltage rises above threshold for battery recovery)
+  NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
+
+  // Enable 50mV hysteresis for noise immunity
+  NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
+
+  // Clear stale events/interrupts before enabling wake
+  NRF_LPCOMP->EVENTS_READY = 0;
+  NRF_LPCOMP->EVENTS_DOWN = 0;
+  NRF_LPCOMP->EVENTS_UP = 0;
+  NRF_LPCOMP->EVENTS_CROSS = 0;
+
+  NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
+  NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
+
+  // Enable LPCOMP
+  NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
+  NRF_LPCOMP->TASKS_START = 1;
+
+  // Wait for comparator to settle before entering SYSTEMOFF
+  for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
+    delayMicroseconds(50);
+  }
+
+  if (refsel == 7) {
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
+  } else if (refsel <= 6) {
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
+      ain_channel, refsel + 1);
+  } else {
+    uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
+      ain_channel, ref_num);
+  }
+
+  // Configure VBUS (USB power) wake alongside LPCOMP
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_usbdetected_enable(1);
+  } else {
+    NRF_POWER->EVENTS_USBDETECTED = 0;
+    NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
+  }
+
+  MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
+}
+#endif
+
 void NRF52BoardDCDC::begin() {
   NRF52Board::begin();
 

+ 43 - 0
src/helpers/NRF52Board.h

@@ -5,11 +5,45 @@
 
 #if defined(NRF52_PLATFORM)
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
+#define SHUTDOWN_REASON_NONE          0x00
+#define SHUTDOWN_REASON_LOW_VOLTAGE   0x4C  // 'L' - Runtime low voltage threshold
+#define SHUTDOWN_REASON_USER          0x55  // 'U' - User requested powerOff()
+#define SHUTDOWN_REASON_BOOT_PROTECT  0x42  // 'B' - Boot voltage protection
+
+// Boards provide this struct with their hardware-specific settings and callbacks.
+struct PowerMgtConfig {
+  // LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
+  uint8_t lpcomp_ain_channel;       // AIN0-7 for voltage sensing pin
+  uint8_t lpcomp_refsel;            // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
+
+  // Boot protection voltage threshold (millivolts)
+  // Set to 0 to disable boot protection
+  uint16_t voltage_bootlock;
+};
+#endif
+
 class NRF52Board : public mesh::MainBoard {
+#ifdef NRF52_POWER_MANAGEMENT
+  void initPowerMgr();
+#endif
+
 protected:
   uint8_t startup_reason;
   char *ota_name;
 
+#ifdef NRF52_POWER_MANAGEMENT
+  uint32_t reset_reason;              // RESETREAS register value
+  uint8_t shutdown_reason;            // GPREGRET value (why we entered last SYSTEMOFF)
+  uint16_t boot_voltage_mv;           // Battery voltage at boot (millivolts)
+
+  bool checkBootVoltage(const PowerMgtConfig* config);
+  void enterSystemOff(uint8_t reason);
+  void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
+  virtual void initiateShutdown(uint8_t reason);
+#endif
+
 public:
   NRF52Board(char *otaname) : ota_name(otaname) {}
   virtual void begin();
@@ -17,6 +51,15 @@ public:
   virtual float getMCUTemperature() override;
   virtual void reboot() override { NVIC_SystemReset(); }
   virtual bool startOTAUpdate(const char *id, char reply[]) override;
+
+#ifdef NRF52_POWER_MANAGEMENT
+  bool isExternalPowered() override;
+  uint16_t getBootVoltage() override { return boot_voltage_mv; }
+  virtual uint32_t getResetReason() const override { return reset_reason; }
+  uint8_t getShutdownReason() const override { return shutdown_reason; }
+  const char* getResetReasonString(uint32_t reason) override;
+  const char* getShutdownReasonString(uint8_t reason) override;
+#endif
 };
 
 /*

+ 108 - 16
src/helpers/RegionMap.cpp

@@ -2,6 +2,45 @@
 #include <helpers/TxtDataHelpers.h>
 #include <SHA256.h>
 
+// helper class for region map exporter, we emulate Stream with a safe buffer writer.
+
+class BufStream : public Stream {
+public:
+  BufStream(char *buf, size_t max_len)
+    : _buf(buf), _max_len(max_len), _pos(0) {
+    if (_max_len > 0) _buf[0] = 0;
+  }
+
+  size_t write(uint8_t c) override {
+    if (_pos + 1 >= _max_len) return 0;
+    _buf[_pos++] = c;
+    _buf[_pos] = 0;
+    return 1;
+  }
+
+  size_t write(const uint8_t *buffer, size_t size) override {
+    size_t written = 0;
+    while (written < size) {
+      if (!write(buffer[written])) break;
+      written++;
+    }
+    return written;
+  }
+
+  int available() override { return 0; }
+  int read() override { return -1; }
+  int peek() override { return -1; }
+  void flush() override {}
+
+  size_t length() const { return _pos; }
+
+private:
+  char *_buf;
+  size_t _max_len;
+  size_t _pos;
+};
+
+
 RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
   next_id = 1; num_regions = 0; home_id = 0;
   wildcard.id = wildcard.parent = 0;
@@ -9,8 +48,13 @@ RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
   strcpy(wildcard.name, "*");
 }
 
-bool RegionMap::is_name_char(char c) {
-  return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#';
+bool RegionMap::is_name_char(uint8_t c) {
+  // accept all alpha-num or accented characters, but exclude most punctuation chars
+  return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
+}
+
+static const char* skip_hash(const char* name) {
+  return *name == '#' ? name + 1 : name;
 }
 
 static File openWrite(FILESYSTEM* _fs, const char* filename) {
@@ -24,12 +68,12 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
   #endif
 }
 
-bool RegionMap::load(FILESYSTEM* _fs) {
-  if (_fs->exists("/regions2")) {
+bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
+  if (_fs->exists(path ? path : "/regions2")) {
   #if defined(RP2040_PLATFORM)
-    File file = _fs->open("/regions2", "r");
+    File file = _fs->open(path ? path : "/regions2", "r");
   #else
-    File file = _fs->open("/regions2");
+    File file = _fs->open(path ? path : "/regions2");
   #endif
 
     if (file) {
@@ -67,8 +111,8 @@ bool RegionMap::load(FILESYSTEM* _fs) {
   return false;  // failed
 }
 
-bool RegionMap::save(FILESYSTEM* _fs) {
-  File file = openWrite(_fs, "/regions2");
+bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
+  File file = openWrite(_fs, path ? path : "/regions2");
   if (file) {
     uint8_t pad[128];
     memset(pad, 0, sizeof(pad));
@@ -126,11 +170,17 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
     if ((region->flags & mask) == 0) {   // does region allow this? (per 'mask' param)
       TransportKey keys[4];
       int num;
-      if (region->name[0] == '#') {   // auto hashtag region
+      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 {
-        num = _store->loadKeysFor(region->id, keys, 4);
+      } 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;
       }
       for (int j = 0; j < num; j++) {
         uint16_t code = keys[j].calcTransportCode(packet);
@@ -146,9 +196,10 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
 RegionEntry* RegionMap::findByName(const char* name) {
   if (strcmp(name, "*") == 0) return &wildcard;
 
+  if (*name == '#') { name++; }  // ignore the '#' when matching by name
   for (int i = 0; i < num_regions; i++) {
     auto region = &regions[i];
-    if (strcmp(name, region->name) == 0) return region;
+    if (strcmp(name, skip_hash(region->name)) == 0) return region;
   }
   return NULL;  // not found
 }
@@ -156,11 +207,12 @@ RegionEntry* RegionMap::findByName(const char* name) {
 RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
   if (strcmp(prefix, "*") == 0) return &wildcard;
 
+  if (*prefix == '#') { prefix++; }  // ignore the '#' when matching by name
   RegionEntry* partial = NULL;
   for (int i = 0; i < num_regions; i++) {
     auto region = &regions[i];
-    if (strcmp(prefix, region->name) == 0) return region;  // is a complete match, preference this one
-    if (memcmp(prefix, region->name, strlen(prefix)) == 0) {
+    if (strcmp(prefix, skip_hash(region->name)) == 0) return region;  // is a complete match, preference this one
+    if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
       partial = region;
     }
   }
@@ -219,9 +271,9 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
   }
 
   if (parent->flags & REGION_DENY_FLOOD) {
-    out.printf("%s%s\n", parent->name, parent->id == home_id ? "^" : "");
+    out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
   } else {
-    out.printf("%s%s F\n", parent->name, parent->id == home_id ? "^" : "");
+    out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
   }
 
   for (int i = 0; i < num_regions; i++) {
@@ -235,3 +287,43 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
 void RegionMap::exportTo(Stream& out) const {
   printChildRegions(0, &wildcard, out);   // recursive
 }
+
+size_t RegionMap::exportTo(char *dest, size_t max_len) const {
+  if (!dest || max_len == 0) return 0;
+
+  BufStream bs(dest, max_len);
+  exportTo(bs);              // ← reuse existing logic
+  return bs.length();
+}
+
+int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) {
+  char *dp = dest;
+  
+  // Check wildcard region
+  bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
+  if (wildcard_matches) {
+    *dp++ = '*';
+    *dp++ = ',';
+  }
+
+    for (int i = 0; i < num_regions; i++) {
+    auto region = &regions[i];
+    
+    // Check if region matches the filter criteria
+    bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
+    
+    if (region_matches) {
+      int len = strlen(skip_hash(region->name));
+      if ((dp - dest) + len + 2 < max_len) {   // only append if name will fit
+        memcpy(dp, skip_hash(region->name), len);
+        dp += len;
+        *dp++ = ',';
+      }
+    }
+  }
+
+  if (dp > dest) { dp--; }   // don't include trailing comma
+
+  *dp = 0;  // set null terminator
+  return dp - dest;   // return length
+}

+ 9 - 4
src/helpers/RegionMap.h

@@ -30,10 +30,10 @@ class RegionMap {
 public:
   RegionMap(TransportKeyStore& store);
 
-  static bool is_name_char(char c);
+  static bool is_name_char(uint8_t c);
 
-  bool load(FILESYSTEM* _fs);
-  bool save(FILESYSTEM* _fs);
+  bool load(FILESYSTEM* _fs, const char* path=NULL);
+  bool save(FILESYSTEM* _fs, const char* path=NULL);
 
   RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
   RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
@@ -47,6 +47,11 @@ public:
   bool clear();
   void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
   int getCount() const { return num_regions; }
+  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);
 
-  void exportTo(Stream& out) const;
+  void    exportTo(Stream& out) const;
+  size_t  exportTo(char *dest, size_t max_len) const;
+ 
 };

+ 3 - 2
src/helpers/StatsFormatHelper.h

@@ -42,13 +42,14 @@ public:
                                uint32_t n_recv_flood,
                                uint32_t n_recv_direct) {
     sprintf(reply, 
-      "{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u}",
+      "{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,\"recv_errors\":%u}",
       driver.getPacketsRecv(),
       driver.getPacketsSent(),
       n_sent_flood,
       n_sent_direct,
       n_recv_flood,
-      n_recv_direct
+      n_recv_direct,
+      driver.getPacketsRecvErrors()
     );
   }
 };

+ 12 - 2
src/helpers/esp32/SerialBLEInterface.cpp

@@ -9,11 +9,21 @@
 
 #define ADVERT_RESTART_DELAY  1000   // millis
 
-void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
+void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
   _pin_code = pin_code;
 
+  if (strcmp(name, "@@MAC") == 0) {
+    uint8_t addr[8];
+    memset(addr, 0, sizeof(addr));
+    esp_efuse_mac_get_default(addr);
+    sprintf(name, "%02X%02X%02X%02X%02X%02X",    // modify (IN-OUT param)
+          addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
+  }
+  char dev_name[32+16];
+  sprintf(dev_name, "%s%s", prefix, name);
+
   // Create the BLE Device
-  BLEDevice::init(device_name);
+  BLEDevice::init(dev_name);
   BLEDevice::setSecurityCallbacks(this);
   BLEDevice::setMTU(MAX_FRAME_SIZE);
 

+ 7 - 1
src/helpers/esp32/SerialBLEInterface.h

@@ -61,7 +61,13 @@ public:
     send_queue_len = recv_queue_len = 0;
   }
 
-  void begin(const char* device_name, uint32_t pin_code);
+  /**
+   * init the BLE interface.
+   * @param prefix   a prefix for the device name
+   * @param name  IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
+   * @param pin_code   the BLE security pin
+   */
+  void begin(const char* prefix, char* name, uint32_t pin_code);
 
   // BaseSerialInterface methods
   void enable() override;

+ 13 - 3
src/helpers/nrf52/SerialBLEInterface.cpp

@@ -123,7 +123,7 @@ void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) {
   }
 }
 
-void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
+void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
   instance = this;
 
   char charpin[20];
@@ -133,7 +133,17 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
   // Bluefruit.autoConnLed(false);
   Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
   Bluefruit.begin();
-  
+ 
+  char dev_name[32+16];
+  if (strcmp(name, "@@MAC") == 0) {
+    ble_gap_addr_t addr;
+    if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
+      sprintf(name, "%02X%02X%02X%02X%02X%02X",    // modify (IN-OUT param)
+          addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2], addr.addr[1], addr.addr[0]);
+    }
+  }
+  sprintf(dev_name, "%s%s", prefix, name);
+
   // Connection interval units: 1.25ms, supervision timeout units: 10ms
   ble_gap_conn_params_t ppcp_params;
   ppcp_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
@@ -153,7 +163,7 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
   }
   
   Bluefruit.setTxPower(BLE_TX_POWER);
-  Bluefruit.setName(device_name);
+  Bluefruit.setName(dev_name);
 
   Bluefruit.Security.setMITM(true);
   Bluefruit.Security.setPIN(charpin);

+ 8 - 1
src/helpers/nrf52/SerialBLEInterface.h

@@ -52,7 +52,14 @@ public:
     recv_queue_len = 0;
   }
 
-  void begin(const char* device_name, uint32_t pin_code);
+  /**
+   * init the BLE interface.
+   * @param prefix   a prefix for the device name
+   * @param name  IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
+   * @param pin_code   the BLE security pin
+   */
+  void begin(const char* prefix, char* name, uint32_t pin_code);
+
   void disconnect();
   void enable() override;
   void disable() override;

+ 1 - 0
src/helpers/radiolib/RadioLibWrappers.cpp

@@ -105,6 +105,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
       if (err != RADIOLIB_ERR_NONE) {
         MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
         len = 0;
+        n_recv_errors++;
       } else {
       //  Serial.print("  readData() -> "); Serial.println(len);
         n_recv++;

+ 3 - 2
src/helpers/radiolib/RadioLibWrappers.h

@@ -7,7 +7,7 @@ class RadioLibWrapper : public mesh::Radio {
 protected:
   PhysicalLayer* _radio;
   mesh::MainBoard* _board;
-  uint32_t n_recv, n_sent;
+  uint32_t n_recv, n_sent, n_recv_errors;
   int16_t _noise_floor, _threshold;
   uint16_t _num_floor_samples;
   int32_t _floor_sample_sum;
@@ -45,8 +45,9 @@ public:
   void loop() override;
 
   uint32_t getPacketsRecv() const { return n_recv; }
+  uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
   uint32_t getPacketsSent() const { return n_sent; }
-  void resetStats() { n_recv = n_sent = 0; }
+  void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
 
   virtual float getLastRSSI() const override;
   virtual float getLastSNR() const override;

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

@@ -42,7 +42,7 @@ static Adafruit_BME280 BME280;
 #endif
 #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25)    // Athmospheric pressure at sea level
 #include <Adafruit_BMP280.h>
-static Adafruit_BMP280 BMP280;
+static Adafruit_BMP280 BMP280(TELEM_WIRE);
 #endif
 
 #if ENV_INCLUDE_SHTC3
@@ -58,6 +58,7 @@ static SensirionI2cSht4x SHT4X;
 
 #if ENV_INCLUDE_LPS22HB
 #include <Arduino_LPS22HB.h>
+LPS22HBClass LPS22HB(*TELEM_WIRE);
 #endif
 
 #if ENV_INCLUDE_INA3221
@@ -218,7 +219,7 @@ bool EnvironmentSensorManager::begin() {
   #endif
 
   #if ENV_INCLUDE_SHTC3
-  if (SHTC3.begin()) {
+  if (SHTC3.begin(TELEM_WIRE)) {
     MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
     SHTC3_initialized = true;
   } else {
@@ -243,7 +244,7 @@ bool EnvironmentSensorManager::begin() {
   #endif
 
   #if ENV_INCLUDE_LPS22HB
-  if (BARO.begin()) {
+  if (LPS22HB.begin()) {
     MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
     LPS22HB_initialized = true;
   } else {
@@ -407,8 +408,8 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
 
     #if ENV_INCLUDE_LPS22HB
     if (LPS22HB_initialized) {
-      telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
-      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure() * 10); // convert kPa to hPa
+      telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
+      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
     }
     #endif
 

+ 12 - 2
src/helpers/ui/SSD1306Display.cpp

@@ -7,6 +7,10 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
 }
 
 bool SSD1306Display::begin() {
+  if (!_isOn) {
+    if (_peripher_power) _peripher_power->claim();
+    _isOn = true;
+  }
   #ifdef DISPLAY_ROTATION
   display.setRotation(DISPLAY_ROTATION);
   #endif
@@ -15,12 +19,18 @@ bool SSD1306Display::begin() {
 
 void SSD1306Display::turnOn() {
   display.ssd1306_command(SSD1306_DISPLAYON);
-  _isOn = true;
+  if (!_isOn) {
+    if (_peripher_power) _peripher_power->claim();
+    _isOn = true;
+  }
 }
 
 void SSD1306Display::turnOff() {
   display.ssd1306_command(SSD1306_DISPLAYOFF);
-  _isOn = false;
+  if (_isOn) {
+    if (_peripher_power) _peripher_power->release();
+    _isOn = false;
+  }
 }
 
 void SSD1306Display::clear() {

+ 8 - 1
src/helpers/ui/SSD1306Display.h

@@ -5,6 +5,7 @@
 #include <Adafruit_GFX.h>
 #define SSD1306_NO_SPLASH
 #include <Adafruit_SSD1306.h>
+#include <helpers/RefCountedDigitalPin.h>
 
 #ifndef PIN_OLED_RESET
   #define PIN_OLED_RESET        21 // Reset pin # (or -1 if sharing Arduino reset pin)
@@ -18,10 +19,16 @@ class SSD1306Display : public DisplayDriver {
   Adafruit_SSD1306 display;
   bool _isOn;
   uint8_t _color;
+  RefCountedDigitalPin* _peripher_power;
 
   bool i2c_probe(TwoWire& wire, uint8_t addr);
 public:
-  SSD1306Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; }
+  SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), 
+      display(128, 64, &Wire, PIN_OLED_RESET),
+      _peripher_power(peripher_power)
+  {
+    _isOn = false; 
+  }
   bool begin();
 
   bool isOn() override { return _isOn; }

+ 4 - 1
src/helpers/ui/ST7789LCDDisplay.cpp

@@ -28,11 +28,14 @@ bool ST7789LCDDisplay::begin() {
       digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
     }
     if (PIN_TFT_RST != -1) {
+      pinMode(PIN_TFT_RST, OUTPUT);
+      digitalWrite(PIN_TFT_RST, LOW); 
+      delay(10);
       digitalWrite(PIN_TFT_RST, HIGH);
     }
 
     // Im not sure if this is just a t-deck problem or not, if your display is slow try this.
-    #ifdef LILYGO_TDECK
+    #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
       displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
     #endif
 

+ 2 - 2
src/helpers/ui/ST7789LCDDisplay.h

@@ -8,7 +8,7 @@
 #include <helpers/RefCountedDigitalPin.h>
 
 class ST7789LCDDisplay : public DisplayDriver {
-  #ifdef LILYGO_TDECK
+  #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
     SPIClass displaySPI;
   #endif
   Adafruit_ST7789 display;
@@ -25,7 +25,7 @@ public:
   {
     _isOn = false;
   }
-#elif LILYGO_TDECK
+#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
   ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
       displaySPI(HSPI),
       display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),

+ 34 - 0
variants/heltec_t114/T114Board.cpp

@@ -3,6 +3,35 @@
 #include <Arduino.h>
 #include <Wire.h>
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values come from variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void T114Board::initiateShutdown(uint8_t reason) {
+#if ENV_INCLUDE_GPS == 1
+  pinMode(GPS_EN, OUTPUT);
+  digitalWrite(GPS_EN, LOW);
+#endif
+  digitalWrite(SX126X_POWER_EN, LOW);
+
+  bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+                        reason == SHUTDOWN_REASON_BOOT_PROTECT);
+  pinMode(PIN_BAT_CTL, OUTPUT);
+  digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW);
+
+  if (enable_lpcomp) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void T114Board::begin() {
   NRF52Board::begin();
   NRF_POWER->DCDCEN = 1;
@@ -21,6 +50,11 @@ void T114Board::begin() {
 #endif
 
   pinMode(SX126X_POWER_EN, OUTPUT);
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  // We need to call this after we configure SX126X_POWER_EN as output but before we pull high
+  checkBootVoltage(&power_config);
+#endif
   digitalWrite(SX126X_POWER_EN, HIGH);
   delay(10); // give sx1262 some time to power up
 }

+ 9 - 4
variants/heltec_t114/T114Board.h

@@ -10,6 +10,11 @@
 #define  MV_LSB   (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range
 
 class T114Board : public NRF52BoardDCDC {
+protected:
+#ifdef NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   T114Board() : NRF52Board("T114_OTA") {}
   void begin();
@@ -42,13 +47,13 @@ public:
   }
 
   void powerOff() override {
-    #ifdef LED_PIN
+#ifdef LED_PIN
     digitalWrite(LED_PIN, HIGH);
-    #endif
-    #if ENV_INCLUDE_GPS == 1
+#endif
+#if ENV_INCLUDE_GPS == 1
     pinMode(GPS_EN, OUTPUT);
     digitalWrite(GPS_EN, LOW);
-    #endif
+#endif
     sd_power_system_off();
   }
 };

+ 1 - 0
variants/heltec_t114/platformio.ini

@@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/heltec_t114
   -I src/helpers/ui
   -D HELTEC_T114
+  -D NRF52_POWER_MANAGEMENT
   -D P_LORA_DIO_1=20
   -D P_LORA_NSS=24
   -D P_LORA_RESET=25

+ 10 - 2
variants/heltec_t114/variant.h

@@ -30,6 +30,14 @@
 
 #define AREF_VOLTAGE            (3.0)
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK 3300   // Won't boot below this voltage (mV)
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+// AIN2 = P0.04 = BATTERY_PIN / PIN_VBAT_READ
+#define PWRMGT_LPCOMP_AIN 2
+#define PWRMGT_LPCOMP_REFSEL 1  // 2/8 VDD (~3.68-4.04V)
+
 ////////////////////////////////////////////////////////////////////////////////
 // Number of pins
 
@@ -50,8 +58,8 @@
 ////////////////////////////////////////////////////////////////////////////////
 // I2C pin definition
 
-#define PIN_WIRE_SDA            (26)             // P0.26
-#define PIN_WIRE_SCL            (27)             // P0.27
+#define PIN_WIRE_SDA            (16)             // P0.16
+#define PIN_WIRE_SCL            (13)             // P0.13
 
 ////////////////////////////////////////////////////////////////////////////////
 // SPI pin definition

+ 1 - 1
variants/heltec_v3/platformio.ini

@@ -323,7 +323,7 @@ lib_deps =
 extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
-  -D MAX_CONTACTS=140
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1

+ 5 - 1
variants/heltec_v4/HeltecV4Board.cpp

@@ -86,5 +86,9 @@ void HeltecV4Board::begin() {
   }
 
   const char* HeltecV4Board::getManufacturerName() const {
-    return "Heltec V4";
+  #ifdef HELTEC_LORA_V4_TFT
+    return "Heltec V4 TFT";
+  #else
+    return "Heltec V4 OLED";
+  #endif
   }

+ 230 - 36
variants/heltec_v4/platformio.ini

@@ -20,11 +20,9 @@ build_flags =
   -D P_LORA_PA_POWER=7   ;power en
   -D P_LORA_PA_EN=2
   -D P_LORA_PA_TX_EN=46  ;enable tx
-  -D PIN_BOARD_SDA=17
-  -D PIN_BOARD_SCL=18
   -D PIN_USER_BTN=0
   -D PIN_VEXT_EN=36
-  -D PIN_VEXT_EN_ACTIVE=HIGH
+  -D PIN_VEXT_EN_ACTIVE=LOW
   -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
   -D MAX_LORA_TX_POWER=22 ; Max SX1262 output
   -D SX126X_DIO2_AS_RF_SWITCH=true
@@ -47,10 +45,44 @@ lib_deps =
   ${esp32_base.lib_deps}
   ${sensor_base.lib_deps}
 
-[env:heltec_v4_repeater]
+[heltec_v4_oled]
+extends = Heltec_lora32_v4
+build_flags =
+  ${Heltec_lora32_v4.build_flags}
+  -D HELTEC_LORA_V4_OLED
+  -D PIN_BOARD_SDA=17
+  -D PIN_BOARD_SCL=18
+  -D ENV_PIN_SDA=4
+  -D ENV_PIN_SCL=3
+build_src_filter= ${Heltec_lora32_v4.build_src_filter}
+lib_deps = ${Heltec_lora32_v4.lib_deps}
+
+[heltec_v4_tft]
 extends = Heltec_lora32_v4
 build_flags =
   ${Heltec_lora32_v4.build_flags}
+  -D HELTEC_LORA_V4_TFT
+  -D PIN_BOARD_SDA=4
+  -D PIN_BOARD_SCL=3
+  -D DISPLAY_SCALE_X=2.5
+  -D DISPLAY_SCALE_Y=3.75
+  -D PIN_TFT_RST=18
+  -D PIN_TFT_VDD_CTL=-1
+  -D PIN_TFT_LEDA_CTL=21
+  -D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
+  -D PIN_TFT_CS=15
+  -D PIN_TFT_DC=16
+  -D PIN_TFT_SCL=17
+  -D PIN_TFT_SDA=33
+build_src_filter= ${Heltec_lora32_v4.build_src_filter}
+lib_deps = 
+  ${Heltec_lora32_v4.lib_deps}
+  adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
+
+[env:heltec_v4_repeater]
+extends = heltec_v4_oled
+build_flags =
+  ${heltec_v4_oled.build_flags}
   -D DISPLAY_CLASS=SSD1306Display
   -D ADVERT_NAME='"Heltec Repeater"'
   -D ADVERT_LAT=0.0
@@ -59,18 +91,18 @@ build_flags =
   -D MAX_NEIGHBOURS=50
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   ${esp32_ota.lib_deps}
   bakercp/CRC32 @ ^2.0.0
 
 [env:heltec_v4_repeater_bridge_espnow]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -D DISPLAY_CLASS=SSD1306Display
   -D ADVERT_NAME='"ESPNow Bridge"'
   -D ADVERT_LAT=0.0
@@ -81,18 +113,18 @@ build_flags =
 ;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/bridges/ESPNowBridge.cpp>
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   ${esp32_ota.lib_deps}
 
 [env:heltec_v4_room_server]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -D DISPLAY_CLASS=SSD1306Display
   -D ADVERT_NAME='"Heltec Room"'
   -D ADVERT_LAT=0.0
@@ -101,50 +133,50 @@ build_flags =
   -D ROOM_PASSWORD='"hello"'
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   ${esp32_ota.lib_deps}
 
 [env:heltec_v4_terminal_chat]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<../examples/simple_secure_chat/main.cpp>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [env:heltec_v4_companion_radio_usb]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [env:heltec_v4_companion_radio_ble]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
@@ -155,20 +187,20 @@ build_flags =
   -D OFFLINE_QUEUE_SIZE=256
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [env:heltec_v4_companion_radio_wifi]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
+  ${heltec_v4_oled.build_flags}
   -I examples/companion_radio/ui-new
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
@@ -176,24 +208,23 @@ build_flags =
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'
   -D WIFI_PWD='"mypwd"'
-  -D OFFLINE_QUEUE_SIZE=256
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [env:heltec_v4_sensor]
-extends = Heltec_lora32_v4
+extends = heltec_v4_oled
 build_flags =
-  ${Heltec_lora32_v4.build_flags}
-  -D ADVERT_NAME='"Heltec v3 Sensor"'
+  ${heltec_v4_oled.build_flags}
+  -D ADVERT_NAME='"Heltec v4 Sensor"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
   -D ADMIN_PASSWORD='"password"'
@@ -202,9 +233,172 @@ build_flags =
   -D DISPLAY_CLASS=SSD1306Display
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+build_src_filter = ${heltec_v4_oled.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_sensor>
 lib_deps =
-  ${Heltec_lora32_v4.lib_deps}
+  ${heltec_v4_oled.lib_deps}
+  ${esp32_ota.lib_deps}
+
+
+[env:heltec_v4_tft_repeater]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+  -D ADVERT_NAME='"Heltec Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  ${esp32_ota.lib_deps}
+  bakercp/CRC32 @ ^2.0.0
+
+
+[env:heltec_v4_tft_repeater_bridge_espnow]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+  -D ADVERT_NAME='"ESPNow Bridge"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+  -D WITH_ESPNOW_BRIDGE=1
+;  -D BRIDGE_DEBUG=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/bridges/ESPNowBridge.cpp>
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:heltec_v4_tft_room_server]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+  -D ADVERT_NAME='"Heltec Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<../examples/simple_room_server>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:heltec_v4_tft_terminal_chat]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_v4_tft_companion_radio_usb]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
+; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_v4_tft_companion_radio_ble]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -I examples/companion_radio/ui-new
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D BLE_PIN_CODE=123456   ; dynamic, random PIN
+  -D AUTO_SHUTDOWN_MILLIVOLTS=3400
+  -D BLE_DEBUG_LOGGING=1
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_v4_tft_companion_radio_wifi]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+  -D WIFI_DEBUG_LOGGING=1
+  -D WIFI_SSID='"myssid"'
+  -D WIFI_PWD='"mypwd"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_v4_tft_sensor]
+extends = heltec_v4_tft
+build_flags =
+  ${heltec_v4_tft.build_flags}
+  -D ADVERT_NAME='"Heltec v4 Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ENV_PIN_SDA=3
+  -D ENV_PIN_SCL=4
+  -D DISPLAY_CLASS=ST7789LCDDisplay
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${heltec_v4_tft.build_src_filter}
+  +<helpers/ui/ST7789LCDDisplay.cpp>
+  +<../examples/simple_sensor>
+lib_deps =
+  ${heltec_v4_tft.lib_deps}
   ${esp32_ota.lib_deps}

+ 1 - 1
variants/heltec_v4/target.cpp

@@ -24,7 +24,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
 #endif
 
 #ifdef DISPLAY_CLASS
-  DISPLAY_CLASS display;
+  DISPLAY_CLASS display(&(board.periph_power));
   MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
 #endif
 

+ 5 - 1
variants/heltec_v4/target.h

@@ -9,7 +9,11 @@
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>
 #ifdef DISPLAY_CLASS
-  #include <helpers/ui/SSD1306Display.h>
+#ifdef HELTEC_LORA_V4_OLED
+    #include <helpers/ui/SSD1306Display.h>
+#elif defined(HELTEC_LORA_V4_TFT)
+    #include <helpers/ui/ST7789LCDDisplay.h>
+#endif
   #include <helpers/ui/MomentaryButton.h>
 #endif
 

+ 4 - 4
variants/heltec_wireless_paper/platformio.ini

@@ -5,7 +5,7 @@ build_flags =
   ${esp32_base.build_flags}
   -I variants/heltec_wireless_paper
   -D HELTEC_WIRELESS_PAPER
-  -D ARDUINO_USB_CDC_ON_BOOT=1      ; need for Serial
+  ;-D ARDUINO_USB_CDC_ON_BOOT=1      ; this breaks Serial
   -D P_LORA_DIO_1=14
   -D P_LORA_NSS=8
   -D P_LORA_RESET=RADIOLIB_NC
@@ -17,8 +17,8 @@ build_flags =
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D LORA_TX_POWER=22
   -D P_LORA_TX_LED=18
-  -D PIN_BOARD_SDA=17
-  -D PIN_BOARD_SCL=18
+  ;-D PIN_BOARD_SDA=17
+  ;-D PIN_BOARD_SCL=18               ; same GPIO as P_LORA_TX_LED
   -D PIN_USER_BTN=0
   -D PIN_VEXT_EN=45
   -D PIN_VBAT_READ=20
@@ -139,4 +139,4 @@ build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}
   +<../examples/simple_room_server>
 lib_deps =
   ${Heltec_Wireless_Paper_base.lib_deps}
-  ${esp32_ota.lib_deps}
+  ${esp32_ota.lib_deps}

+ 24 - 24
variants/ikoka_handheld_nrf/platformio.ini

@@ -1,8 +1,5 @@
-[ikoka_nrf52]
-extends = Xiao_nrf52
-lib_deps = ${nrf52_base.lib_deps}
-  ${sensor_base.lib_deps}
-  densaugeo/base64 @ ~1.4.0  
+[ikoka_handheld_nrf]
+extends = nrf52_base
 build_flags = ${nrf52_base.build_flags}
   ${sensor_base.build_flags}
   -I lib/nrf52/s140_nrf52_7.3.0_API/include
@@ -26,12 +23,15 @@ build_flags = ${nrf52_base.build_flags}
 build_src_filter = ${nrf52_base.build_src_filter}
   +<../variants/ikoka_handheld_nrf>
   +<helpers/sensors>
+lib_deps = ${nrf52_base.lib_deps}
+  ${sensor_base.lib_deps}
+  densaugeo/base64 @ ~1.4.0
 
 # larger screen has a different driver, this is for the 0.96 inch
-[ikoka_nrf52_ssd1306_companion]
-lib_deps = ${ikoka_nrf52.lib_deps}
+[ikoka_handheld_nrf_ssd1306_companion]
+lib_deps = ${ikoka_handheld_nrf.lib_deps}
   adafruit/Adafruit SSD1306 @ ^2.5.13
-build_flags = ${ikoka_nrf52.build_flags}
+build_flags = ${ikoka_handheld_nrf.build_flags}
   -D DISPLAY_CLASS=SSD1306Display
   -D DISPLAY_ROTATION=0
   -D PIN_WIRE_SCL=D6
@@ -42,62 +42,62 @@ build_flags = ${ikoka_nrf52.build_flags}
   -D OFFLINE_QUEUE_SIZE=256
   -D QSPIFLASH=1
   -I examples/companion_radio/ui-new
-build_src_filter = ${ikoka_nrf52.build_src_filter}
+build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio/ui-new/UITask.cpp>
   +<../examples/companion_radio/*.cpp>
 
 [env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_ble]
 extends = ikoka_nrf52
-build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
+build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
   -D BLE_PIN_CODE=123456
   -D LORA_TX_POWER=20
-build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
+build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
 
 [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_ble]
 extends = ikoka_nrf52
-build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
+build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
   -D BLE_PIN_CODE=123456
   -D LORA_TX_POWER=20
-  -D DISPLAY_ROTATION=2    
-build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
+  -D DISPLAY_ROTATION=2
+build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
 
 [env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_usb]
 extends = ikoka_nrf52
-build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
+build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
   -D LORA_TX_POWER=20
-build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
+build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
 
 [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb]
 extends = ikoka_nrf52
-build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
+build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
   -D LORA_TX_POWER=20
   -D DISPLAY_ROTATION=2
-build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
+build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
 
 [env:ikoka_handheld_nrf_e22_30dbm_repeater]
 extends = ikoka_nrf52
 build_flags =
-  ${ikoka_nrf52.build_flags}
+  ${ikoka_handheld_nrf.build_flags}
   -D ADVERT_NAME='"ikoka_handheld Repeater"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
-  -D LORA_TX_POWER=20  
-build_src_filter = ${ikoka_nrf52.build_src_filter}
+  -D LORA_TX_POWER=20
+build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
   +<../examples/simple_repeater/*.cpp>
 
 [env:ikoka_handheld_nrf_e22_30dbm_room_server]
 extends = ikoka_nrf52
 build_flags =
-  ${ikoka_nrf52.build_flags}
+  ${ikoka_handheld_nrf.build_flags}
   -D ADVERT_NAME='"ikoka_handheld Room"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
   -D ADMIN_PASSWORD='"password"'
-  -D LORA_TX_POWER=20  
-build_src_filter = ${ikoka_nrf52.build_src_filter}
+  -D LORA_TX_POWER=20
+build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
   +<../examples/simple_room_server/*.cpp>

+ 36 - 68
variants/ikoka_nano_nrf/platformio.ini

@@ -1,151 +1,124 @@
-[nrf52840_xiao]
+[ikoka_nano_nrf]
 extends = nrf52_base
-platform_packages =
-  toolchain-gccarmnoneeabi@~1.100301.0
-  framework-arduinoadafruitnrf52
 board = seeed-xiao-afruitnrf52-nrf52840
 board_build.ldscript = boards/nrf52840_s140_v7.ld
 build_flags = ${nrf52_base.build_flags}
+   ${sensor_base.build_flags}
   -D NRF52_PLATFORM -D XIAO_NRF52
   -I lib/nrf52/s140_nrf52_7.3.0_API/include
   -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
-lib_ignore =
-  BluetoothOTA
-  lvgl
-  lib5b4
-lib_deps =
-  ${nrf52_base.lib_deps}
-  rweather/Crypto @ ^0.4.0
-  adafruit/Adafruit INA3221 Library @ ^1.0.1
-  adafruit/Adafruit INA219 @ ^1.2.3
-  adafruit/Adafruit AHTX0 @ ^2.0.5
-  adafruit/Adafruit BME280 Library @ ^2.3.0
-  adafruit/Adafruit SSD1306 @ ^2.5.13
-
-[ikoka_nano_nrf_baseboard]
-extends = nrf52840_xiao
-;board_build.ldscript = boards/nrf52840_s140_v7.ld
-build_flags = ${nrf52840_xiao.build_flags}
-  -D P_LORA_TX_LED=11
   -I variants/ikoka_nano_nrf
   -I src/helpers/nrf52
+  -D P_LORA_TX_LED=11
   -D DISPLAY_CLASS=NullDisplayDriver
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D P_LORA_DIO_1=D1
-;  -D P_LORA_BUSY=D3
-  -D P_LORA_BUSY=D2 ; specific to ikoka nano variant.
-;  -D P_LORA_RESET=D2
-  -D P_LORA_RESET=D3 ; specific to ikoka nano variant.
-;  -D P_LORA_NSS=D4
-  -D P_LORA_NSS=D0 ; specific to ikoka nano variant.
-;  -D SX126X_RXEN=D5
+  -D P_LORA_BUSY=D2
+  -D P_LORA_RESET=D3
+  -D P_LORA_NSS=D0
   -D SX126X_RXEN=D7
   -D SX126X_TXEN=RADIOLIB_NC
   -D SX126X_DIO2_AS_RF_SWITCH=1
   -D SX126X_DIO3_TCXO_VOLTAGE=1.8
   -D SX126X_CURRENT_LIMIT=140
   -D SX126X_RX_BOOSTED_GAIN=1
-  -D PIN_WIRE_SCL=5 ; specific to ikoka nano variant.
-  -D PIN_WIRE_SDA=4 ; specific to ikoka nano variant.
-  -D ENV_INCLUDE_AHTX0=1
-  -D ENV_INCLUDE_BME280=1
-  -D ENV_INCLUDE_INA3221=1
-  -D ENV_INCLUDE_INA219=1
+  -D PIN_WIRE_SCL=5
+  -D PIN_WIRE_SDA=4
+  -UENV_INCLUDE_GPS
 debug_tool = jlink
 upload_protocol = nrfutil
-
-
-;;; abstracted hardware variants
+lib_deps = ${nrf52_base.lib_deps}
+   ${sensor_base.lib_deps}
 
 [ikoka_nano_nrf_e22_22dbm]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 ; No PA in this model, full 22dBm
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Nano-E22-22dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=22
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/NullDisplayDriver.cpp>
   +<../variants/ikoka_nano_nrf>
 
 [ikoka_nano_nrf_e22_30dbm]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 ; limit txpower to 20dBm on E22-900M30S. Anything higher will
 ; cause distortion in the PA output. 20dBm in -> 30dBm out
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Nano-E22-30dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=20
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/NullDisplayDriver.cpp>
   +<../variants/ikoka_nano_nrf>
 
 [ikoka_nano_nrf_e22_33dbm]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 ; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
 ; to the rf amplifier frontend. 9dBm in -> 33dBm out
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Nano-E22-33dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=9
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/NullDisplayDriver.cpp>
   +<../variants/ikoka_nano_nrf>
 
-;;; abstracted firmware roles
-
 [ikoka_nano_nrf_companion_radio_ble]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
 board_upload.maximum_size = 708608
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456
   -D OFFLINE_QUEUE_SIZE=256
   -I examples/companion_radio/ui-new
+  -D QSPIFLASH=1
 ;  -D BLE_DEBUG_LOGGING=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${ikoka_nano_nrf_baseboard.lib_deps}
+  ${ikoka_nano_nrf.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [ikoka_nano_nrf_companion_radio_usb]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
 board_upload.maximum_size = 708608
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -I examples/companion_radio/ui-new
+  -D QSPIFLASH=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${ikoka_nano_nrf_baseboard.lib_deps}
+  ${ikoka_nano_nrf.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [ikoka_nano_nrf_repeater]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D ADVERT_NAME='"Ikoka Nano Repeater"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
@@ -153,26 +126,23 @@ build_flags =
   -D MAX_NEIGHBOURS=50
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<../examples/simple_repeater/*.cpp>
 
 [ikoka_nano_nrf_room_server]
-extends = ikoka_nano_nrf_baseboard
+extends = ikoka_nano_nrf
 build_flags =
-  ${ikoka_nano_nrf_baseboard.build_flags}
+  ${ikoka_nano_nrf.build_flags}
   -D ADVERT_NAME='"Ikoka Nano Room"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
   -D ADMIN_PASSWORD='"password"'
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_nano_nrf.build_src_filter}
   +<../examples/simple_room_server/*.cpp>
 
-;;; hardware + firmware variants
-
 ;;;    22dBm EBYTE E22-900M22 variants
-
 [env:ikoka_nano_nrf_22dbm_companion_radio_usb]
 extends =
    ikoka_nano_nrf_e22_22dbm
@@ -219,7 +189,6 @@ build_src_filter =
 
 
 ;;;    30dBm EBYTE E22-900M30 variants
-
 [env:ikoka_nano_nrf_30dbm_companion_radio_usb]
 extends =
    ikoka_nano_nrf_e22_30dbm
@@ -266,7 +235,6 @@ build_src_filter =
 
 
 ;;;    33dBm EBYTE E22-900M33 variants
-
 [env:ikoka_nano_nrf_33dbm_companion_radio_usb]
 extends =
    ikoka_nano_nrf_e22_33dbm

+ 31 - 54
variants/ikoka_stick_nrf/platformio.ini

@@ -1,34 +1,15 @@
-[nrf52840_xiao]
+[ikoka_stick_nrf]
 extends = nrf52_base
-platform_packages =
-  toolchain-gccarmnoneeabi@~1.100301.0
-  framework-arduinoadafruitnrf52
 board = seeed-xiao-afruitnrf52-nrf52840
 board_build.ldscript = boards/nrf52840_s140_v7.ld
 build_flags = ${nrf52_base.build_flags}
+  ${sensor_base.build_flags}
   -D NRF52_PLATFORM -D XIAO_NRF52
   -I lib/nrf52/s140_nrf52_7.3.0_API/include
   -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
-lib_ignore =
-  BluetoothOTA
-  lvgl
-  lib5b4
-lib_deps =
-  ${nrf52_base.lib_deps}
-  rweather/Crypto @ ^0.4.0
-  adafruit/Adafruit INA3221 Library @ ^1.0.1
-  adafruit/Adafruit INA219 @ ^1.2.3
-  adafruit/Adafruit AHTX0 @ ^2.0.5
-  adafruit/Adafruit BME280 Library @ ^2.3.0
-  adafruit/Adafruit SSD1306 @ ^2.5.13
-
-[ikoka_stick_nrf_baseboard]
-extends = nrf52840_xiao
-;board_build.ldscript = boards/nrf52840_s140_v7.ld
-build_flags = ${nrf52840_xiao.build_flags}
-  -D P_LORA_TX_LED=11
   -I variants/ikoka_stick_nrf
   -I src/helpers/nrf52
+  -D P_LORA_TX_LED=11
   -D DISPLAY_CLASS=SSD1306Display
   -D DISPLAY_ROTATION=2
   -D RADIO_CLASS=CustomSX1262
@@ -46,24 +27,18 @@ build_flags = ${nrf52840_xiao.build_flags}
   -D PIN_USER_BTN=0
   -D PIN_WIRE_SCL=7
   -D PIN_WIRE_SDA=6
-  -D ENV_INCLUDE_AHTX0=1
-  -D ENV_INCLUDE_BME280=1
-  -D ENV_INCLUDE_INA3221=1
-  -D ENV_INCLUDE_INA219=1
-debug_tool = jlink
-upload_protocol = nrfutil
-
-
-;;; abstracted hardware variants
+  -UENV_INCLUDE_GPS
+lib_deps = ${nrf52_base.lib_deps}
+  ${sensor_base.lib_deps}
 
 [ikoka_stick_nrf_e22_22dbm]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 ; No PA in this model, full 22dBm
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Stick-E22-22dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=22
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/MomentaryButton.cpp>
@@ -71,14 +46,14 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
   +<../variants/ikoka_stick_nrf>
 
 [ikoka_stick_nrf_e22_30dbm]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 ; limit txpower to 20dBm on E22-900M30S. Anything higher will
 ; cause distortion in the PA output. 20dBm in -> 30dBm out
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Stick-E22-30dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=20
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/MomentaryButton.cpp>
@@ -86,14 +61,14 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
   +<../variants/ikoka_stick_nrf>
 
 [ikoka_stick_nrf_e22_33dbm]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 ; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
 ; to the rf amplifier frontend. 9dBm in -> 33dBm out
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D MANUFACTURER_STRING='"Ikoka Stick-E22-33dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=9
-build_src_filter = ${nrf52840_xiao.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
   +<helpers/ui/MomentaryButton.cpp>
@@ -103,50 +78,52 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
 ;;; abstracted firmware roles
 
 [ikoka_stick_nrf_companion_radio_ble]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
 board_upload.maximum_size = 708608
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456
   -D OFFLINE_QUEUE_SIZE=256
   -I examples/companion_radio/ui-new
+  -D QSPIFLASH=1
 ;  -D BLE_DEBUG_LOGGING=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${ikoka_stick_nrf_baseboard.lib_deps}
+  ${ikoka_stick_nrf.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [ikoka_stick_nrf_companion_radio_usb]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
 board_upload.maximum_size = 708608
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -I examples/companion_radio/ui-new
+  -D QSPIFLASH=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
 lib_deps =
-  ${ikoka_stick_nrf_baseboard.lib_deps}
+  ${ikoka_stick_nrf.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
 [ikoka_stick_nrf_repeater]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D ADVERT_NAME='"Ikoka Stick Repeater"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
@@ -154,21 +131,21 @@ build_flags =
   -D MAX_NEIGHBOURS=50
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater/*.cpp>
 
 [ikoka_stick_nrf_room_server]
-extends = ikoka_stick_nrf_baseboard
+extends = ikoka_stick_nrf
 build_flags =
-  ${ikoka_stick_nrf_baseboard.build_flags}
+  ${ikoka_stick_nrf.build_flags}
   -D ADVERT_NAME='"Ikoka Stick Room"'
   -D ADVERT_LAT=0.0
   -D ADVERT_LON=0.0
   -D ADMIN_PASSWORD='"password"'
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+build_src_filter = ${ikoka_stick_nrf.build_src_filter}
   +<../examples/simple_room_server/*.cpp>
 
 ;;; hardware + firmware variants

+ 0 - 2
variants/lilygo_t3s3_sx1276/platformio.ini

@@ -138,8 +138,6 @@ build_flags =
   -D DISPLAY_CLASS=SSD1306Display
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
-  -D MESH_PACKET_LOGGING=1
-  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>

+ 44 - 0
variants/meshtiny/MeshtinyBoard.cpp

@@ -0,0 +1,44 @@
+#include "MeshtinyBoard.h"
+
+#include <Arduino.h>
+#include <Wire.h>
+#include <bluefruit.h>
+
+static BLEDfu bledfu;
+
+static void connect_callback(uint16_t conn_handle) {
+  (void)conn_handle;
+  MESH_DEBUG_PRINTLN("BLE client connected");
+}
+
+static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
+  (void)conn_handle;
+  (void)reason;
+
+  MESH_DEBUG_PRINTLN("BLE client disconnected");
+}
+
+void MeshtinyBoard::begin() {
+  NRF52BoardDCDC::begin();
+  btn_prev_state = HIGH;
+
+  pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input
+
+  // Set all button pins to INPUT_PULLUP
+  pinMode(PIN_BUTTON1, INPUT_PULLUP);
+  pinMode(PIN_BUTTON2, INPUT_PULLUP);
+  pinMode(PIN_BUTTON3, INPUT_PULLUP);
+  pinMode(PIN_BUTTON4, INPUT_PULLUP);
+
+#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
+  Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
+#endif
+
+  Wire.begin();
+
+  pinMode(SX126X_POWER_EN, OUTPUT);
+  digitalWrite(SX126X_POWER_EN, HIGH);
+  delay(10);   // give sx1262 some time to power up
+}
+
+

+ 66 - 0
variants/meshtiny/MeshtinyBoard.h

@@ -0,0 +1,66 @@
+#pragma once
+
+#include <Arduino.h>
+#include <MeshCore.h>
+#include <helpers/NRF52Board.h>
+
+class MeshtinyBoard : public NRF52BoardDCDC, public NRF52BoardOTA {
+protected:
+  uint8_t btn_prev_state;
+
+public:
+  MeshtinyBoard() : NRF52BoardOTA("Meshtiny OTA") {}
+  void begin();
+
+#if defined(P_LORA_TX_LED)
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED on
+  }
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED off
+  }
+#endif
+
+  uint16_t getBattMilliVolts() override {
+    int adcvalue = 0;
+    analogReadResolution(12);
+    analogReference(AR_INTERNAL_3_0);
+    delay(10);
+    adcvalue = analogRead(PIN_VBAT_READ);
+    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
+  }
+
+  const char *getManufacturerName() const override { return "Meshtiny"; }
+
+  void reboot() override { NVIC_SystemReset(); }
+
+  void powerOff() override {
+
+#ifdef PIN_USER_BTN
+    while (digitalRead(PIN_USER_BTN) == LOW) {
+      delay(10);
+    }
+#endif
+
+#ifdef PIN_3V3_EN
+    pinMode(PIN_3V3_EN, OUTPUT);
+    digitalWrite(PIN_3V3_EN, LOW);
+#endif
+
+
+#ifdef PIN_LED1
+    digitalWrite(PIN_LED1, LOW);
+#endif
+
+#ifdef PIN_LED2
+    digitalWrite(PIN_LED2, LOW);
+#endif
+
+#ifdef PIN_USER_BTN
+    nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
+#endif
+
+    sd_power_system_off();
+  }
+
+};

+ 68 - 0
variants/meshtiny/platformio.ini

@@ -0,0 +1,68 @@
+[Meshtiny]
+extends = nrf52_base
+board = meshtiny
+board_build.ldscript = boards/nrf52840_s140_v6.ld
+build_flags = ${nrf52_base.build_flags}
+  -I lib/nrf52/s140_nrf52_6.1.1_API/include
+  -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
+  -I variants/meshtiny
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D LORA_TX_POWER=22
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D PIN_3V3_EN=34
+  -D MESHTINY
+  -D UI_HAS_JOYSTICK
+build_src_filter = ${nrf52_base.build_src_filter}
+  +<../variants/meshtiny>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/buzzer.cpp>
+  +<helpers/sensors>
+lib_deps =
+  ${nrf52_base.lib_deps}
+  adafruit/Adafruit SSD1306 @ ^2.5.13
+  end2endzone/NonBlockingRTTTL@^1.3.0
+
+[env:Meshtiny_companion_radio_usb]
+extends = Meshtiny
+build_flags =
+  ${Meshtiny.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MESHTINY
+  -D PIN_BUZZER=30
+  -D DISPLAY_CLASS=SSD1306Display
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Meshtiny.build_src_filter}
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Meshtiny.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:Meshtiny_companion_radio_ble]
+extends = Meshtiny
+build_flags =
+  ${Meshtiny.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MESHTINY
+  -D PIN_BUZZER=30
+  -D DISPLAY_CLASS=SSD1306Display
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D BLE_PIN_CODE=123456
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+;  -D BLE_DEBUG_LOGGING=1
+build_src_filter = ${Meshtiny.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Meshtiny.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 47 - 0
variants/meshtiny/target.cpp

@@ -0,0 +1,47 @@
+#include "target.h"
+
+#include <Arduino.h>
+#include <helpers/ArduinoHelpers.h>
+
+MeshtinyBoard board;
+
+RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+VolatileRTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+EnvironmentSensorManager sensors = EnvironmentSensorManager();
+
+#ifdef DISPLAY_CLASS
+DISPLAY_CLASS display;
+  MomentaryButton user_btn(ENCODER_PRESS, 1000, true, true);
+  MomentaryButton joystick_left(ENCODER_LEFT, 1000, true, true);
+  MomentaryButton joystick_right(ENCODER_RIGHT, 1000, true, true);
+  MomentaryButton back_btn(PIN_SIDE_BUTTON, 1000, true, true);
+#endif
+
+bool radio_init() {
+  rtc_clock.begin(Wire);
+  return radio.std_init(&SPI);
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng); // create new random identity
+}

+ 33 - 0
variants/meshtiny/target.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <MeshtinyBoard.h>
+#include <RadioLib.h>
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#ifdef DISPLAY_CLASS
+#include <helpers/ui/MomentaryButton.h>
+#include <helpers/ui/SSD1306Display.h>
+#endif
+#include <helpers/sensors/EnvironmentSensorManager.h>
+
+extern MeshtinyBoard board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern EnvironmentSensorManager sensors;
+
+#ifdef DISPLAY_CLASS
+extern DISPLAY_CLASS display;
+extern MomentaryButton user_btn;
+extern MomentaryButton joystick_left;
+extern MomentaryButton joystick_right;
+extern MomentaryButton back_btn;
+#endif
+
+bool radio_init();
+uint32_t radio_get_rng_seed();
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
+void radio_set_tx_power(uint8_t dbm);
+mesh::LocalIdentity radio_new_identity();

+ 51 - 0
variants/meshtiny/variant.cpp

@@ -0,0 +1,51 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+  // P0
+  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+  29, 30, 31,
+
+  // P1
+  32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47
+};
+
+void initVariant() {
+  // LED1 & LED2
+#ifdef PIN_LED1
+  pinMode(PIN_LED1, OUTPUT);
+  ledOff(PIN_LED1);
+#endif
+
+#ifdef PIN_LED2
+  pinMode(PIN_LED2, OUTPUT);
+  ledOff(PIN_LED2);
+#endif
+
+  // 3V3 Power Rail - nothing connected on meshtiny
+  pinMode(PIN_3V3_EN, OUTPUT);
+  digitalWrite(PIN_3V3_EN, LOW);
+}

+ 98 - 0
variants/meshtiny/variant.h

@@ -0,0 +1,98 @@
+#ifndef _MESHTINY_H_
+#define _MESHTINY_H_
+
+/** Master clock frequency */
+#define VARIANT_MCK                     (64000000ul)
+
+#define USE_LFXO                        // Board uses 32khz crystal for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#define PINS_COUNT                      (48)
+#define NUM_DIGITAL_PINS                (48)
+#define NUM_ANALOG_INPUTS               (6)
+#define NUM_ANALOG_OUTPUTS              (0)
+
+// LEDs
+#define PIN_LED1                        (35) // Green LED
+#define PIN_LED2                        (36) // Blue LED
+
+#define LED_RED                         (-1)
+#define LED_GREEN                       PIN_LED1
+#define LED_BLUE                        (-1)  // Disable annoying flashing caused by Bluefruit
+
+#define P_LORA_TX_LED                   PIN_LED2 // Blue LED
+// #define PIN_STATUS_LED                  LED_GREEN  // disable status led.
+#define LED_BUILTIN                     LED_GREEN
+#define PIN_LED                         LED_BUILTIN
+#define LED_PIN                         LED_BUILTIN
+#define LED_STATE_ON                    HIGH
+
+// Buttons
+#define PIN_BUTTON1                     (9)   // side button
+#define PIN_BUTTON2                     (4)   // encoder left
+#define PIN_BUTTON3                     (26)  // encoder right
+#define PIN_BUTTON4                     (28)  // encoder press
+#define PIN_SIDE_BUTTON                 PIN_BUTTON1
+#define ENCODER_LEFT                    PIN_BUTTON2
+#define ENCODER_RIGHT                   PIN_BUTTON3
+#define ENCODER_PRESS                   PIN_BUTTON4
+#define PIN_USER_BTN                    PIN_SIDE_BUTTON
+
+// VBAT sensing
+#define PIN_VBAT_READ                   (5)
+#define BATTERY_SENSE_RESOLUTION_BITS   12
+#define BATTERY_SENSE_RESOLUTION        4096.0
+#define AREF_VOLTAGE                    3.0
+#define VBAT_AR_INTERNAL                AR_INTERNAL_3_0
+#define ADC_MULTIPLIER                  1.73
+#define ADC_RESOLUTION                  14
+
+// Serial interfaces
+#define PIN_SERIAL1_RX                  (15)
+#define PIN_SERIAL1_TX                  (16)
+#define PIN_SERIAL2_RX                  (8)     // Connected to Jlink CDC
+#define PIN_SERIAL2_TX                  (6)
+
+// SPI Interfaces
+#define SPI_INTERFACES_COUNT            2
+
+#define PIN_SPI_MISO                    (45)
+#define PIN_SPI_MOSI                    (44)
+#define PIN_SPI_SCK                     (43)
+
+#define PIN_SPI1_MISO                   (29)
+#define PIN_SPI1_MOSI                   (30)
+#define PIN_SPI1_SCK                    (3)
+
+// LoRa SX1262 module pins
+#define P_LORA_SCLK                     PIN_SPI_SCK
+#define P_LORA_MISO                     PIN_SPI_MISO
+#define P_LORA_MOSI                     PIN_SPI_MOSI
+#define P_LORA_DIO_1                    (47)
+#define P_LORA_RESET                    (38)
+#define P_LORA_BUSY                     (46)
+#define P_LORA_NSS                      (42)
+#define SX126X_POWER_EN                 (37)
+
+#define SX126X_RXEN                     RADIOLIB_NC
+#define SX126X_TXEN                     RADIOLIB_NC
+
+#define SX126X_DIO2_AS_RF_SWITCH        true
+#define SX126X_DIO3_TCXO_VOLTAGE        (1.8f)
+
+// Wire Interfaces
+#define WIRE_INTERFACES_COUNT           1
+#define PIN_WIRE_SDA                    (13)
+#define PIN_WIRE_SCL                    (14)
+#define PIN_BOARD_SDA                   (13)
+#define PIN_BOARD_SCL                   (14)
+
+// Power control
+#define PIN_3V3_EN                      (34)    // nothing connected on meshtiny board
+
+#endif // _MESHTINY_H_

+ 96 - 0
variants/rak3112/RAK3112Board.h

@@ -0,0 +1,96 @@
+#pragma once
+
+#include <Arduino.h>
+#include <helpers/RefCountedDigitalPin.h>
+#include <helpers/ESP32Board.h>
+
+// built-ins
+#ifndef PIN_VBAT_READ
+  #define  PIN_VBAT_READ    1
+#endif
+#ifndef PIN_ADC_CTRL
+  #define  PIN_ADC_CTRL         36
+#endif
+#define  PIN_ADC_CTRL_ACTIVE    LOW
+#define  PIN_ADC_CTRL_INACTIVE  HIGH
+#define  ADC_MULTIPLIER   (3 * 1.73 * 1.187 * 1000)
+#define  BATTERY_SAMPLES 8
+
+#include <driver/rtc_io.h>
+
+class RAK3112Board : public ESP32Board {
+private:
+  bool adc_active_state;
+
+public:
+  RefCountedDigitalPin periph_power;
+
+  RAK3112Board() : periph_power(PIN_VEXT_EN) { }
+
+  void begin() {
+    ESP32Board::begin();
+
+    // Auto-detect correct ADC_CTRL pin polarity (different for boards >3.2)
+    pinMode(PIN_ADC_CTRL, INPUT);
+    adc_active_state = !digitalRead(PIN_ADC_CTRL);
+
+    pinMode(PIN_ADC_CTRL, OUTPUT);
+    digitalWrite(PIN_ADC_CTRL, !adc_active_state); // Initially inactive
+
+    periph_power.begin();
+
+    esp_reset_reason_t reason = esp_reset_reason();
+    if (reason == ESP_RST_DEEPSLEEP) {
+      long wakeup_source = esp_sleep_get_ext1_wakeup_status();
+      if (wakeup_source & (1 << P_LORA_DIO_1)) {  // received a LoRa packet (while in deep sleep)
+        startup_reason = BD_STARTUP_RX_PACKET;
+      }
+
+      rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
+      rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
+    }
+  }
+
+  void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
+    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
+
+    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
+    rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
+    rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
+
+    rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
+
+    if (pin_wake_btn < 0) {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet
+    } else {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet OR wake btn
+    }
+
+    if (secs > 0) {
+      esp_sleep_enable_timer_wakeup(secs * 1000000);
+    }
+
+    // Finally set ESP32 into sleep
+    esp_deep_sleep_start();   // CPU halts here and never returns!
+  }
+
+  void powerOff() override {
+    enterDeepSleep(0);
+  }
+
+  uint16_t getBattMilliVolts() override {
+    analogReadResolution(12);
+
+    uint32_t raw = 0;
+    for (int i = 0; i < BATTERY_SAMPLES; i++) {
+      raw += analogRead(PIN_VBAT_READ);
+    }
+    raw = raw / BATTERY_SAMPLES;
+
+    return (ADC_MULTIPLIER * raw) / 4096;
+  }
+
+  const char* getManufacturerName() const override {
+    return "RAK 3112";
+  }
+};

+ 223 - 0
variants/rak3112/platformio.ini

@@ -0,0 +1,223 @@
+[rak3112]
+extends = esp32_base
+board = esp32-s3-devkitc-1
+build_flags =
+  ${esp32_base.build_flags}
+  ${sensor_base.build_flags}
+  -I variants/rak3112
+  -D RAK_3112=1
+  -D ESP32_CPU_FREQ=80
+  -D P_LORA_DIO_1=47
+  -D P_LORA_NSS=7
+  -D P_LORA_RESET=8
+  -D P_LORA_BUSY=48
+  -D P_LORA_SCLK=5
+  -D P_LORA_MISO=3
+  -D P_LORA_MOSI=6
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D LORA_TX_POWER=22
+  -D P_LORA_TX_LED=46
+  -D PIN_BOARD_SDA=9
+  -D PIN_BOARD_SCL=40
+  -D PIN_USER_BTN=-1
+  -D PIN_VEXT_EN=14
+  -D SX126X_DIO2_AS_RF_SWITCH=true
+  -D SX126X_DIO3_TCXO_VOLTAGE=1.8
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D PIN_GPS_RX=43
+  -D PIN_GPS_TX=44
+;  -D PIN_GPS_EN=26
+build_src_filter = ${esp32_base.build_src_filter}
+  +<../variants/rak3112>
+  +<helpers/sensors>
+lib_deps =
+  ${esp32_base.lib_deps}
+  ${sensor_base.lib_deps}
+
+[env:RAK3112_repeater]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"RAK3112 Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${rak3112.lib_deps}
+  ${esp32_ota.lib_deps}
+  bakercp/CRC32 @ ^2.0.0
+
+[env:RAK3112_repeater_bridge_rs232]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"RS232 Bridge"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+  -D WITH_RS232_BRIDGE=Serial2
+  -D WITH_RS232_BRIDGE_RX=5
+  -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/bridges/RS232Bridge.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${rak3112.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:RAK3112_repeater_bridge_espnow]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"ESPNow Bridge"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+  -D WITH_ESPNOW_BRIDGE=1
+;  -D BRIDGE_DEBUG=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/bridges/ESPNowBridge.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${rak3112.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:RAK3112_room_server]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"RAK3112 Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_room_server>
+lib_deps =
+  ${rak3112.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:RAK3112_terminal_chat]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps =
+  ${rak3112.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK3112_companion_radio_usb]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=SSD1306Display
+; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
+; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${rak3112.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK3112_companion_radio_ble]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=SSD1306Display
+  -D BLE_PIN_CODE=123456   ; dynamic, random PIN
+  -D AUTO_SHUTDOWN_MILLIVOLTS=3400
+  -D BLE_DEBUG_LOGGING=1
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${rak3112.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK3112_companion_radio_wifi]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=SSD1306Display
+  -D WIFI_DEBUG_LOGGING=1
+  -D WIFI_SSID='"myssid"'
+  -D WIFI_PWD='"mypwd"'
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${rak3112.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK3112_sensor]
+extends = rak3112
+build_flags =
+  ${rak3112.build_flags}
+  -D ADVERT_NAME='"RAK3112 v3 Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ENV_PIN_SDA=33
+  -D ENV_PIN_SCL=34
+  -D DISPLAY_CLASS=SSD1306Display
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${rak3112.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_sensor>
+lib_deps =
+  ${rak3112.lib_deps}
+  ${esp32_ota.lib_deps}

+ 60 - 0
variants/rak3112/target.cpp

@@ -0,0 +1,60 @@
+#include <Arduino.h>
+#include "target.h"
+
+RAK3112Board board;
+
+#if defined(P_LORA_SCLK)
+  static SPIClass spi;
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#else
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
+#endif
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+ESP32RTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+
+#if ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+  EnvironmentSensorManager sensors;
+#endif
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+  MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
+#endif
+
+bool radio_init() {
+  fallback_clock.begin();
+  rtc_clock.begin(Wire);
+
+#if defined(P_LORA_SCLK)
+  return radio.std_init(&spi);
+#else
+  return radio.std_init();
+#endif
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 30 - 0
variants/rak3112/target.h

@@ -0,0 +1,30 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <RAK3112Board.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/SensorManager.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+#ifdef DISPLAY_CLASS
+  #include <helpers/ui/SSD1306Display.h>
+  #include <helpers/ui/MomentaryButton.h>
+#endif
+
+extern RAK3112Board board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern EnvironmentSensorManager sensors;
+
+#ifdef DISPLAY_CLASS
+  extern DISPLAY_CLASS display;
+  extern MomentaryButton user_btn;
+#endif
+
+bool radio_init();
+uint32_t radio_get_rng_seed();
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
+void radio_set_tx_power(uint8_t dbm);
+mesh::LocalIdentity radio_new_identity();

+ 32 - 0
variants/rak3401/RAK3401Board.cpp

@@ -0,0 +1,32 @@
+#include <Arduino.h>
+#include <Wire.h>
+
+#include "RAK3401Board.h"
+
+void RAK3401Board::begin() {
+  NRF52BoardDCDC::begin();
+  pinMode(PIN_VBAT_READ, INPUT);
+#ifdef PIN_USER_BTN
+  pinMode(PIN_USER_BTN, INPUT_PULLUP);
+#endif
+
+#ifdef PIN_USER_BTN_ANA
+  pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP);
+#endif
+
+#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
+  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
+#endif
+
+  Wire.begin();
+
+  pinMode(PIN_3V3_EN, OUTPUT);
+  digitalWrite(PIN_3V3_EN, HIGH);
+
+#ifdef P_LORA_PA_EN
+  // Initialize RAK13302 1W LoRa transceiver module PA control pin
+  pinMode(P_LORA_PA_EN, OUTPUT);
+  digitalWrite(P_LORA_PA_EN, LOW);  // Start with PA disabled
+  delay(10);  // Allow PA module to initialize
+#endif
+}

+ 70 - 0
variants/rak3401/RAK3401Board.h

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+#include <helpers/NRF52Board.h>
+
+// LoRa radio module pins for RAK13302
+#define  P_LORA_SCLK    3
+#define  P_LORA_MISO    29
+#define  P_LORA_MOSI    30
+#define  P_LORA_NSS     26
+#define  P_LORA_DIO_1   10
+#define  P_LORA_BUSY    9
+#define  P_LORA_RESET   4
+#ifndef  P_LORA_PA_EN
+  #define  P_LORA_PA_EN  31
+#endif
+
+//#define PIN_GPS_SDA       13  //GPS SDA pin (output option)
+//#define PIN_GPS_SCL       14  //GPS SCL pin (output option)
+// #define PIN_GPS_TX        16  //GPS TX pin
+// #define PIN_GPS_RX        15  //GPS RX pin
+#define PIN_GPS_1PPS      17  //GPS PPS pin
+#define GPS_BAUD_RATE   9600
+#define GPS_ADDRESS   0x42  //i2c address for GPS
+
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE   1.8
+
+
+// built-ins
+#define  PIN_VBAT_READ    5
+#define  ADC_MULTIPLIER   (3 * 1.73 * 1.187 * 1000)
+
+#define PIN_3V3_EN (34)
+#define WB_IO2 PIN_3V3_EN
+
+class RAK3401Board : public NRF52BoardDCDC, public NRF52BoardOTA {
+public:
+  RAK3401Board() : NRF52BoardOTA("RAK3401_OTA") {}
+  void begin();
+
+  #define BATTERY_SAMPLES 8
+
+  uint16_t getBattMilliVolts() override {
+    analogReadResolution(12);
+
+    uint32_t raw = 0;
+    for (int i = 0; i < BATTERY_SAMPLES; i++) {
+      raw += analogRead(PIN_VBAT_READ);
+    }
+    raw = raw / BATTERY_SAMPLES;
+
+    return (ADC_MULTIPLIER * raw) / 4096;
+  }
+
+  const char* getManufacturerName() const override {
+    return "RAK 3401";
+  }
+
+#ifdef P_LORA_PA_EN
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_PA_EN, HIGH);  // Enable PA before transmission
+  }
+
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_PA_EN, LOW);   // Disable PA after transmission to save power
+  }
+#endif
+};

+ 127 - 0
variants/rak3401/platformio.ini

@@ -0,0 +1,127 @@
+[rak3401]
+extends = nrf52_base
+board = rak3401
+board_check = true
+build_flags = ${nrf52_base.build_flags}
+  ${sensor_base.build_flags}
+  -I variants/rak3401
+  -D RAK_3401
+  -D RAK13302
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D LORA_TX_POWER=22
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+build_src_filter = ${nrf52_base.build_src_filter}
+  +<../variants/rak3401>
+  +<helpers/sensors>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+lib_deps =
+  ${nrf52_base.lib_deps}
+  ${sensor_base.lib_deps}
+  adafruit/Adafruit SSD1306 @ ^2.5.13
+  sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27
+
+[env:RAK_3401_repeater]
+extends = rak3401
+build_flags =
+  ${rak3401.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"RAK3401 1W Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+  ;-D MESH_PACKET_LOGGING=1
+  ;-D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_repeater>
+
+[env:RAK_3401_room_server]
+extends = rak3401
+build_flags =
+  ${rak3401.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"Test Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+  ;-D MESH_PACKET_LOGGING=1
+  ;-D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_room_server>
+
+[env:RAK_3401_companion_radio_usb]
+extends = rak3401
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
+build_flags =
+  ${rak3401.build_flags}
+  -I examples/companion_radio/ui-new
+  -D DISPLAY_CLASS=SSD1306Display
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
+; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${rak3401.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK_3401_companion_radio_ble]
+extends = rak3401
+board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+board_upload.maximum_size = 712704
+build_flags =
+  ${rak3401.build_flags}
+  -I examples/companion_radio/ui-new
+  -D DISPLAY_CLASS=SSD1306Display
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D BLE_PIN_CODE=123456
+  -D BLE_DEBUG_LOGGING=1
+  -D OFFLINE_QUEUE_SIZE=256
+  ;-D MESH_PACKET_LOGGING=1
+  ;-D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${rak3401.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK_3401_terminal_chat]
+extends = rak3401
+build_flags =
+  ${rak3401.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=1
+  ;-D MESH_PACKET_LOGGING=1
+  ;-D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps =
+  ${rak3401.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:RAK_3401_sensor]
+extends = rak3401
+build_flags =
+  ${rak3401.build_flags}
+  -D DISPLAY_CLASS=SSD1306Display
+  -D ADVERT_NAME='"RAK3401 Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  ;-D MESH_PACKET_LOGGING=1
+  ;-D MESH_DEBUG=1
+build_src_filter = ${rak3401.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_sensor>

+ 58 - 0
variants/rak3401/target.cpp

@@ -0,0 +1,58 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+RAK3401Board board;
+
+#ifndef PIN_USER_BTN
+  #define PIN_USER_BTN (-1)
+#endif
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+  MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
+
+  #if defined(PIN_USER_BTN_ANA)
+  MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, 20);
+  #endif
+#endif
+
+RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+VolatileRTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+
+#if ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+  EnvironmentSensorManager sensors;
+#endif
+
+bool radio_init() {
+  rtc_clock.begin(Wire);
+  return radio.std_init(&SPI);
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 30 - 0
variants/rak3401/target.h

@@ -0,0 +1,30 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <RAK3401Board.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+
+#ifdef DISPLAY_CLASS
+  #include <helpers/ui/SSD1306Display.h>
+  extern DISPLAY_CLASS display;
+  #include <helpers/ui/MomentaryButton.h>
+  extern MomentaryButton user_btn;
+  #if defined(PIN_USER_BTN_ANA)
+  extern MomentaryButton analog_btn;
+  #endif
+#endif
+
+extern RAK3401Board board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern EnvironmentSensorManager sensors;
+
+bool radio_init();
+uint32_t radio_get_rng_seed();
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
+void radio_set_tx_power(uint8_t dbm);
+mesh::LocalIdentity radio_new_identity();

+ 52 - 0
variants/rak3401/variant.cpp

@@ -0,0 +1,52 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+#include "nrf.h"
+
+const uint32_t g_ADigitalPinMap[] =
+{
+  // P0
+  0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
+  8 , 9 , 10, 11, 12, 13, 14, 15,
+  16, 17, 18, 19, 20, 21, 22, 23,
+  24, 25, 26, 27, 28, 29, 30, 31,
+
+  // P1
+  32, 33, 34, 35, 36, 37, 38, 39,
+  40, 41, 42, 43, 44, 45, 46, 47
+};
+
+
+void initVariant()
+{
+  // LED1 & LED2
+  pinMode(PIN_LED1, OUTPUT);
+  ledOff(PIN_LED1);
+
+  pinMode(PIN_LED2, OUTPUT);
+  ledOff(PIN_LED2);
+
+  // 3V3 Power Rail
+  pinMode(PIN_3V3_EN, OUTPUT);
+  digitalWrite(PIN_3V3_EN, HIGH);
+}

+ 199 - 0
variants/rak3401/variant.h

@@ -0,0 +1,199 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_RAK3401_
+#define _VARIANT_RAK3401_
+
+#define RAK4630
+
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+// define USE_LFRC    // Board uses RC for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (6)
+#define NUM_ANALOG_OUTPUTS (0)
+
+// LEDs
+#define PIN_LED1 (35)
+#define PIN_LED2 (36)
+
+#define LED_BUILTIN PIN_LED1
+#define LED_CONN PIN_LED2
+
+#define LED_GREEN PIN_LED1
+#define LED_BLUE PIN_LED2
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+/*
+ * Analog pins
+ */
+#define PIN_A0 (5)	//(3)
+#define PIN_A1 (31) //(4)
+#define PIN_A2 (28)
+#define PIN_A3 (29)
+#define PIN_A4 (30)
+#define PIN_A5 (31)
+#define PIN_A6 (0xff)
+#define PIN_A7 (0xff)
+
+	static const uint8_t A0 = PIN_A0;
+	static const uint8_t A1 = PIN_A1;
+	static const uint8_t A2 = PIN_A2;
+	static const uint8_t A3 = PIN_A3;
+	static const uint8_t A4 = PIN_A4;
+	static const uint8_t A5 = PIN_A5;
+	static const uint8_t A6 = PIN_A6;
+	static const uint8_t A7 = PIN_A7;
+#define ADC_RESOLUTION 14
+
+// Other pins
+#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
+#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
+
+#define PIN_AREF (2)
+#define PIN_NFC1 (9)
+#define WB_IO5 PIN_NFC1
+#define WB_IO4 (4)
+#define PIN_NFC2 (10)
+
+static const uint8_t AREF = PIN_AREF;
+
+/*
+ * Serial interfaces
+ */
+// TXD1 RXD1 on Base Board
+#define PIN_SERIAL1_RX (15)
+#define PIN_SERIAL1_TX (16)
+
+// Connected to Jlink CDC
+#define PIN_SERIAL2_RX (8)
+#define PIN_SERIAL2_TX (6)
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 2
+
+#define PIN_SPI_MISO (45)
+#define PIN_SPI_MOSI (44)
+#define PIN_SPI_SCK (43)
+
+#define PIN_SPI1_MISO (29)
+#define PIN_SPI1_MOSI (30)
+#define PIN_SPI1_SCK (3)
+
+	static const uint8_t SS = 42;
+	static const uint8_t MOSI = PIN_SPI_MOSI;
+	static const uint8_t MISO = PIN_SPI_MISO;
+	static const uint8_t SCK = PIN_SPI_SCK;
+
+/*
+ * Wire Interfaces
+ */
+#define WIRE_INTERFACES_COUNT 1
+
+#define PIN_WIRE_SDA (WB_I2C1_SDA)
+#define PIN_WIRE_SCL (WB_I2C1_SCL)
+
+// QSPI Pins
+// QSPI occupied by GPIO's
+#define PIN_QSPI_SCK 3
+#define PIN_QSPI_CS 26
+#define PIN_QSPI_IO0 30
+#define PIN_QSPI_IO1 29
+#define PIN_QSPI_IO2 28
+#define PIN_QSPI_IO3 2
+
+// On-board QSPI Flash
+// No onboard flash
+#define EXTERNAL_FLASH_DEVICES IS25LP080D
+#define EXTERNAL_FLASH_USE_QSPI
+
+#define P_LORA_SCK PIN_SPI1_SCK
+#define P_LORA_MISO PIN_SPI1_MISO
+#define P_LORA_MOSI PIN_SPI1_MOSI
+#define P_LORA_CS 26
+
+#define USE_SX1262
+#define SX126X_CS (26)
+#define SX126X_DIO1 (10)
+#define SX126X_BUSY (9)
+#define SX126X_RESET (4)
+
+#define SX126X_POWER_EN (21)
+// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+// enables 3.3V periphery like GPS or IO Module
+// Do not toggle this for GPS power savings
+#define PIN_3V3_EN (34)
+#define WB_IO2 PIN_3V3_EN
+
+// RAK1910 GPS module
+// If using the wisblock GPS module and pluged into Port A on WisBlock base
+// IO1 is hooked to PPS (pin 12 on header) = gpio 17
+// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on).
+// Therefore must be 1 to keep peripherals powered
+// Power is on the controllable 3V3_S rail
+#define PIN_GPS_PPS (17) // Pulse per second input from the GPS
+
+#define PIN_GPS_RX PIN_SERIAL1_RX
+#define PIN_GPS_TX PIN_SERIAL1_TX
+
+// Battery
+// The battery sense is hooked to pin A0 (5)
+#define BATTERY_PIN PIN_A0
+// and has 12 bit resolution
+#define BATTERY_SENSE_RESOLUTION_BITS 12
+#define BATTERY_SENSE_RESOLUTION 4096.0
+#undef AREF_VOLTAGE
+#define AREF_VOLTAGE 3.0
+#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
+#define ADC_MULTIPLIER 1.73
+
+#define HAS_RTC 1
+
+#define RAK_4631 1
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif

+ 27 - 0
variants/rak4631/RAK4631Board.cpp

@@ -3,6 +3,28 @@
 
 #include "RAK4631Board.h"
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values set in variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void RAK4631Board::initiateShutdown(uint8_t reason) {
+  // Disable LoRa module power before shutdown
+  digitalWrite(SX126X_POWER_EN, LOW);
+
+  if (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+      reason == SHUTDOWN_REASON_BOOT_PROTECT) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void RAK4631Board::begin() {
   NRF52BoardDCDC::begin();
   pinMode(PIN_VBAT_READ, INPUT);
@@ -21,6 +43,11 @@ void RAK4631Board::begin() {
   Wire.begin();
 
   pinMode(SX126X_POWER_EN, OUTPUT);
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  // We need to call this after we configure SX126X_POWER_EN as output but before we pull high
+  checkBootVoltage(&power_config);
+#endif
   digitalWrite(SX126X_POWER_EN, HIGH);
   delay(10);   // give sx1262 some time to power up
 }

+ 5 - 0
variants/rak4631/RAK4631Board.h

@@ -30,6 +30,11 @@
 #define  ADC_MULTIPLIER   (3 * 1.73 * 1.187 * 1000)
 
 class RAK4631Board : public NRF52BoardDCDC {
+protected:
+#ifdef NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   RAK4631Board() : NRF52Board("RAK4631_OTA") {}
   void begin();

+ 1 - 0
variants/rak4631/platformio.ini

@@ -7,6 +7,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/rak4631
   -D RAK_4631
   -D RAK_BOARD
+  -D NRF52_POWER_MANAGEMENT
   -D PIN_BOARD_SCL=14
   -D PIN_BOARD_SDA=13
   -D PIN_GPS_TX=PIN_SERIAL1_RX

+ 8 - 0
variants/rak4631/variant.h

@@ -104,6 +104,14 @@ extern "C"
 	static const uint8_t A7 = PIN_A7;
 #define ADC_RESOLUTION 14
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK 3300   // Won't boot below this voltage (mV)
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+// AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ
+#define PWRMGT_LPCOMP_AIN 3
+#define PWRMGT_LPCOMP_REFSEL 4  // 5/8 VDD (~3.13-3.44V)
+
 // Other pins
 #define PIN_AREF (2)
 #define PIN_NFC1 (9)

+ 2 - 1
variants/station_g2/platformio.ini

@@ -16,7 +16,8 @@ build_flags =
   -D P_LORA_SCLK=12
   -D P_LORA_MISO=14
   -D P_LORA_MOSI=13
-  -D LORA_TX_POWER=19
+  -D LORA_TX_POWER=7 ; configured as 7dbm, because the final output will be ~27dbm (~0.5w) if the PA is enabled.
+  -D MAX_LORA_TX_POWER=19 ; max output without burning out the PA
 ;  -D P_LORA_TX_LED=35
   -D PIN_BOARD_SDA=5
   -D PIN_BOARD_SCL=6

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

@@ -107,6 +107,7 @@ build_flags = ${t1000-e.build_flags}
   -D DISPLAY_CLASS=NullDisplayDriver
   -D PIN_BUZZER=25
   -D PIN_BUZZER_EN=37      ; P1/5 - required for T1000-E
+  -D ADVERT_NAME='"@@MAC"'
 build_src_filter = ${t1000-e.build_src_filter}
   +<helpers/nrf52/SerialBLEInterface.cpp>
   +<helpers/ui/buzzer.cpp>

+ 1 - 0
variants/thinknode_m1/platformio.ini

@@ -83,6 +83,7 @@ build_flags =
   -D PIN_BUZZER=6
   -D AUTO_SHUTDOWN_MILLIVOLTS=3300
   -D QSPIFLASH=1
+  -D ENV_INCLUDE_GPS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${ThinkNode_M1.build_src_filter}

+ 1 - 1
variants/thinknode_m1/target.cpp

@@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
 
 VolatileRTCClock fallback_clock;
 AutoDiscoverRTCClock rtc_clock(fallback_clock);
-MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
+MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
 ThinkNodeM1SensorManager sensors = ThinkNodeM1SensorManager(nmea);
 
 #ifdef DISPLAY_CLASS

+ 1 - 0
variants/thinknode_m1/target.h

@@ -22,6 +22,7 @@ class ThinkNodeM1SensorManager : public SensorManager {
   void stop_gps();
 public:
   ThinkNodeM1SensorManager(LocationProvider &location): _location(&location) { }
+  LocationProvider* getLocationProvider() override { return _location; }
   bool begin() override;
   bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
   void loop() override;

+ 1 - 0
variants/wio-tracker-l1/platformio.ini

@@ -95,6 +95,7 @@ build_flags = ${WioTrackerL1.build_flags}
   -D UI_HAS_JOYSTICK=1
   -D PIN_BUZZER=12
   -D QSPIFLASH=1
+  -D ADVERT_NAME='"@@MAC"'
   ; -D MESH_PACKET_LOGGING=1
   ; -D MESH_DEBUG=1
 build_src_filter = ${WioTrackerL1.build_src_filter}

+ 43 - 4
variants/xiao_nrf52/XiaoNrf52Board.cpp

@@ -5,12 +5,40 @@
 
 #include "XiaoNrf52Board.h"
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values set in variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void XiaoNrf52Board::initiateShutdown(uint8_t reason) {
+  bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+                        reason == SHUTDOWN_REASON_BOOT_PROTECT);
+
+  pinMode(VBAT_ENABLE, OUTPUT);
+  digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH);
+
+  if (enable_lpcomp) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void XiaoNrf52Board::begin() {
   NRF52BoardDCDC::begin();
 
+  // Configure battery voltage ADC
   pinMode(PIN_VBAT, INPUT);
   pinMode(VBAT_ENABLE, OUTPUT);
-  digitalWrite(VBAT_ENABLE, HIGH);
+  digitalWrite(VBAT_ENABLE, LOW);  // Enable VBAT divider for reading
+  analogReadResolution(12);
+  analogReference(AR_INTERNAL_3_0);
+  delay(50);  // Allow ADC to settle
 
 #ifdef PIN_USER_BTN
   pinMode(PIN_USER_BTN, INPUT_PULLUP);
@@ -27,9 +55,20 @@ void XiaoNrf52Board::begin() {
   digitalWrite(P_LORA_TX_LED, HIGH);
 #endif
 
-  //  pinMode(SX126X_POWER_EN, OUTPUT);
-  //  digitalWrite(SX126X_POWER_EN, HIGH);
-  delay(10); // give sx1262 some time to power up
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  checkBootVoltage(&power_config);
+#endif
+
+  delay(10);  // Give sx1262 some time to power up
+}
+
+uint16_t XiaoNrf52Board::getBattMilliVolts() {
+  // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+  // VBAT_ENABLE must be LOW to read battery voltage
+  digitalWrite(VBAT_ENABLE, LOW);
+  int adcvalue = analogRead(PIN_VBAT);
+  return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
 }
 
 #endif

+ 6 - 15
variants/xiao_nrf52/XiaoNrf52Board.h

@@ -7,6 +7,11 @@
 #ifdef XIAO_NRF52
 
 class XiaoNrf52Board : public NRF52BoardDCDC {
+protected:
+#if NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   XiaoNrf52Board() : NRF52Board("XIAO_NRF52_OTA") {}
   void begin();
@@ -20,21 +25,7 @@ public:
   }
 #endif
 
-  uint16_t getBattMilliVolts() override {
-    // Please read befor going further ;)
-    // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
-
-    // We can't drive VBAT_ENABLE to HIGH as long
-    // as we don't know wether we are charging or not ...
-    // this is a 3mA loss (4/1500)
-    digitalWrite(VBAT_ENABLE, LOW);
-    int adcvalue = 0;
-    analogReadResolution(12);
-    analogReference(AR_INTERNAL_3_0);
-    delay(10);
-    adcvalue = analogRead(PIN_VBAT);
-    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
-  }
+  uint16_t getBattMilliVolts() override;
 
   const char* getManufacturerName() const override {
     return "Seeed Xiao-nrf52";

+ 1 - 0
variants/xiao_nrf52/platformio.ini

@@ -9,6 +9,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/xiao_nrf52
   -UENV_INCLUDE_GPS
   -D NRF52_PLATFORM
+  -D NRF52_POWER_MANAGEMENT
   -D XIAO_NRF52
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper

+ 15 - 0
variants/xiao_nrf52/variant.h

@@ -75,6 +75,21 @@ static const uint8_t D10 = 10;
 #define AREF_VOLTAGE            (3.0)
 #define ADC_MULTIPLIER          (3.0F) // 1M, 512k divider bridge
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK    3300   // Won't boot below this voltage
+
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+#define PWRMGT_LPCOMP_AIN           7     // AIN7 = P0.31 = PIN_VBAT
+// IMPORTANT: The XIAO exposes battery via a resistor divider (ADC_MULTIPLIER = 3.0).
+// LPCOMP measures the divided voltage, not the battery voltage directly.
+// Vpin = VDD * (REFSEL fraction), and VBAT ≈ Vpin * ADC_MULTIPLIER.
+//
+// Using 3/8 VDD gives a wake threshold above the boot protection point:
+// - If VDD ≈ 3.0V:  VBAT ≈ (3.0 * 3/8) * 3 ≈ 3375mV
+// - If VDD ≈ 3.3V:  VBAT ≈ (3.3 * 3/8) * 3 ≈ 3712mV
+#define PWRMGT_LPCOMP_REFSEL   2     // 3/8 VDD (~3.38-3.71V)
+
 static const uint8_t A0  = PIN_A0;
 static const uint8_t A1  = PIN_A1;
 static const uint8_t A2  = PIN_A2;