mirror of https://github.com/meshcore-dev/MeshCore
183 changed files with 8370 additions and 2080 deletions
@ -0,0 +1,45 @@ |
|||
{ |
|||
"name": "MeshCore", |
|||
"image": "mcr.microsoft.com/devcontainers/python:3-bookworm", |
|||
"features": { |
|||
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { |
|||
"packages": [ |
|||
"sudo" |
|||
] |
|||
} |
|||
}, |
|||
"runArgs": [ |
|||
"--privileged", |
|||
"--network=host", |
|||
"--volume=/dev/bus/usb:/dev/bus/usb:ro", |
|||
// arch tty* is owned by uucp (986) |
|||
// debian tty* is owned by dialout (20) |
|||
"--group-add=20", |
|||
"--group-add=986" |
|||
], |
|||
"postCreateCommand": { |
|||
"platformio": "pipx install platformio" |
|||
}, |
|||
"customizations": { |
|||
"vscode": { |
|||
"settings": { |
|||
"platformio-ide.disablePIOHomeStartup": true, |
|||
"editor.formatOnSave": false, |
|||
"workbench.colorCustomizations": { |
|||
"titleBar.activeBackground": "#0d1a2b", |
|||
"titleBar.activeForeground": "#ffffff", |
|||
"titleBar.inactiveBackground": "#0d1a2b99", |
|||
"titleBar.inactiveForeground": "#ffffff99" |
|||
} |
|||
}, |
|||
"extensions": [ |
|||
"platformio.platformio-ide", |
|||
"github.vscode-github-actions", |
|||
"GitHub.vscode-pull-request-github" |
|||
], |
|||
"unwantedRecommendations": [ |
|||
"ms-vscode.cpptools-extension-pack" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,198 @@ |
|||
""" |
|||
Bluefruit BLE Patch Script |
|||
|
|||
Patches Bluefruit library to fix semaphore leak bug that causes device lockup |
|||
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout). |
|||
|
|||
Patches applied: |
|||
1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size |
|||
2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect |
|||
|
|||
Bug description: |
|||
- When a BLE central disconnects unexpectedly (reason=8 supervision timeout), |
|||
the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire |
|||
- This leaves the _hvn_sem counting semaphore in a decremented state |
|||
- Since BLEConnection objects are reused (destructor never called), the |
|||
semaphore count is never restored |
|||
- Eventually all semaphore counts are exhausted and notify() blocks/fails |
|||
|
|||
""" |
|||
|
|||
from pathlib import Path |
|||
|
|||
Import("env") # pylint: disable=undefined-variable |
|||
|
|||
|
|||
def _patch_ble_connection_header(source: Path) -> bool: |
|||
""" |
|||
Add _hvn_qsize member variable to BLEConnection class. |
|||
|
|||
This is needed to restore the semaphore to its correct count on disconnect. |
|||
|
|||
Returns True if patch was applied or already applied, False on error. |
|||
""" |
|||
try: |
|||
content = source.read_text() |
|||
|
|||
# Check if already patched |
|||
if "_hvn_qsize" in content: |
|||
return True # Already patched |
|||
|
|||
# Find the location to insert - after _phy declaration |
|||
original_pattern = ''' uint8_t _phy; |
|||
|
|||
uint8_t _role;''' |
|||
|
|||
patched_pattern = ''' uint8_t _phy; |
|||
uint8_t _hvn_qsize; |
|||
|
|||
uint8_t _role;''' |
|||
|
|||
if original_pattern not in content: |
|||
print("Bluefruit patch: WARNING - BLEConnection.h pattern not found") |
|||
return False |
|||
|
|||
content = content.replace(original_pattern, patched_pattern) |
|||
source.write_text(content) |
|||
|
|||
# Verify |
|||
if "_hvn_qsize" not in source.read_text(): |
|||
return False |
|||
|
|||
return True |
|||
except Exception as e: |
|||
print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}") |
|||
return False |
|||
|
|||
|
|||
def _patch_ble_connection_source(source: Path) -> bool: |
|||
""" |
|||
Patch BLEConnection.cpp to: |
|||
1. Store hvn_qsize in constructor |
|||
2. Restore _hvn_sem semaphore to full count on disconnect |
|||
|
|||
Returns True if patch was applied or already applied, False on error. |
|||
""" |
|||
try: |
|||
content = source.read_text() |
|||
|
|||
# Check if already patched (look for the restore loop) |
|||
if "uxSemaphoreGetCount(_hvn_sem)" in content: |
|||
return True # Already patched |
|||
|
|||
# Patch 1: Store queue size in constructor |
|||
constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);''' |
|||
|
|||
constructor_patched = ''' _hvn_qsize = hvn_qsize; |
|||
_hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);''' |
|||
|
|||
if constructor_original not in content: |
|||
print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found") |
|||
return False |
|||
|
|||
content = content.replace(constructor_original, constructor_patched) |
|||
|
|||
# Patch 2: Restore semaphore on disconnect |
|||
disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED: |
|||
// mark as disconnected |
|||
_connected = false; |
|||
break;''' |
|||
|
|||
disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED: |
|||
// Restore notification semaphore to full count |
|||
// This fixes lockup when disconnect occurs with notifications in flight |
|||
while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) { |
|||
xSemaphoreGive(_hvn_sem); |
|||
} |
|||
// Release indication semaphore if waiting |
|||
if (_hvc_sem) { |
|||
_hvc_received = false; |
|||
xSemaphoreGive(_hvc_sem); |
|||
} |
|||
// mark as disconnected |
|||
_connected = false; |
|||
break;''' |
|||
|
|||
if disconnect_original not in content: |
|||
print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found") |
|||
return False |
|||
|
|||
content = content.replace(disconnect_original, disconnect_patched) |
|||
source.write_text(content) |
|||
|
|||
# Verify |
|||
verify_content = source.read_text() |
|||
if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content: |
|||
return False |
|||
if "_hvn_qsize = hvn_qsize" not in verify_content: |
|||
return False |
|||
|
|||
return True |
|||
except Exception as e: |
|||
print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}") |
|||
return False |
|||
|
|||
|
|||
def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument |
|||
framework_path = env.get("PLATFORMFW_DIR") |
|||
if not framework_path: |
|||
framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52") |
|||
|
|||
if not framework_path: |
|||
print("Bluefruit patch: ERROR - framework directory not found") |
|||
env.Exit(1) |
|||
return |
|||
|
|||
framework_dir = Path(framework_path) |
|||
bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src" |
|||
patch_failed = False |
|||
|
|||
# Patch BLEConnection.h |
|||
conn_header = bluefruit_lib / "BLEConnection.h" |
|||
if conn_header.exists(): |
|||
before = conn_header.read_text() |
|||
success = _patch_ble_connection_header(conn_header) |
|||
after = conn_header.read_text() |
|||
|
|||
if success: |
|||
if before != after: |
|||
print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)") |
|||
else: |
|||
print("Bluefruit patch: OK - BLEConnection.h already patched") |
|||
else: |
|||
print("Bluefruit patch: FAILED - BLEConnection.h") |
|||
patch_failed = True |
|||
else: |
|||
print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}") |
|||
patch_failed = True |
|||
|
|||
# Patch BLEConnection.cpp |
|||
conn_source = bluefruit_lib / "BLEConnection.cpp" |
|||
if conn_source.exists(): |
|||
before = conn_source.read_text() |
|||
success = _patch_ble_connection_source(conn_source) |
|||
after = conn_source.read_text() |
|||
|
|||
if success: |
|||
if before != after: |
|||
print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)") |
|||
else: |
|||
print("Bluefruit patch: OK - BLEConnection.cpp already patched") |
|||
else: |
|||
print("Bluefruit patch: FAILED - BLEConnection.cpp") |
|||
patch_failed = True |
|||
else: |
|||
print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}") |
|||
patch_failed = True |
|||
|
|||
if patch_failed: |
|||
print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.") |
|||
env.Exit(1) |
|||
|
|||
|
|||
# Register the patch to run before build |
|||
bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...") |
|||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action) |
|||
|
|||
# Also run immediately to patch before any compilation |
|||
_apply_bluefruit_patches(None, None, env) |
|||
@ -0,0 +1,40 @@ |
|||
{ |
|||
"build": { |
|||
"arduino": { |
|||
"ldscript": "esp32s3_out.ld" |
|||
}, |
|||
"core": "esp32", |
|||
"extra_flags": [ |
|||
"-D ARDUINO_USB_CDC_ON_BOOT=1", |
|||
"-D ARDUINO_USB_MSC_ON_BOOT=0", |
|||
"-D ARDUINO_USB_DFU_ON_BOOT=0", |
|||
"-D ARDUINO_USB_MODE=1", |
|||
"-D ARDUINO_RUNNING_CORE=1", |
|||
"-D ARDUINO_EVENT_RUNNING_CORE=1" |
|||
], |
|||
"f_cpu": "240000000L", |
|||
"f_flash": "80000000L", |
|||
"flash_mode": "qio", |
|||
"hwids": [["0x303A", "0x1001"]], |
|||
"mcu": "esp32s3", |
|||
"variant": "esp32s3" |
|||
}, |
|||
"connectivity": ["wifi", "bluetooth"], |
|||
"debug": { |
|||
"default_tool": "esp-builtin", |
|||
"onboard_tools": ["esp-builtin"], |
|||
"openocd_target": "esp32s3.cfg" |
|||
}, |
|||
"frameworks": ["arduino", "espidf"], |
|||
"name": "ESP32-S3-Zero", |
|||
"upload": { |
|||
"flash_size": "4MB", |
|||
"maximum_ram_size": 327680, |
|||
"maximum_size": 4194304, |
|||
"require_upload_port": true, |
|||
"speed": 921600 |
|||
}, |
|||
"url": "https://www.espressif.com", |
|||
"vendor": "Espressif" |
|||
} |
|||
|
|||
@ -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" |
|||
} |
|||
@ -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" |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
{ |
|||
"build": { |
|||
"arduino": { |
|||
"ldscript": "nrf52840_s140_v6.ld" |
|||
}, |
|||
"core": "nRF5", |
|||
"cpu": "cortex-m4", |
|||
"extra_flags": "-DNRF52840_XXAA", |
|||
"f_cpu": "64000000L", |
|||
"hwids": [ |
|||
[ |
|||
"0x239A", |
|||
"0x4405" |
|||
], |
|||
[ |
|||
"0x239A", |
|||
"0x0029" |
|||
], |
|||
[ |
|||
"0x239A", |
|||
"0x002A" |
|||
] |
|||
], |
|||
"usb_product": "elecrow_eink", |
|||
"mcu": "nrf52840", |
|||
"variant": "ELECROW-ThinkNode-M3", |
|||
"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", |
|||
"onboard_tools": [ |
|||
"jlink" |
|||
], |
|||
"svd_path": "nrf52840.svd", |
|||
"openocd_target": "nrf52.cfg" |
|||
}, |
|||
"frameworks": [ |
|||
"arduino" |
|||
], |
|||
"name": "elecrow nrf", |
|||
"upload": { |
|||
"maximum_ram_size": 248832, |
|||
"maximum_size": 815104, |
|||
"speed": 115200, |
|||
"use_1200bps_touch": true, |
|||
"require_upload_port": true, |
|||
"wait_for_upload_port": true, |
|||
"protocol": "nrfutil", |
|||
"protocols": [ |
|||
"jlink", |
|||
"nrfjprog", |
|||
"nrfutil", |
|||
"stlink" |
|||
] |
|||
}, |
|||
"url": "https://github.com/Elecrow-RD", |
|||
"vendor": "ELECROW" |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
{ |
|||
"build": { |
|||
"arduino": { |
|||
"ldscript": "nrf52840_s140_v6.ld" |
|||
}, |
|||
"core": "nRF5", |
|||
"cpu": "cortex-m4", |
|||
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M6 -DNRF52840_XXAA", |
|||
"f_cpu": "64000000L", |
|||
"hwids": [ |
|||
[ |
|||
"0x239A", |
|||
"0x4405" |
|||
], |
|||
[ |
|||
"0x239A", |
|||
"0x0029" |
|||
], |
|||
[ |
|||
"0x239A", |
|||
"0x002A" |
|||
] |
|||
], |
|||
"usb_product": "elecrow_solar", |
|||
"mcu": "nrf52840", |
|||
"variant": "ELECROW-ThinkNode-M6", |
|||
"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", |
|||
"onboard_tools": [ |
|||
"jlink" |
|||
], |
|||
"svd_path": "nrf52840.svd", |
|||
"openocd_target": "nrf52.cfg" |
|||
}, |
|||
"frameworks": [ |
|||
"arduino" |
|||
], |
|||
"name": "elecrow solar", |
|||
"upload": { |
|||
"maximum_ram_size": 248832, |
|||
"maximum_size": 815104, |
|||
"speed": 115200, |
|||
"use_1200bps_touch": true, |
|||
"require_upload_port": true, |
|||
"wait_for_upload_port": true, |
|||
"protocol": "nrfutil", |
|||
"protocols": [ |
|||
"jlink", |
|||
"nrfjprog", |
|||
"nrfutil", |
|||
"stlink" |
|||
] |
|||
}, |
|||
"url": "https://github.com/Elecrow-RD", |
|||
"vendor": "ELECROW" |
|||
} |
|||
@ -0,0 +1,881 @@ |
|||
# MeshCore Repeater & Room Server CLI Commands |
|||
|
|||
## Navigation |
|||
|
|||
- [Operational](#operational) |
|||
- [Neighbors](#neighbors-repeater-only) |
|||
- [Statistics](#statistics) |
|||
- [Logging](#logging) |
|||
- [Information](#info) |
|||
- [Configuration](#configuration) |
|||
- [Radio](#radio) |
|||
- [System](#system) |
|||
- [Routing](#routing) |
|||
- [ACL](#acl) |
|||
- [Region Management](#region-management-v110) |
|||
- [Region Examples](#region-examples) |
|||
- [GPS](#gps-when-gps-support-is-compiled-in) |
|||
- [Sensors](#sensors-when-sensor-support-is-compiled-in) |
|||
- [Bridge](#bridge-when-bridge-support-is-compiled-in) |
|||
|
|||
--- |
|||
|
|||
## Operational |
|||
|
|||
### Reboot the node |
|||
**Usage:** |
|||
- `reboot` |
|||
|
|||
--- |
|||
|
|||
### Reset the clock and reboot |
|||
**Usage:** |
|||
- `clkreboot` |
|||
|
|||
--- |
|||
|
|||
### Sync the clock with the remote device |
|||
**Usage:** |
|||
- `clock sync` |
|||
|
|||
--- |
|||
|
|||
### Display current time in UTC |
|||
**Usage:** |
|||
- `clock` |
|||
|
|||
--- |
|||
|
|||
### Set the time to a specific timestamp |
|||
**Usage:** |
|||
- `time <epoch_seconds>` |
|||
|
|||
**Parameters:** |
|||
- `epoc_seconds`: Unix epoc time |
|||
|
|||
--- |
|||
|
|||
### Send a flood advert |
|||
**Usage:** |
|||
- `advert` |
|||
|
|||
--- |
|||
|
|||
### Start an Over-The-Air (OTA) firmware update |
|||
**Usage:** |
|||
- `start ota` |
|||
|
|||
--- |
|||
|
|||
### Erase/Factory Reset |
|||
**Usage:** |
|||
- `erase` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
**Warning:** _**This is destructive!**_ |
|||
|
|||
--- |
|||
|
|||
## Neighbors (Repeater Only) |
|||
|
|||
### List nearby neighbors |
|||
**Usage:** |
|||
- `neighbors` |
|||
|
|||
**Note:** The output of this command is limited to the 8 most recent adverts. |
|||
|
|||
**Note:** Each line is encoded as `{pubkey-prefix}:{timestamp}:{snr*4}` |
|||
|
|||
--- |
|||
|
|||
### Remove a neighbor |
|||
**Usage:** |
|||
- `neighbor.remove <pubkey_prefix>` |
|||
|
|||
**Parameters:** |
|||
- `pubkey_prefix`: The public key of the node to remove from the neighbors list |
|||
|
|||
--- |
|||
|
|||
## Statistics |
|||
|
|||
### Clear Stats |
|||
**Usage:** `clear stats` |
|||
|
|||
--- |
|||
|
|||
### System Stats - Battery, Uptime, Queue Length and Debug Flags |
|||
**Usage:** |
|||
- `stats-core` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
### Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors |
|||
**Usage:** `stats-radio` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
### Packet stats - Packet counters: Received, Sent |
|||
**Usage:** `stats-packets` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
## Logging |
|||
|
|||
### Begin capture of rx log to node storage |
|||
**Usage:** `log start` |
|||
|
|||
--- |
|||
|
|||
### End capture of rx log to node sotrage |
|||
**Usage:** `log stop` |
|||
|
|||
--- |
|||
|
|||
### Erase captured log |
|||
**Usage:** `log erase` |
|||
|
|||
--- |
|||
|
|||
### Print the captured log to the serial terminal |
|||
**Usage:** `log` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
## Info |
|||
|
|||
### Get the Version |
|||
**Usage:** `ver` |
|||
|
|||
--- |
|||
|
|||
### Show the hardware name |
|||
**Usage:** `board` |
|||
|
|||
--- |
|||
|
|||
## Configuration |
|||
|
|||
### Radio |
|||
|
|||
#### View or change this node's radio parameters |
|||
**Usage:** |
|||
- `get radio` |
|||
- `set radio <freq>,<bw>,<sf>,<cr>` |
|||
|
|||
**Parameters:** |
|||
- `freq`: Frequency in MHz |
|||
- `bw`: Bandwidth in kHz |
|||
- `sf`: Spreading factor (5-12) |
|||
- `cr`: Coding rate (5-8) |
|||
|
|||
**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` |
|||
|
|||
**Default:** `869.525,250,11,5` |
|||
|
|||
**Note:** Requires reboot to apply |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's transmit power |
|||
**Usage:** |
|||
- `get tx` |
|||
- `set tx <dbm>` |
|||
|
|||
**Parameters:** |
|||
- `dbm`: Power level in dBm (1-22) |
|||
|
|||
**Set by build flag:** `LORA_TX_POWER` |
|||
|
|||
**Default:** Varies by board |
|||
|
|||
**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Referr to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.** |
|||
|
|||
--- |
|||
|
|||
#### Change the radio parameters for a set duration |
|||
**Usage:** |
|||
- `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>` |
|||
|
|||
**Parameters:** |
|||
- `freq`: Frequency in MHz (300-2500) |
|||
- `bw`: Bandwidth in kHz (7.8-500) |
|||
- `sf`: Spreading factor (5-12) |
|||
- `cr`: Coding rate (5-8) |
|||
- `timeout_mins`: Duration in minutes (must be > 0) |
|||
|
|||
**Note:** This is not saved to preferences and will clear on reboot |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's frequency |
|||
**Usage:** |
|||
- `get freq` |
|||
- `set freq <frequency>` |
|||
|
|||
**Parameters:** |
|||
- `frequency`: Frequency in MHz |
|||
|
|||
**Default:** `869.525` |
|||
|
|||
**Note:** Requires reboot to apply |
|||
|
|||
### System |
|||
|
|||
#### View or change this node's name |
|||
**Usage:** |
|||
- `get name` |
|||
- `set name <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Node name |
|||
|
|||
**Set by build flag:** `ADVERT_NAME` |
|||
|
|||
**Default:** Varies by board |
|||
|
|||
**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte. |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's latitude |
|||
**Usage:** |
|||
- `get lat` |
|||
- `set lat <degrees>` |
|||
|
|||
**Set by build flag:** `ADVERT_LAT` |
|||
|
|||
**Default:** `0` |
|||
|
|||
**Parameters:** |
|||
- `degrees`: Latitude in degrees |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's longitude |
|||
**Usage:** |
|||
- `get lon` |
|||
- `set lon <degrees>` |
|||
|
|||
**Set by build flag:** `ADVERT_LON` |
|||
|
|||
**Default:** `0` |
|||
|
|||
**Parameters:** |
|||
- `degrees`: Longitude in degrees |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's identity (Private Key) |
|||
**Usage:** |
|||
- `get prv.key` |
|||
- `set prv.key <private_key>` |
|||
|
|||
**Parameters:** |
|||
- `private_key`: Private key in hex format (64 hex characters) |
|||
|
|||
**Serial Only:** |
|||
- `get prv.key`: Yes |
|||
- `set prv.key`: No |
|||
|
|||
**Note:** Requires reboot to take effect after setting |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's admin password |
|||
**Usage:** |
|||
- `get password` |
|||
- `set password <password>` |
|||
|
|||
**Parameters:** |
|||
- `password`: Admin password |
|||
|
|||
**Set by build flag:** `ADMIN_PASSWORD` |
|||
|
|||
**Default:** `password` |
|||
|
|||
**Note:** Echoed back for confirmation |
|||
|
|||
**Note:** Any node using this password will be added to the admin ACL list. |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's guest password |
|||
**Usage:** |
|||
- `get guest.password` |
|||
- `set guest.password <password>` |
|||
|
|||
**Parameters:** |
|||
- `password`: Guest password |
|||
|
|||
**Set by build flag:** `ROOM_PASSWORD` (Room Server only) |
|||
|
|||
**Default:** `<blank>` |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's owner info |
|||
**Usage:** |
|||
- `get owner.info` |
|||
- `set owner.info <text>` |
|||
|
|||
**Parameters:** |
|||
- `text`: Owner information text |
|||
|
|||
**Default:** `<blank>` |
|||
|
|||
**Note:** `|` characters are translated to newlines |
|||
|
|||
**Note:** Requires firmware 1.12.+ |
|||
|
|||
--- |
|||
|
|||
#### Fine-tune the battery reading |
|||
**Usage:** |
|||
- `get adc.multiplier` |
|||
- `set adc.multiplier <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: ADC multiplier (0.0-10.0) |
|||
|
|||
**Default:** `0.0` (value defined by board) |
|||
|
|||
**Note:** Returns "Error: unsupported by this board" if hardware doesn't support it |
|||
|
|||
--- |
|||
|
|||
#### View or change this node's power saving flag (Repeater Only) |
|||
**Usage:** |
|||
- `powersaving <state>` |
|||
- `powersaving` |
|||
|
|||
**Parameters:** |
|||
- `state`: `on`|`off` |
|||
|
|||
**Default:** `on` |
|||
|
|||
**Note:** When enabled, device enters sleep mode between radio transmissions |
|||
|
|||
--- |
|||
|
|||
### Routing |
|||
|
|||
#### View or change this node's repeat flag |
|||
**Usage:** |
|||
- `get repeat` |
|||
- `set repeat <state>` |
|||
|
|||
**Parameters:** |
|||
- `state`: `on`|`off` |
|||
|
|||
**Default:** `on` |
|||
|
|||
--- |
|||
|
|||
#### View or change the retransmit delay factor for flood traffic |
|||
**Usage:** |
|||
- `get txdelay` |
|||
- `set txdelay <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Transmit delay factor (0-2) |
|||
|
|||
**Default:** `0.5` |
|||
|
|||
--- |
|||
|
|||
#### View or change the retransmit delay factor for direct traffic |
|||
**Usage:** |
|||
- `get direct.txdelay` |
|||
- `set direct.txdelay <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Direct transmit delay factor (0-2) |
|||
|
|||
**Default:** `0.2` |
|||
|
|||
--- |
|||
|
|||
#### [Experimental] View or change the processing delay for received traffic |
|||
**Usage:** |
|||
- `get rxdelay` |
|||
- `set rxdelay <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Receive delay base (0-20) |
|||
|
|||
**Default:** `0.0` |
|||
|
|||
--- |
|||
|
|||
#### View or change the airtime factor (duty cycle limit) |
|||
**Usage:** |
|||
- `get af` |
|||
- `set af <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Airtime factor (0-9) |
|||
|
|||
**Default:** `1.0` |
|||
|
|||
--- |
|||
|
|||
#### View or change the local interference threshold |
|||
**Usage:** |
|||
- `get int.thresh` |
|||
- `set int.thresh <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Interference threshold value |
|||
|
|||
**Default:** `0.0` |
|||
|
|||
--- |
|||
|
|||
#### View or change the AGC Reset Interval |
|||
**Usage:** |
|||
- `get agc.reset.interval` |
|||
- `set agc.reset.interval <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16) |
|||
|
|||
**Default:** `0.0` |
|||
|
|||
--- |
|||
|
|||
#### Enable or disable Multi-Acks support |
|||
**Usage:** |
|||
- `get multi.acks` |
|||
- `set multi.acks <state>` |
|||
|
|||
**Parameters:** |
|||
- `state`: `0` (disable) or `1` (enable) |
|||
|
|||
**Default:** `0` |
|||
|
|||
--- |
|||
|
|||
#### View or change the flood advert interval |
|||
**Usage:** |
|||
- `get flood.advert.interval` |
|||
- `set flood.advert.interval <hours>` |
|||
|
|||
**Parameters:** |
|||
- `hours`: Interval in hours (3-168) |
|||
|
|||
**Default:** `12` (Repeater) - `0` (Sensor) |
|||
|
|||
--- |
|||
|
|||
#### View or change the zero-hop advert interval |
|||
**Usage:** |
|||
- `get advert.interval` |
|||
- `set advert.interval <minutes>` |
|||
|
|||
**Parameters:** |
|||
- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240) |
|||
|
|||
**Default:** `0` |
|||
|
|||
--- |
|||
|
|||
#### Limit the number of hops for a flood message |
|||
**Usage:** |
|||
- `get flood.max` |
|||
- `set flood.max <value>` |
|||
|
|||
**Parameters:** |
|||
- `value`: Maximum flood hop count (0-64) |
|||
|
|||
**Default:** `64` |
|||
|
|||
--- |
|||
|
|||
### ACL |
|||
|
|||
#### Add, update or remove permissions for a companion |
|||
**Usage:** |
|||
- `setperm <pubkey> <permissions>` |
|||
|
|||
**Parameters:** |
|||
- `pubkey`: Companion public key |
|||
- `permissions`: |
|||
- `0`: Guest |
|||
- `1`: Read-only |
|||
- `2`: Read-write |
|||
- `3`: Admin |
|||
|
|||
**Note:** Removes the entry when `permissions` is omitted |
|||
|
|||
--- |
|||
|
|||
#### View the current ACL |
|||
**Usage:** |
|||
- `get acl` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
#### View or change this room server's 'read-only' flag |
|||
**Usage:** |
|||
- `get allow.read.only` |
|||
- `set allow.read.only <state>` |
|||
|
|||
**Parameters:** |
|||
- `state`: `on` (enable) or `off` (disable) |
|||
|
|||
**Default:** `off` |
|||
|
|||
--- |
|||
|
|||
### Region Management (v1.10.+) |
|||
|
|||
#### Bulk-load region lists |
|||
**Usage:** |
|||
- `region load` |
|||
- `region load <name> [flood_flag]` |
|||
|
|||
**Parameters:** |
|||
- `name`: A name of a region. `*` represents the wildcard region |
|||
|
|||
**Note:** `flood_flag`: Optional `F` to allow flooding |
|||
|
|||
**Note:** Indentation creates parent-child relationships (max 8 levels) |
|||
|
|||
**Note:** `region load` with an empty name will not work remotely (it's interactive) |
|||
|
|||
--- |
|||
|
|||
#### Save any changes to regions made since reboot |
|||
**Usage:** |
|||
- `region save` |
|||
|
|||
--- |
|||
|
|||
#### Allow a region |
|||
**Usage:** |
|||
- `region allowf <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name (or `*` for wildcard) |
|||
|
|||
**Note:** Setting on wildcard `*` allows packets without region transport codes |
|||
|
|||
--- |
|||
|
|||
#### Block a region |
|||
**Usage:** |
|||
- `region denyf <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name (or `*` for wildcard) |
|||
|
|||
**Note:** Setting on wildcard `*` drops packets without region transport codes |
|||
|
|||
--- |
|||
|
|||
#### Show information for a region |
|||
**Usage:** |
|||
- `region get <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name (or `*` for wildcard) |
|||
|
|||
--- |
|||
|
|||
#### View or change the home region for this node |
|||
**Usage:** |
|||
- `region home` |
|||
- `region home <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name |
|||
|
|||
--- |
|||
|
|||
#### Create a new region |
|||
**Usage:** |
|||
- `region put <name> [parent_name]` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name |
|||
- `parent_name`: Parent region name (optional, defaults to wildcard) |
|||
|
|||
--- |
|||
|
|||
#### Remove a region |
|||
**Usage:** |
|||
- `region remove <name>` |
|||
|
|||
**Parameters:** |
|||
- `name`: Region name |
|||
|
|||
**Note:** Must remove all child regions before the region can be removed |
|||
|
|||
--- |
|||
|
|||
#### View all regions |
|||
**Usage:** |
|||
- `region list <filter>` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
**Parameters:** |
|||
- `filter`: `allowed`|`denied` |
|||
|
|||
**Note:** Requires firmware 1.12.+ |
|||
|
|||
--- |
|||
|
|||
#### Dump all defined regions and flood permissions |
|||
**Usage:** |
|||
- `region` |
|||
|
|||
**Serial Only:** Yes |
|||
|
|||
--- |
|||
|
|||
### Region Examples |
|||
|
|||
**Example 1: Using F Flag with Named Public Region** |
|||
``` |
|||
region load |
|||
#Europe F |
|||
<blank line to end region load> |
|||
region save |
|||
``` |
|||
|
|||
**Explanation:** |
|||
- Creates a region named `#Europe` with flooding enabled |
|||
- Packets from this region will be flooded to other nodes |
|||
|
|||
--- |
|||
|
|||
**Example 2: Using Wildcard with F Flag** |
|||
``` |
|||
region load |
|||
* F |
|||
<blank line to end region load> |
|||
region save |
|||
``` |
|||
|
|||
**Explanation:** |
|||
- Creates a wildcard region `*` with flooding enabled |
|||
- Enables flooding for all regions automatically |
|||
- Applies only to packets without transport codes |
|||
|
|||
--- |
|||
|
|||
**Example 3: Using Wildcard Without F Flag** |
|||
``` |
|||
region load |
|||
* |
|||
<blank line to end region load> |
|||
region save |
|||
``` |
|||
**Explanation:** |
|||
- Creates a wildcard region `*` without flooding |
|||
- This region exists but doesn't affect packet distribution |
|||
- Used as a default/empty region |
|||
|
|||
--- |
|||
|
|||
**Example 4: Nested Public Region with F Flag** |
|||
``` |
|||
region load |
|||
#Europe F |
|||
#UK |
|||
#London |
|||
#Manchester |
|||
#France |
|||
#Paris |
|||
#Lyon |
|||
<blank line to end region load> |
|||
region save |
|||
``` |
|||
|
|||
**Explanation:** |
|||
- Creates `#Europe` region with flooding enabled |
|||
- Adds nested child regions (`#UK`, `#France`) |
|||
- All nested regions inherit the flooding flag from parent |
|||
|
|||
--- |
|||
|
|||
**Example 5: Wildcard with Nested Public Regions** |
|||
``` |
|||
region load |
|||
* F |
|||
#NorthAmerica |
|||
#USA |
|||
#NewYork |
|||
#California |
|||
#Canada |
|||
#Ontario |
|||
#Quebec |
|||
<blank line to end region load> |
|||
region save |
|||
``` |
|||
|
|||
**Explanation:** |
|||
- Creates wildcard region `*` with flooding enabled |
|||
- Adds nested `#NorthAmerica` hierarchy |
|||
- Enables flooding for all child regions automatically |
|||
- Useful for global networks with specific regional rules |
|||
|
|||
--- |
|||
### GPS (When GPS support is compiled in) |
|||
|
|||
#### View or change GPS state |
|||
**Usage:** |
|||
- `gps` |
|||
- `gps <state>` |
|||
|
|||
**Parameters:** |
|||
- `state`: `on`|`off` |
|||
|
|||
**Default:** `off` |
|||
|
|||
**Note:** Output format: `{status}, {fix}, {sat count}` (when enabled) |
|||
|
|||
--- |
|||
|
|||
#### Sync this node's clock with GPS time |
|||
**Usage:** |
|||
- `gps sync` |
|||
|
|||
--- |
|||
|
|||
#### Set this node's location based on the GPS coordinates |
|||
**Usage:** |
|||
- `gps setloc` |
|||
|
|||
--- |
|||
|
|||
#### View or change the GPS advert policy |
|||
**Usage:** |
|||
- `gps advert` |
|||
- `gps advert <policy>` |
|||
|
|||
**Parameters:** |
|||
- `policy`: `none`|`shared`|`prefs` |
|||
- `none`: don't include location in adverts |
|||
- `share`: share gps location (from SensorManager) |
|||
- `prefs`: location stored in node's lat and lon settings |
|||
|
|||
**Default:** `prefs` |
|||
|
|||
--- |
|||
|
|||
### Sensors (When sensor support is compiled in) |
|||
|
|||
#### View the list of sensors on this node |
|||
**Usage:** `sensor list [start]` |
|||
|
|||
**Parameters:** |
|||
- `start`: Optional starting index (defaults to 0) |
|||
|
|||
**Note:** Output format: `<var_name>=<value>\n` |
|||
|
|||
--- |
|||
|
|||
#### View or change thevalue of a sensor |
|||
**Usage:** |
|||
- `sensor get <key>` |
|||
- `sensor set <key> <value>` |
|||
|
|||
**Parameters:** |
|||
- `key`: Sensor setting name |
|||
- `value`: The value to set the sensor to |
|||
|
|||
--- |
|||
|
|||
### Bridge (When bridge support is compiled in) |
|||
|
|||
#### View or change the bridge enabled flag |
|||
**Usage:** |
|||
- `get bridge.enabled` |
|||
- `set bridge.enabled <state>` |
|||
|
|||
**Parameters:** |
|||
- `state`: `on`|`off` |
|||
|
|||
**Default:** `off` |
|||
|
|||
--- |
|||
|
|||
#### View the bridge source |
|||
**Usage:** |
|||
- `get bridge.source` |
|||
|
|||
--- |
|||
|
|||
#### Add a delay to packets routed through this bridge |
|||
**Usage:** |
|||
- `get bridge.delay` |
|||
- `set bridge.delay <ms>` |
|||
|
|||
**Parameters:** |
|||
- `ms`: Delay in milliseconds (0-10000) |
|||
|
|||
**Default:** `500` |
|||
|
|||
--- |
|||
|
|||
#### View or change the source of packets bridged to the external interface |
|||
**Usage:** |
|||
- `get bridge.source` |
|||
- `set bridge.source <source>` |
|||
|
|||
**Parameters:** |
|||
- `source`: |
|||
- `rx`: bridges received packets |
|||
- `tx`: bridges transmitted packets |
|||
|
|||
**Default:** `tx` |
|||
|
|||
--- |
|||
|
|||
#### View or change the speed of the bridge (RS-232 only) |
|||
**Usage:** |
|||
- `get bridge.baud` |
|||
- `set bridge.baud <rate>` |
|||
|
|||
**Parameters:** |
|||
- `rate`: Baud rate (`9600`, `19200`, `38400`, `57600`, or `115200`) |
|||
|
|||
**Default:** `115200` |
|||
|
|||
--- |
|||
|
|||
#### View or change the channel used for bridging (ESPNow only) |
|||
**Usage:** |
|||
- `get bridge.channel` |
|||
- `set bridge.channel <channel>` |
|||
|
|||
**Parameters:** |
|||
- `channel`: Channel number (1-14) |
|||
|
|||
--- |
|||
|
|||
#### Set the ESP-Now secret |
|||
**Usage:** |
|||
- `get bridge.secret` |
|||
- `set bridge.secret <secret>` |
|||
|
|||
**Parameters:** |
|||
- `secret`: 16-character encryption secret |
|||
|
|||
**Default:** Varies by board |
|||
|
|||
--- |
|||
@ -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) |
|||
File diff suppressed because it is too large
@ -0,0 +1,321 @@ |
|||
#if defined(NRF52_PLATFORM) |
|||
#include "NRF52Board.h" |
|||
|
|||
#include <bluefruit.h> |
|||
#include <nrf_soc.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 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(); |
|||
|
|||
// Enable DC/DC converter for improved power efficiency
|
|||
uint8_t sd_enabled = 0; |
|||
sd_softdevice_is_enabled(&sd_enabled); |
|||
if (sd_enabled) { |
|||
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE); |
|||
} else { |
|||
NRF_POWER->DCDCEN = 1; |
|||
} |
|||
} |
|||
|
|||
// Temperature from NRF52 MCU
|
|||
float NRF52Board::getMCUTemperature() { |
|||
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
|
|||
|
|||
long startTime = millis(); |
|||
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
|
|||
if(millis() - startTime > 5) { // To wait 5ms just in case
|
|||
NRF_TEMP->TASKS_STOP = 1; |
|||
return NAN; |
|||
} |
|||
} |
|||
|
|||
NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
|
|||
|
|||
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
|
|||
NRF_TEMP->TASKS_STOP = 1; |
|||
|
|||
return temp * 0.25f; // Convert to *C
|
|||
} |
|||
|
|||
bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { |
|||
// Config the peripheral connection with maximum bandwidth
|
|||
// more SRAM required by SoftDevice
|
|||
// Note: All config***() function must be called before begin()
|
|||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); |
|||
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); |
|||
|
|||
Bluefruit.begin(1, 0); |
|||
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
|
|||
Bluefruit.setTxPower(4); |
|||
// Set the BLE device name
|
|||
Bluefruit.setName(ota_name); |
|||
|
|||
Bluefruit.Periph.setConnectCallback(connect_callback); |
|||
Bluefruit.Periph.setDisconnectCallback(disconnect_callback); |
|||
|
|||
// To be consistent OTA DFU should be added first if it exists
|
|||
bledfu.begin(); |
|||
|
|||
// Set up and start advertising
|
|||
// Advertising packet
|
|||
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); |
|||
Bluefruit.Advertising.addTxPower(); |
|||
Bluefruit.Advertising.addName(); |
|||
|
|||
/* Start Advertising
|
|||
- Enable auto advertising if disconnected |
|||
- Interval: fast mode = 20 ms, slow mode = 152.5 ms |
|||
- Timeout for fast mode is 30 seconds |
|||
- Start(timeout) with timeout = 0 will advertise forever (until connected) |
|||
|
|||
For recommended advertising interval |
|||
https://developer.apple.com/library/content/qa/qa1931/_index.html
|
|||
*/ |
|||
Bluefruit.Advertising.restartOnDisconnect(true); |
|||
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
|
|||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
|||
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
|
|||
|
|||
uint8_t mac_addr[6]; |
|||
memset(mac_addr, 0, sizeof(mac_addr)); |
|||
Bluefruit.getAddr(mac_addr); |
|||
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3], |
|||
mac_addr[2], mac_addr[1], mac_addr[0]); |
|||
|
|||
return true; |
|||
} |
|||
#endif |
|||
@ -0,0 +1,77 @@ |
|||
#pragma once |
|||
|
|||
#include <Arduino.h> |
|||
#include <MeshCore.h> |
|||
|
|||
#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(); |
|||
virtual uint8_t getStartupReason() const override { return startup_reason; } |
|||
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 |
|||
}; |
|||
|
|||
/*
|
|||
* The NRF52 has an internal DC/DC regulator that allows increased efficiency |
|||
* compared to the LDO regulator. For being able to use it, the module/board |
|||
* needs to have the required inductors and and capacitors populated. If the |
|||
* hardware requirements are met, this subclass can be used to enable the DC/DC |
|||
* regulator. |
|||
*/ |
|||
class NRF52BoardDCDC : virtual public NRF52Board { |
|||
public: |
|||
NRF52BoardDCDC() {} |
|||
virtual void begin() override; |
|||
}; |
|||
#endif |
|||
@ -1,193 +1,397 @@ |
|||
#include "SerialBLEInterface.h" |
|||
#include <stdio.h> |
|||
#include <string.h> |
|||
#include "ble_gap.h" |
|||
#include "ble_hci.h" |
|||
|
|||
static SerialBLEInterface* instance; |
|||
// Magic numbers came from actual testing
|
|||
#define BLE_HEALTH_CHECK_INTERVAL 10000 // Advertising watchdog check every 10 seconds
|
|||
#define BLE_RETRY_THROTTLE_MS 250 // Throttle retries to 250ms when queue buildup detected
|
|||
|
|||
// Connection parameters (units: interval=1.25ms, timeout=10ms)
|
|||
#define BLE_MIN_CONN_INTERVAL 12 // 15ms
|
|||
#define BLE_MAX_CONN_INTERVAL 24 // 30ms
|
|||
#define BLE_SLAVE_LATENCY 4 |
|||
#define BLE_CONN_SUP_TIMEOUT 200 // 2000ms
|
|||
|
|||
// Advertising parameters
|
|||
#define BLE_ADV_INTERVAL_MIN 32 // 20ms (units: 0.625ms)
|
|||
#define BLE_ADV_INTERVAL_MAX 244 // 152.5ms (units: 0.625ms)
|
|||
#define BLE_ADV_FAST_TIMEOUT 30 // seconds
|
|||
|
|||
// RX drain buffer size for overflow protection
|
|||
#define BLE_RX_DRAIN_BUF_SIZE 32 |
|||
|
|||
static SerialBLEInterface* instance = nullptr; |
|||
|
|||
void SerialBLEInterface::onConnect(uint16_t connection_handle) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected"); |
|||
// we now set _isDeviceConnected=true in onSecured callback instead
|
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle); |
|||
if (instance) { |
|||
instance->_conn_handle = connection_handle; |
|||
instance->_isDeviceConnected = false; |
|||
instance->clearBuffers(); |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason); |
|||
if(instance){ |
|||
instance->_isDeviceConnected = false; |
|||
instance->startAdv(); |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason); |
|||
if (instance) { |
|||
if (instance->_conn_handle == connection_handle) { |
|||
instance->_conn_handle = BLE_CONN_HANDLE_INVALID; |
|||
instance->_isDeviceConnected = false; |
|||
instance->clearBuffers(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::onSecured(uint16_t connection_handle) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured"); |
|||
if(instance){ |
|||
instance->_isDeviceConnected = true; |
|||
// no need to stop advertising on connect, as the ble stack does this automatically
|
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle); |
|||
if (instance) { |
|||
if (instance->isValidConnection(connection_handle, true)) { |
|||
instance->_isDeviceConnected = true; |
|||
|
|||
// Connection interval units: 1.25ms, supervision timeout units: 10ms
|
|||
// Apple: "The product will not read or use the parameters in the Peripheral Preferred Connection Parameters characteristic."
|
|||
// So we explicitly set it here to make Android & Apple match
|
|||
ble_gap_conn_params_t conn_params; |
|||
conn_params.min_conn_interval = BLE_MIN_CONN_INTERVAL; |
|||
conn_params.max_conn_interval = BLE_MAX_CONN_INTERVAL; |
|||
conn_params.slave_latency = BLE_SLAVE_LATENCY; |
|||
conn_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT; |
|||
|
|||
uint32_t err_code = sd_ble_gap_conn_param_update(connection_handle, &conn_params); |
|||
if (err_code == NRF_SUCCESS) { |
|||
BLE_DEBUG_PRINTLN("Connection parameter update requested: %u-%ums interval, latency=%u, %ums timeout", |
|||
conn_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
|
|||
conn_params.max_conn_interval * 5 / 4, |
|||
conn_params.slave_latency, |
|||
conn_params.conn_sup_timeout * 10); // convert to ms (10ms units)
|
|||
} else { |
|||
BLE_DEBUG_PRINTLN("Failed to request connection parameter update: %lu", err_code); |
|||
} |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("onSecured: ignoring stale/duplicate callback"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { |
|||
bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) { |
|||
(void)connection_handle; |
|||
(void)passkey; |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request); |
|||
return true; |
|||
} |
|||
|
|||
void SerialBLEInterface::onPairingComplete(uint16_t connection_handle, uint8_t auth_status) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing complete handle=0x%04X status=%u", connection_handle, auth_status); |
|||
if (instance) { |
|||
if (instance->isValidConnection(connection_handle)) { |
|||
if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing successful"); |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing failed, disconnecting"); |
|||
instance->disconnect(); |
|||
} |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("onPairingComplete: ignoring stale callback"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) { |
|||
if (!instance) return; |
|||
|
|||
if (evt->header.evt_id == BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST) { |
|||
uint16_t conn_handle = evt->evt.gap_evt.conn_handle; |
|||
if (instance->isValidConnection(conn_handle)) { |
|||
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: handle=0x%04X, min_interval=%u, max_interval=%u, latency=%u, timeout=%u", |
|||
conn_handle, |
|||
evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval, |
|||
evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval, |
|||
evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency, |
|||
evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout); |
|||
|
|||
uint32_t err_code = sd_ble_gap_conn_param_update(conn_handle, NULL); |
|||
if (err_code == NRF_SUCCESS) { |
|||
BLE_DEBUG_PRINTLN("Accepted CONN_PARAM_UPDATE_REQUEST (using PPCP)"); |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("ERROR: Failed to accept CONN_PARAM_UPDATE_REQUEST: 0x%08X", err_code); |
|||
} |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: ignoring stale callback for handle=0x%04X", conn_handle); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { |
|||
instance = this; |
|||
|
|||
char charpin[20]; |
|||
sprintf(charpin, "%d", pin_code); |
|||
|
|||
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code); |
|||
|
|||
// If we want to control BLE LED ourselves, uncomment this:
|
|||
// Bluefruit.autoConnLed(false);
|
|||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); |
|||
Bluefruit.configPrphConn(250, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); // increase MTU
|
|||
Bluefruit.setTxPower(BLE_TX_POWER); |
|||
Bluefruit.begin(); |
|||
Bluefruit.setName(device_name); |
|||
|
|||
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; |
|||
ppcp_params.max_conn_interval = BLE_MAX_CONN_INTERVAL; |
|||
ppcp_params.slave_latency = BLE_SLAVE_LATENCY; |
|||
ppcp_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT; |
|||
|
|||
uint32_t err_code = sd_ble_gap_ppcp_set(&ppcp_params); |
|||
if (err_code == NRF_SUCCESS) { |
|||
BLE_DEBUG_PRINTLN("PPCP set: %u-%ums interval, latency=%u, %ums timeout", |
|||
ppcp_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
|
|||
ppcp_params.max_conn_interval * 5 / 4, |
|||
ppcp_params.slave_latency, |
|||
ppcp_params.conn_sup_timeout * 10); // convert to ms (10ms units)
|
|||
} else { |
|||
BLE_DEBUG_PRINTLN("Failed to set PPCP: %lu", err_code); |
|||
} |
|||
|
|||
Bluefruit.setTxPower(BLE_TX_POWER); |
|||
Bluefruit.setName(dev_name); |
|||
|
|||
Bluefruit.Security.setMITM(true); |
|||
Bluefruit.Security.setPIN(charpin); |
|||
Bluefruit.Security.setIOCaps(true, false, false); |
|||
Bluefruit.Security.setPairPasskeyCallback(onPairingPasskey); |
|||
Bluefruit.Security.setPairCompleteCallback(onPairingComplete); |
|||
|
|||
Bluefruit.Periph.setConnectCallback(onConnect); |
|||
Bluefruit.Periph.setDisconnectCallback(onDisconnect); |
|||
Bluefruit.Security.setSecuredCallback(onSecured); |
|||
|
|||
// To be consistent OTA DFU should be added first if it exists
|
|||
//bledfu.begin();
|
|||
Bluefruit.setEventCallback(onBLEEvent); |
|||
|
|||
// Configure and start the BLE Uart service
|
|||
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); |
|||
bleuart.begin(); |
|||
|
|||
} |
|||
|
|||
void SerialBLEInterface::startAdv() { |
|||
|
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising"); |
|||
|
|||
// clean restart if already advertising
|
|||
if(Bluefruit.Advertising.isRunning()){ |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart"); |
|||
Bluefruit.Advertising.stop(); |
|||
} |
|||
bleuart.setRxCallback(onBleUartRX); |
|||
|
|||
Bluefruit.Advertising.clearData(); // clear advertising data
|
|||
Bluefruit.ScanResponse.clearData(); // clear scan response data
|
|||
|
|||
// Advertising packet
|
|||
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); |
|||
Bluefruit.Advertising.addTxPower(); |
|||
|
|||
// Include the BLE UART (AKA 'NUS') 128-bit UUID
|
|||
Bluefruit.Advertising.addService(bleuart); |
|||
|
|||
// Secondary Scan Response packet (optional)
|
|||
// Since there is no room for 'Name' in Advertising packet
|
|||
Bluefruit.ScanResponse.addName(); |
|||
|
|||
/* Start Advertising
|
|||
* - Enable auto advertising if disconnected |
|||
* - Interval: fast mode = 20 ms, slow mode = 152.5 ms |
|||
* - Timeout for fast mode is 30 seconds |
|||
* - Start(timeout) with timeout = 0 will advertise forever (until connected) |
|||
* |
|||
* For recommended advertising interval |
|||
* https://developer.apple.com/library/content/qa/qa1931/_index.html
|
|||
*/ |
|||
Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
|
|||
Bluefruit.Advertising.setInterval(32, 244); |
|||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
|||
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
|
|||
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX); |
|||
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT); |
|||
|
|||
Bluefruit.Advertising.restartOnDisconnect(true); |
|||
|
|||
} |
|||
|
|||
void SerialBLEInterface::stopAdv() { |
|||
void SerialBLEInterface::clearBuffers() { |
|||
send_queue_len = 0; |
|||
recv_queue_len = 0; |
|||
_last_retry_attempt = 0; |
|||
bleuart.flush(); |
|||
} |
|||
|
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising"); |
|||
|
|||
// we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack
|
|||
if(!Bluefruit.Advertising.isRunning()){ |
|||
return; |
|||
void SerialBLEInterface::shiftSendQueueLeft() { |
|||
if (send_queue_len > 0) { |
|||
send_queue_len--; |
|||
for (uint8_t i = 0; i < send_queue_len; i++) { |
|||
send_queue[i] = send_queue[i + 1]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// stop advertising
|
|||
Bluefruit.Advertising.stop(); |
|||
void SerialBLEInterface::shiftRecvQueueLeft() { |
|||
if (recv_queue_len > 0) { |
|||
recv_queue_len--; |
|||
for (uint8_t i = 0; i < recv_queue_len; i++) { |
|||
recv_queue[i] = recv_queue[i + 1]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const { |
|||
if (_conn_handle != handle) { |
|||
return false; |
|||
} |
|||
BLEConnection* conn = Bluefruit.Connection(handle); |
|||
if (conn == nullptr || !conn->connected()) { |
|||
return false; |
|||
} |
|||
if (requireWaitingForSecurity && _isDeviceConnected) { |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
// ---------- public methods
|
|||
bool SerialBLEInterface::isAdvertising() const { |
|||
ble_gap_addr_t adv_addr; |
|||
uint32_t err_code = sd_ble_gap_adv_addr_get(0, &adv_addr); |
|||
return (err_code == NRF_SUCCESS); |
|||
} |
|||
|
|||
void SerialBLEInterface::enable() { |
|||
void SerialBLEInterface::enable() { |
|||
if (_isEnabled) return; |
|||
|
|||
_isEnabled = true; |
|||
clearBuffers(); |
|||
_last_health_check = millis(); |
|||
|
|||
// Start advertising
|
|||
startAdv(); |
|||
Bluefruit.Advertising.start(0); |
|||
} |
|||
|
|||
void SerialBLEInterface::disconnect() { |
|||
if (_conn_handle != BLE_CONN_HANDLE_INVALID) { |
|||
sd_ble_gap_disconnect(_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); |
|||
} |
|||
} |
|||
|
|||
void SerialBLEInterface::disable() { |
|||
_isEnabled = false; |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: disable"); |
|||
|
|||
#ifdef RAK_BOARD |
|||
Bluefruit.disconnect(Bluefruit.connHandle()); |
|||
#else |
|||
uint16_t conn_id; |
|||
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) { |
|||
Bluefruit.disconnect(conn_id); |
|||
} |
|||
#endif |
|||
|
|||
Bluefruit.Advertising.restartOnDisconnect(false); |
|||
disconnect(); |
|||
Bluefruit.Advertising.stop(); |
|||
Bluefruit.Advertising.clearData(); |
|||
|
|||
stopAdv(); |
|||
_last_health_check = 0; |
|||
} |
|||
|
|||
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { |
|||
if (len > MAX_FRAME_SIZE) { |
|||
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len); |
|||
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len); |
|||
return 0; |
|||
} |
|||
|
|||
if (_isDeviceConnected && len > 0) { |
|||
bool connected = isConnected(); |
|||
if (connected && len > 0) { |
|||
if (send_queue_len >= FRAME_QUEUE_SIZE) { |
|||
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); |
|||
return 0; |
|||
} |
|||
|
|||
send_queue[send_queue_len].len = len; // add to send queue
|
|||
send_queue[send_queue_len].len = len; |
|||
memcpy(send_queue[send_queue_len].buf, src, len); |
|||
send_queue_len++; |
|||
|
|||
|
|||
return len; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
#define BLE_WRITE_MIN_INTERVAL 60 |
|||
|
|||
bool SerialBLEInterface::isWriteBusy() const { |
|||
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
|
|||
} |
|||
|
|||
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { |
|||
if (send_queue_len > 0 // first, check send queue
|
|||
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
|
|||
) { |
|||
_last_write = millis(); |
|||
bleuart.write(send_queue[0].buf, send_queue[0].len); |
|||
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]); |
|||
|
|||
send_queue_len--; |
|||
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
|
|||
send_queue[i] = send_queue[i + 1]; |
|||
if (send_queue_len > 0) { |
|||
if (!isConnected()) { |
|||
BLE_DEBUG_PRINTLN("writeBytes: connection invalid, clearing send queue"); |
|||
send_queue_len = 0; |
|||
} else { |
|||
unsigned long now = millis(); |
|||
bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS); |
|||
|
|||
if (!throttle_active) { |
|||
Frame frame_to_send = send_queue[0]; |
|||
|
|||
size_t written = bleuart.write(frame_to_send.buf, frame_to_send.len); |
|||
if (written == frame_to_send.len) { |
|||
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", (unsigned)frame_to_send.len, (unsigned)frame_to_send.buf[0]); |
|||
_last_retry_attempt = 0; |
|||
shiftSendQueueLeft(); |
|||
} else if (written > 0) { |
|||
BLE_DEBUG_PRINTLN("writeBytes: partial write, sent=%u of %u, dropping corrupted frame", (unsigned)written, (unsigned)frame_to_send.len); |
|||
_last_retry_attempt = 0; |
|||
shiftSendQueueLeft(); |
|||
} else { |
|||
if (!isConnected()) { |
|||
BLE_DEBUG_PRINTLN("writeBytes failed: connection lost, dropping frame"); |
|||
_last_retry_attempt = 0; |
|||
shiftSendQueueLeft(); |
|||
} else { |
|||
BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry"); |
|||
_last_retry_attempt = now; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
int len = bleuart.available(); |
|||
if (len > 0) { |
|||
bleuart.readBytes(dest, len); |
|||
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); |
|||
return len; |
|||
} |
|||
|
|||
if (recv_queue_len > 0) { |
|||
size_t len = recv_queue[0].len; |
|||
memcpy(dest, recv_queue[0].buf, len); |
|||
|
|||
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, (unsigned)dest[0]); |
|||
|
|||
shiftRecvQueueLeft(); |
|||
return len; |
|||
} |
|||
|
|||
// Advertising watchdog: periodically check if advertising is running, restart if not
|
|||
// Only run when truly disconnected (no connection handle), not during connection establishment
|
|||
unsigned long now = millis(); |
|||
if (_isEnabled && !isConnected() && _conn_handle == BLE_CONN_HANDLE_INVALID) { |
|||
if (now - _last_health_check >= BLE_HEALTH_CHECK_INTERVAL) { |
|||
_last_health_check = now; |
|||
|
|||
if (!isAdvertising()) { |
|||
BLE_DEBUG_PRINTLN("SerialBLEInterface: advertising watchdog - advertising stopped, restarting"); |
|||
Bluefruit.Advertising.start(0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
void SerialBLEInterface::onBleUartRX(uint16_t conn_handle) { |
|||
if (!instance) { |
|||
return; |
|||
} |
|||
|
|||
if (instance->_conn_handle != conn_handle || !instance->isConnected()) { |
|||
while (instance->bleuart.available() > 0) { |
|||
instance->bleuart.read(); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
while (instance->bleuart.available() > 0) { |
|||
if (instance->recv_queue_len >= FRAME_QUEUE_SIZE) { |
|||
while (instance->bleuart.available() > 0) { |
|||
instance->bleuart.read(); |
|||
} |
|||
BLE_DEBUG_PRINTLN("onBleUartRX: recv queue full, dropping data"); |
|||
break; |
|||
} |
|||
|
|||
int avail = instance->bleuart.available(); |
|||
|
|||
if (avail > MAX_FRAME_SIZE) { |
|||
BLE_DEBUG_PRINTLN("onBleUartRX: WARN: BLE RX overflow, avail=%d, draining all", avail); |
|||
uint8_t drain_buf[BLE_RX_DRAIN_BUF_SIZE]; |
|||
while (instance->bleuart.available() > 0) { |
|||
int chunk = instance->bleuart.available() > BLE_RX_DRAIN_BUF_SIZE ? BLE_RX_DRAIN_BUF_SIZE : instance->bleuart.available(); |
|||
instance->bleuart.readBytes(drain_buf, chunk); |
|||
} |
|||
continue; |
|||
} |
|||
|
|||
int read_len = avail; |
|||
instance->recv_queue[instance->recv_queue_len].len = read_len; |
|||
instance->bleuart.readBytes(instance->recv_queue[instance->recv_queue_len].buf, read_len); |
|||
instance->recv_queue_len++; |
|||
} |
|||
} |
|||
|
|||
bool SerialBLEInterface::isConnected() const { |
|||
return _isDeviceConnected; |
|||
return _isDeviceConnected && Bluefruit.connected() > 0; |
|||
} |
|||
|
|||
bool SerialBLEInterface::isWriteBusy() const { |
|||
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); |
|||
} |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue