Pārlūkot izejas kodu

Merge branch 'main' into dev

Scott Powell 5 mēneši atpakaļ
vecāks
revīzija
bb8dc5b55b

+ 100 - 6
docs/cli_commands.md

@@ -63,6 +63,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore
 
 ---
 
+### Send a zero-hop advert
+**Usage:**
+- `advert.zerohop`
+
+---
+
 ### Start an Over-The-Air (OTA) firmware update
 **Usage:**
 - `start ota`
@@ -355,13 +361,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore
 
 ---
 
+#### View this node's public key
+**Usage:** `get public.key`
+
+---
+
+#### View this node's configured role
+**Usage:** `get role`
+
+---
+
 #### View or change this node's power saving flag (Repeater Only)
 **Usage:**
-- `powersaving <state>`
 - `powersaving`
+- `powersaving on`
+- `powersaving off`
 
 **Parameters:** 
-- `state`: `on`|`off` 
+- `on`: enable power saving
+- `off`: disable power saving
 
 **Default:** `on`
 
@@ -383,6 +401,46 @@ This document provides an overview of CLI commands that can be sent to MeshCore
 
 ---
 
+#### View or change this node's advert path hash size
+**Usage:**
+- `get path.hash.mode`
+- `set path.hash.mode <value>`
+
+**Parameters:**
+- `value`: Path hash size (0-2)
+  - `0`: 1 Byte hash size (256 unique ids)[64 max flood]
+  - `1`: 2 Byte hash size (65,536 unique ids)[32 max flood]
+  - `2`: 3 Byte hash size (16,777,216 unique ids)[21 max flood]
+  - `3`: DO NOT USE (Reserved) 
+
+**Default:** `0`
+
+**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
+
+**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. 
+
+---
+
+#### View or change this node's loop detection
+**Usage:**
+- `get loop.detect`
+- `set loop.detect <state>`
+
+**Parameters:**
+- `state`: 
+  - `off`: no loop detection is performed
+  - `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte)
+  - `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
+  - `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)
+  
+**Default:** `off`
+
+**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14
+
+**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet.  If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. 
+
+---
+
 #### View or change the retransmit delay factor for flood traffic
 **Usage:**
 - `get txdelay`
@@ -804,6 +862,11 @@ region save
 
 ### Bridge (When bridge support is compiled in)
 
+#### View the compiled bridge type
+**Usage:** `get bridge.type`
+
+---
+
 #### View or change the bridge enabled flag
 **Usage:**
 - `get bridge.enabled`
@@ -841,10 +904,10 @@ region save
 
 **Parameters:**
 - `source`: 
-  - `rx`: bridges received packets
-  - `tx`: bridges transmitted packets
+  - `logRx`: bridges received packets
+  - `logTx`: bridges transmitted packets
 
-**Default:** `tx`
+**Default:** `logTx`
 
 ---
 
@@ -876,8 +939,39 @@ region save
 - `set bridge.secret <secret>`
 
 **Parameters:**
-- `secret`: 16-character encryption secret
+- `secret`: ESP-NOW bridge secret, up to 15 characters
 
 **Default:** Varies by board
 
 ---
+
+#### View the bootloader version (nRF52 only)
+**Usage:** `get bootloader.ver`
+
+---
+
+#### View power management support
+**Usage:** `get pwrmgt.support`
+
+---
+
+#### View the current power source
+**Usage:** `get pwrmgt.source`
+
+**Note:** Returns an error on boards without power management support.
+
+---
+
+#### View the boot reset and shutdown reasons
+**Usage:** `get pwrmgt.bootreason`
+
+**Note:** Returns an error on boards without power management support.
+
+---
+
+#### View the boot voltage
+**Usage:** `get pwrmgt.bootmv`
+
+**Note:** Returns an error on boards without power management support.
+
+---

+ 56 - 120
docs/companion_protocol.md

@@ -1,6 +1,6 @@
 # Companion Protocol
 
-- **Last Updated**: 2026-01-03
+- **Last Updated**: 2026-03-08
 - **Protocol Version**: Companion Firmware v1.12.0+
 
 > NOTE: This document is still in development. Some information may be inaccurate.
@@ -100,7 +100,7 @@ When writing commands to the RX characteristic, specify the write type:
 
 ### MTU (Maximum Transmission Unit)
 
-The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to:
+The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes), you may need to:
 
 1. **Request Larger MTU**: Request MTU of 512 bytes if supported
     - Android: `gatt.requestMtu(512)`
@@ -167,16 +167,16 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi
 **Command Format**:
 ```
 Byte 0: 0x01
-Byte 1: 0x03
-Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes)
+Bytes 1-7: Reserved (currently ignored by firmware)
+Bytes 8+: Application name (UTF-8, optional)
 ```
 
 **Example** (hex):
 ```
-01 03 6d 63 63 6c 69 00 00 00 00
+01 00 00 00 00 00 00 00 6d 63 63 6c 69
 ```
 
-**Response**: `PACKET_OK` (0x00)
+**Response**: `PACKET_SELF_INFO` (0x05)
 
 ---
 
@@ -216,8 +216,6 @@ Byte 1: Channel Index (0-7)
 
 **Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details
 
-**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels.
-
 ---
 
 ### 4. Set Channel
@@ -229,10 +227,10 @@ Byte 1: Channel Index (0-7)
 Byte 0: 0x20
 Byte 1: Channel Index (0-7)
 Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
-Bytes 34-65: Secret (32 bytes)
+Bytes 34-49: Secret (16 bytes)
 ```
 
-**Total Length**: 66 bytes
+**Total Length**: 50 bytes
 
 **Channel Index**:
 - Index 0: Reserved for public channels (no secret)
@@ -243,16 +241,18 @@ Bytes 34-65: Secret (32 bytes)
 - Maximum 32 bytes
 - Padded with null bytes (0x00) if shorter
 
-**Secret Field** (32 bytes):
-- For **private channels**: 32-byte secret
+**Secret Field** (16 bytes):
+- For **private channels**: 16-byte secret
 - For **public channels**: All zeros (0x00)
 
 **Example** (create channel "YourChannelName" at index 1 with secret):
 ```
 20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
-    [32 bytes of secret]
+    [16 bytes of secret]
 ```
 
+**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`.
+
 **Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure
 
 ---
@@ -304,9 +304,9 @@ Byte 0: 0x0A
 
 ---
 
-### 7. Get Battery
+### 7. Get Battery and Storage
 
-**Purpose**: Query device battery level.
+**Purpose**: Query device battery voltage and storage usage.
 
 **Command Format**:
 ```
@@ -318,7 +318,7 @@ Byte 0: 0x14
 14
 ```
 
-**Response**: `PACKET_BATTERY` (0x0C) with battery percentage
+**Response**: `PACKET_BATTERY` (0x0C) with battery millivolts and storage information
 
 ---
 
@@ -346,7 +346,7 @@ Byte 0: 0x14
 1. **Set Channel**:
     - Fetch all channel slots, and find one with empty name and all-zero secret
     - Generate or provide a 16-byte secret
-    - Send `CMD_SET_CHANNEL` with name and secret
+    - Send `CMD_SET_CHANNEL` with name and a 16-byte secret
 2. **Get Channel**:
     - Send `CMD_GET_CHANNEL` with channel index
     - Parse `RESP_CODE_CHANNEL_INFO` response
@@ -360,7 +360,7 @@ Byte 0: 0x14
 
 ### Receiving Messages
 
-Messages are received via the RX characteristic (notifications). The device sends:
+Messages are received via the TX characteristic (notifications). The device sends:
 
 1. **Channel Messages**:
    - `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format
@@ -544,10 +544,10 @@ Byte 1: Error code (optional)
 Byte 0: 0x12
 Byte 1: Channel Index
 Bytes 2-33: Channel Name (32 bytes, null-terminated)
-Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total)
+Bytes 34-49: Secret (16 bytes)
 ```
 
-**Note**: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons.
+**Note**: The device returns the 16-byte channel secret in this response.
 
 **PACKET_DEVICE_INFO** (0x0D):
 ```
@@ -562,6 +562,8 @@ Bytes 4-7: BLE PIN (32-bit little-endian)
 Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)
 Bytes 20-59: Model (40 bytes, UTF-8, null-padded)
 Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
+Byte 80: Client repeat enabled/preferred (firmware v9+)
+Byte 81: Path hash mode (firmware v10+)
 ```
 
 **Parsing Pseudocode**:
@@ -587,9 +589,7 @@ def parse_device_info(data):
 **PACKET_BATTERY** (0x0C):
 ```
 Byte 0: 0x0C
-Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100)
-
-Optional (if data size > 3):
+Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts)
 Bytes 3-6: Used Storage (32-bit little-endian, KB)
 Bytes 7-10: Total Storage (32-bit little-endian, KB)
 ```
@@ -600,14 +600,12 @@ def parse_battery(data):
     if len(data) < 3:
         return None
     
-    level = int.from_bytes(data[1:3], 'little')
-    info = {'level': level}
+    mv = int.from_bytes(data[1:3], 'little')
+    info = {'battery_mv': mv}
     
-    if len(data) > 3:
-        used_kb = int.from_bytes(data[3:7], 'little')
-        total_kb = int.from_bytes(data[7:11], 'little')
-        info['used_kb'] = used_kb
-        info['total_kb'] = total_kb
+    if len(data) >= 11:
+        info['used_kb'] = int.from_bytes(data[3:7], 'little')
+        info['total_kb'] = int.from_bytes(data[7:11], 'little')
     
     return info
 ```
@@ -629,7 +627,7 @@ Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)
 Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)
 Byte 56: Radio Spreading Factor
 Byte 57: Radio Coding Rate
-Bytes 58+: Device Name (UTF-8, variable length, null-terminated)
+Bytes 58+: Device Name (UTF-8, variable length, no null terminator required)
 ```
 
 **Parsing Pseudocode**:
@@ -680,9 +678,9 @@ def parse_self_info(data):
 **PACKET_MSG_SENT** (0x06):
 ```
 Byte 0: 0x06
-Byte 1: Message Type
-Bytes 2-5: Expected ACK (4 bytes, hex)
-Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds)
+Byte 1: Route Flag (0 = direct, 1 = flood)
+Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian)
+Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)
 ```
 
 **PACKET_ACK** (0x82):
@@ -710,89 +708,32 @@ Bytes 1-6: ACK Code (6 bytes, hex)
 
 **Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.
 
-### Partial Packet Handling
-
-BLE notifications may arrive in chunks, especially for larger packets. Implement buffering:
-
-**Implementation**:
-```python
-class PacketBuffer:
-    def __init__(self):
-        self.buffer = bytearray()
-        self.expected_length = None
-    
-    def add_data(self, data):
-        self.buffer.extend(data)
-        
-        # Check if we have a complete packet
-        if len(self.buffer) >= 1:
-            packet_type = self.buffer[0]
-            
-            # Determine expected length based on packet type
-            expected = self.get_expected_length(packet_type)
-            
-            if expected is not None and len(self.buffer) >= expected:
-                # Complete packet
-                packet = bytes(self.buffer[:expected])
-                self.buffer = self.buffer[expected:]
-                return packet
-            elif expected is None:
-                # Variable length packet - try to parse what we have
-                # Some packets have minimum length requirements
-                if self.can_parse_partial(packet_type):
-                    return self.try_parse_partial()
-        
-        return None  # Incomplete packet
-    
-    def get_expected_length(self, packet_type):
-        # Fixed-length packets
-        fixed_lengths = {
-            0x00: 5,  # PACKET_OK (minimum)
-            0x01: 2,  # PACKET_ERROR (minimum)
-            0x0A: 1,  # PACKET_NO_MORE_MSGS
-            0x14: 3,  # PACKET_BATTERY (minimum)
-        }
-        return fixed_lengths.get(packet_type)
-    
-    def can_parse_partial(self, packet_type):
-        # Some packets can be parsed partially
-        return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D]
-    
-    def try_parse_partial(self):
-        # Try to parse with available data
-        # Return packet if successfully parsed, None otherwise
-        # This is packet-type specific
-        pass
-```
+### Frame Handling
 
-**Usage**:
-```python
-buffer = PacketBuffer()
+BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.
 
-def on_notification_received(data):
-    packet = buffer.add_data(data)
-    if packet:
-        parse_and_handle_packet(packet)
-```
+- Apps should treat each characteristic write/notification as exactly one companion protocol frame
+- Apps should still validate frame lengths before parsing
+- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses
 
 ### Response Handling
 
 1. **Command-Response Pattern**:
-   - Send command via TX characteristic
-   - Wait for response via RX characteristic (notification)
+   - Send command via RX characteristic
+   - Wait for response via TX characteristic (notification)
    - Match response to command using sequence numbers or command type
    - Handle timeout (typically 5 seconds)
    - Use command queue to prevent concurrent commands
 
 2. **Asynchronous Messages**:
-   - Device may send messages at any time via RX characteristic
+   - Device may send messages at any time via TX characteristic
    - Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command
    - Parse incoming messages and route to appropriate handlers
-   - Buffer partial packets until complete
+   - Validate frame length before decoding
 
 3. **Response Matching**:
    - Match responses to commands by expected packet type:
-     - `APP_START` → `PACKET_OK`
+     - `APP_START` → `PACKET_SELF_INFO`
      - `DEVICE_QUERY` → `PACKET_DEVICE_INFO`
      - `GET_CHANNEL` → `PACKET_CHANNEL_INFO`
      - `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR`
@@ -825,16 +766,16 @@ device = scan_for_device("MeshCore")
 gatt = connect_to_device(device)
 
 # 3. Discover services and characteristics
-service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb")
-rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb")
-tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb")
+service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
+rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
+tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
 
-# 4. Enable notifications on RX characteristic
-enable_notifications(rx_char, on_notification_received)
+# 4. Enable notifications on TX characteristic
+enable_notifications(tx_char, on_notification_received)
 
 # 5. Send AppStart command
-send_command(tx_char, build_app_start())
-wait_for_response(PACKET_OK)
+send_command(rx_char, build_app_start())
+wait_for_response(PACKET_SELF_INFO)
 ```
 
 ### Creating a Private Channel
@@ -844,21 +785,16 @@ wait_for_response(PACKET_OK)
 secret_16_bytes = generate_secret(16)  # Use CSPRNG
 secret_hex = secret_16_bytes.hex()
 
-# 2. Expand secret to 32 bytes using SHA-512
-import hashlib
-sha512_hash = hashlib.sha512(secret_16_bytes).digest()
-secret_32_bytes = sha512_hash[:32]
-
-# 3. Build SET_CHANNEL command
+# 2. Build SET_CHANNEL command
 channel_name = "YourChannelName"
 channel_index = 1  # Use 1-7 for private channels
-command = build_set_channel(channel_index, channel_name, secret_32_bytes)
+command = build_set_channel(channel_index, channel_name, secret_16_bytes)
 
-# 4. Send command
-send_command(tx_char, command)
+# 3. Send command
+send_command(rx_char, command)
 response = wait_for_response(PACKET_OK)
 
-# 5. Store secret locally (device won't return it)
+# 4. Store secret locally
 store_channel_secret(channel_index, secret_hex)
 ```
 
@@ -872,7 +808,7 @@ timestamp = int(time.time())
 command = build_channel_message(channel_index, message, timestamp)
 
 # 2. Send command
-send_command(tx_char, command)
+send_command(rx_char, command)
 response = wait_for_response(PACKET_MSG_SENT)
 ```
 
@@ -887,7 +823,7 @@ def on_notification_received(data):
         handle_channel_message(message)
     elif packet_type == PACKET_MESSAGES_WAITING:
         # Poll for messages
-        send_command(tx_char, build_get_message())
+        send_command(rx_char, build_get_message())
 ```
 
 ---

+ 6 - 4
docs/faq.md

@@ -221,11 +221,11 @@ MeshCore allows you to manually broadcast your name, position and public encrypt
 * Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it.
 * Flooded means it's broadcasted out and then repeated by all the repeaters that hear it.
 
-MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command:
+MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:
 
-`set advert.interval {minutes}`
+`set flood.advert.interval {hours}`
 
-As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts.
+The separate `set advert.interval {minutes}` command controls the local zero-hop advert timer.
 
 ### 2.5. Q: Is there a hop limit?
 
@@ -260,7 +260,9 @@ Repeater or room server can be administered with one of the options below:
 ### 3.2. Q: Do I need to set the location for a repeater?
 **A:** While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:
 
-`set lat <GPS Lat> set long <GPS Lon>`
+`set lat <GPS Lat>`
+
+`set lon <GPS Lon>`
 
 You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.
 

+ 3 - 3
docs/kiss_modem_protocol.md

@@ -190,7 +190,7 @@ All values little-endian.
 | Field | Size | Description |
 |-------|------|-------------|
 | MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
-| Ciphertext | variable | AES-128-CBC encrypted data |
+| Ciphertext | variable | AES-128 block-encrypted data with zero padding |
 
 ### Airtime (Airtime response)
 
@@ -268,7 +268,7 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.
 |-----------|-----------|
 | Identity / Signing / Verification | Ed25519 |
 | Key Exchange | X25519 (ECDH) |
-| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) |
+| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) |
 | Hashing | SHA-256 |
 
 ## Notes
@@ -279,4 +279,4 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.
 - SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
 - TxDone is sent as a SetHardware event after each transmission
 - Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
-- See [packet_structure.md](./packet_structure.md) for packet format
+- See [packet_format.md](./packet_format.md) for packet format

+ 1 - 1
docs/nrf52_power_management.md

@@ -42,7 +42,7 @@ Shutdown reason codes (stored in GPREGRET2):
 | 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 |
+| SenseCAP Solar | Yes | Yes | Yes |
 | WIO Tracker L1 / L1 E-Ink | No | No | No |
 | WIO WM1110 | No | No | No |
 | Mesh Pocket | No | No | No |

+ 15 - 20
docs/payloads.md

@@ -90,23 +90,17 @@ Returned path messages provide a description of the route a packet took from the
 
 ## Request
 
-| Field        | Size (bytes)    | Description                |
-|--------------|-----------------|----------------------------|
-| timestamp    | 4               | send time (unix timestamp) |
-| request type | 1               | see below                  |
-| request data | rest of payload | depends on request type    |
+| Field        | Size (bytes)    | Description                              |
+|--------------|-----------------|------------------------------------------|
+| timestamp    | 4               | sender time (unix timestamp)             |
+| request data | rest of payload | application-defined request payload body |
 
-Request type
+For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
 
 | Value  | Name                 | Description                           |
 |--------|----------------------|---------------------------------------|
 | `0x01` | get stats            | get stats of repeater or room server  |
-| `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            |
-| `0x06` | get neighbors        | get repeater node's neighbors              |
-| `0x07` | get owner info       | get repeater firmware-ver/name/owner info  |
+| `0x02` | keepalive            | keep-alive request used for maintained connections |
 
 ### Get stats
 
@@ -133,35 +127,36 @@ Gets information about the node, possibly including the following:
 
 ### Get telemetry data
 
-Request data about sensors on the node, including battery level.
+Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
 
 ### Get Telemetry
 
-TODO
+Not defined in `BaseChatMesh`.
 
 ### Get Min/Max/Ave  (Sensor nodes)
 
-TODO
+Not defined in `BaseChatMesh`.
 
 ### Get Access List
 
-TODO
+Not defined in `BaseChatMesh`.
 
 ### Get Neighors
 
-TODO
+Not defined in `BaseChatMesh`.
 
 ### Get Owner Info
 
-TODO
+Not defined in `BaseChatMesh`.
 
 
 ## Response
 
 | Field   | Size (bytes)    | Description |
 |---------|-----------------|-------------|
-| tag     | 4               | TODO        |
-| content | rest of payload | TODO        |
+| content | rest of payload | application-defined response body |
+
+Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.
 
 ## Plain text message
 

+ 20 - 0
examples/simple_repeater/main.cpp

@@ -23,6 +23,11 @@ static char command[160];
 unsigned long lastActive = 0; // mark last active time
 unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
 
+#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
+static unsigned long userBtnDownAt = 0;
+#define USER_BTN_HOLD_OFF_MILLIS 1500
+#endif
+
 void setup() {
   Serial.begin(115200);
   delay(1000);
@@ -127,6 +132,21 @@ void loop() {
     command[0] = 0;  // reset command buffer
   }
 
+#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
+  // Hold the user button to power off the SenseCAP Solar repeater.
+  int btnState = digitalRead(PIN_USER_BTN);
+  if (btnState == LOW) {
+    if (userBtnDownAt == 0) {
+      userBtnDownAt = millis();
+    } else if ((unsigned long)(millis() - userBtnDownAt) >= USER_BTN_HOLD_OFF_MILLIS) {
+      Serial.println("Powering off...");
+      board.powerOff();  // does not return
+    }
+  } else {
+    userBtnDownAt = 0;
+  }
+#endif
+
   the_mesh.loop();
   sensors.loop();
 #ifdef DISPLAY_CLASS

+ 3 - 1
src/helpers/CommonCLI.cpp

@@ -203,7 +203,9 @@ 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) {
+    if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
+      _board->powerOff();  // doesn't return
+    } else if (memcmp(command, "reboot", 6) == 0) {
       _board->reboot();  // doesn't return
     } else if (memcmp(command, "clkreboot", 9) == 0) {
       // Reset clock

+ 50 - 2
variants/sensecap_solar/SenseCapSolarBoard.cpp

@@ -3,8 +3,43 @@
 
 #include "SenseCapSolarBoard.h"
 
+#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 SenseCapSolarBoard::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 SenseCapSolarBoard::begin() {
-  NRF52Board::begin();
+  NRF52BoardDCDC::begin();
+
+  pinMode(BATTERY_PIN, INPUT);
+  pinMode(VBAT_ENABLE, OUTPUT);
+  digitalWrite(VBAT_ENABLE, LOW);
+  analogReadResolution(12);
+  analogReference(AR_INTERNAL_3_0);
+  delay(50);
+
+#ifdef PIN_USER_BTN
+  pinMode(PIN_USER_BTN, INPUT_PULLUP);
+#elif defined(PIN_BUTTON1)
+  pinMode(PIN_BUTTON1, INPUT_PULLUP);
+#endif
 
 #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
   Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
@@ -12,10 +47,23 @@ void SenseCapSolarBoard::begin() {
 
   Wire.begin();
 
+#ifdef LED_GREEN
+  pinMode(LED_GREEN, OUTPUT);
+  digitalWrite(LED_GREEN, HIGH);
+#endif
+#ifdef LED_BLUE
+  pinMode(LED_BLUE, OUTPUT);
+  digitalWrite(LED_BLUE, LOW);
+#endif
+
 #ifdef P_LORA_TX_LED
   pinMode(P_LORA_TX_LED, OUTPUT);
   digitalWrite(P_LORA_TX_LED, LOW);
 #endif
 
+#ifdef NRF52_POWER_MANAGEMENT
+  checkBootVoltage(&power_config);
+#endif
+
   delay(10);   // give sx1262 some time to power up
-}
+}

+ 26 - 0
variants/sensecap_solar/SenseCapSolarBoard.h

@@ -5,6 +5,11 @@
 #include <helpers/NRF52Board.h>
 
 class SenseCapSolarBoard : public NRF52BoardDCDC {
+protected:
+#ifdef NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   SenseCapSolarBoard() : NRF52Board("SENSECAP_SOLAR_OTA") {}
   void begin();
@@ -31,4 +36,25 @@ public:
   const char* getManufacturerName() const override {
     return "Seeed SenseCap Solar";
   }
+
+  void powerOff() override {
+    digitalWrite(LED_GREEN, LOW);
+    digitalWrite(LED_BLUE, LOW);
+
+#ifdef PIN_USER_BTN
+    while (digitalRead(PIN_USER_BTN) == LOW);
+    // Keep pull-up enabled in system-off so the wake line doesn't float low.
+    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
+#elif defined(PIN_BUTTON1)
+    while (digitalRead(PIN_BUTTON1) == LOW);
+    // Keep pull-up enabled in system-off so the wake line doesn't float low.
+    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_BUTTON1]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
+#endif
+
+#ifdef NRF52_POWER_MANAGEMENT
+    initiateShutdown(SHUTDOWN_REASON_USER);
+#else
+    sd_power_system_off();
+#endif
+  }
 };

+ 4 - 2
variants/sensecap_solar/platformio.ini

@@ -9,13 +9,15 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/sensecap_solar
   -I src/helpers/nrf52
   -D NRF52_PLATFORM=1
+  -D NRF52_POWER_MANAGEMENT
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
-  -D P_LORA_TX_LED=12
+  -D P_LORA_TX_LED=11
   -D P_LORA_DIO_1=1
   -D P_LORA_RESET=2
   -D P_LORA_BUSY=3
   -D P_LORA_NSS=4
+  -D PIN_USER_BTN=PIN_BUTTON1
   -D LORA_TX_POWER=22
   -D SX126X_RXEN=5
   -D SX126X_TXEN=RADIOLIB_NC
@@ -96,4 +98,4 @@ build_src_filter = ${SenseCap_Solar.build_src_filter}
   +<../examples/companion_radio/*.cpp>
 lib_deps =
   ${SenseCap_Solar.lib_deps}
-  densaugeo/base64 @ ~1.4.0
+  densaugeo/base64 @ ~1.4.0

+ 7 - 1
variants/sensecap_solar/variant.h

@@ -32,6 +32,7 @@
 // Buttons
 #define PIN_BUTTON1             (13)
 #define PIN_BUTTON2             (20)
+#define PIN_USER_BTN            PIN_BUTTON1
 
 #define VBAT_ENABLE             (19)    // Output LOW to enable reading of the BAT voltage.
 
@@ -41,6 +42,11 @@
 #define ADC_MULTIPLIER          (3.0F) // 1M, 512k divider bridge
 #define ADC_RESOLUTION          (12)
 
+// nRF52 power management settings
+#define PWRMGT_VOLTAGE_BOOTLOCK (3300) // Won't boot below this voltage (mV)
+#define PWRMGT_LPCOMP_AIN       (7)    // AIN7 = P0.31 = BATTERY_PIN
+#define PWRMGT_LPCOMP_REFSEL    (2)    // 3/8 VDD (~3.38-3.71V)
+
 // Serial interfaces
 #define PIN_SERIAL1_RX          (7)
 #define PIN_SERIAL1_TX          (6)
@@ -82,4 +88,4 @@
 #define EXTERNAL_FLASH_DEVICES P25Q16H
 #define EXTERNAL_FLASH_USE_QSPI
 
-#endif
+#endif