Merge branch 'dev'

# Conflicts:
#	docs/payloads.md
This commit is contained in:
Scott Powell
2026-08-09 15:16:48 +10:00
348 changed files with 11264 additions and 1646 deletions
+3
View File
@@ -31,8 +31,11 @@ jobs:
- Heltec_v3_room_server - Heltec_v3_room_server
# nRF52 # nRF52
- RAK_4631_companion_radio_ble - RAK_4631_companion_radio_ble
- RAK_4631_companion_radio_ethernet
- RAK_4631_repeater - RAK_4631_repeater
- RAK_4631_repeater_ethernet
- RAK_4631_room_server - RAK_4631_room_server
- RAK_4631_room_server_ethernet
# RP2040 # RP2040
- PicoW_repeater - PicoW_repeater
# STM32 # STM32
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment uses: ./.github/actions/setup-build-environment
- name: Run Unit Tests - name: Run Unit Tests
run: pio test -e native -vv run: pio test -e native -e native_kiss_modem -vv
- name: Upload Test Results - name: Upload Test Results
# Upload test results even if the test step failed. # Upload test results even if the test step failed.
-9
View File
@@ -1,9 +0,0 @@
{
"recommendations": [
"pioarduino.pioarduino-ide",
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_rc32"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "Heltec"
}
+61
View File
@@ -0,0 +1,61 @@
{
"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"],
["0x239A","0x0071"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_tower_v2",
"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": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "Heltec Tower V2 Board",
"upload": {
"maximum_ram_size": 235520,
"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://heltec.org/",
"vendor": "Heltec"
}
+2 -2
View File
@@ -9,7 +9,7 @@
"extra_flags": [ "extra_flags": [
"-DBOARD_HAS_PSRAM", "-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0", "-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1", "-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1" "-DARDUINO_EVENT_RUNNING_CORE=1"
], ],
@@ -40,4 +40,4 @@
}, },
"url": "https://heltec.org/", "url": "https://heltec.org/",
"vendor": "heltec" "vendor": "heltec"
} }
+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4_r8"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}
+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x0002"], ["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "meshnology_w12"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v5 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}
+60
View File
@@ -0,0 +1,60 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"],
["0x2886", "0x0057"]
],
"usb_product": "X1-BOOT",
"mcu": "nrf52840",
"variant": "Seeed_Mesh-Tracker-X1",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": ["arduino"],
"name": "Seeed SenseCAP MeshTracker X1",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.seeedstudio.com/SenseCAP-MeshTracker-X1-for-Meshtastic-p-6793.html",
"vendor": "Seeed Studio"
}
+42
View File
@@ -0,0 +1,42 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-D BOARD_HAS_PSRAM",
"-D ARDUINO_USB_CDC_ON_BOOT=0",
"-D ARDUINO_USB_MODE=0",
"-D ARDUINO_RUNNING_CORE=1",
"-D ARDUINO_EVENT_RUNNING_CORE=0"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qio_opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "ELECROW-ThinkNode-M7"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "ELECROW ThinkNode M7",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 524288,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com",
"vendor": "ELECROW"
}
+57
View File
@@ -0,0 +1,57 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qio_opi",
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "ELECROW-ThinkNode-M9"
},
"connectivity": [
"wifi",
"bluetooth",
"lora"
],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": [
"esp-builtin"
],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "elecrow-thinknode-m9",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 524288,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html",
"vendor": "ELECROW"
}
+4 -3
View File
@@ -1,11 +1,12 @@
{ pkgs ? import <nixpkgs> {} }: {pkgs ? import <nixpkgs> {}}: let
let
in in
pkgs.mkShell { pkgs.mkShell {
buildInputs = [ buildInputs = [
pkgs.platformio pkgs.platformio
pkgs.python3 pkgs.python3
pkgs.gcc
pkgs.gtest
# optional: needed as a programmer i.e. for esp32 # optional: needed as a programmer i.e. for esp32
pkgs.avrdude pkgs.avrdude
]; ];
} }
+52 -1
View File
@@ -19,6 +19,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
- [GPS](#gps-when-gps-support-is-compiled-in) - [GPS](#gps-when-gps-support-is-compiled-in)
- [Sensors](#sensors-when-sensor-support-is-compiled-in) - [Sensors](#sensors-when-sensor-support-is-compiled-in)
- [Bridge](#bridge-when-bridge-support-is-compiled-in) - [Bridge](#bridge-when-bridge-support-is-compiled-in)
- [Ethernet](#ethernet-when-ethernet-support-is-compiled-in)
--- ---
@@ -276,6 +277,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore
--- ---
#### View or change the LoRa FEM receive-path gain state on supported boards
**Usage:**
- `get radio.fem.rxgain`
- `set radio.fem.rxgain <state>`
**Parameters:**
- `state`: `on`|`off`
**Notes:**
- This controls the external LoRa FEM receive-path LNA where the board supports it.
- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode.
---
### System ### System
#### View or change this node's name #### View or change this node's name
@@ -290,7 +305,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Default:** Varies by board **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. **Note:** Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary.
--- ---
@@ -577,6 +592,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore
--- ---
#### Enable or disable hardware Channel Activity Detection (CAD)
**Usage:**
- `get cad`
- `set cad <on|off>`
**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` — either, both, or none may be active.
**Parameters:**
- `on|off`: Enable or disable hardware CAD
**Default:** `off`
---
#### View or change the AGC Reset Interval #### View or change the AGC Reset Interval
**Usage:** **Usage:**
- `get agc.reset.interval` - `get agc.reset.interval`
@@ -1124,3 +1153,25 @@ region save
**Note:** Returns an error on boards without power management support. **Note:** Returns an error on boards without power management support.
--- ---
### Ethernet (when Ethernet support is compiled in)
Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature.
---
#### View Ethernet connection status
**Usage:**
- `eth.status`
**Output:**
- `ETH: <ip>:<port>` when connected (e.g. `ETH: 192.168.1.50:23`)
- `ETH: not connected` when Ethernet is not active
**Notes:**
- Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI.
- The Ethernet interface obtains an IP address via DHCP automatically on boot.
- A TCP server listens on port 23 (default) for CLI connections.
- Connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial.
---
+27
View File
@@ -83,6 +83,7 @@ A list of frequently-asked questions and answers for MeshCore
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code) - [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
- [7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?](#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3) - [7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?](#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3)
- [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to) - [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to)
- [7.8. Q: How do I use Ethernet with a RAK4631?](#78-q-how-do-i-use-ethernet-with-a-rak4631)
## 1. Introduction ## 1. Introduction
@@ -820,3 +821,29 @@ For companion radios, you can set these radios' transmit power in the smartphone
| **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) [data sheet](https://www.cdebyte.com/pdf-down.aspx?id=4216) Refer to your local government's requirements | | **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) [data sheet](https://www.cdebyte.com/pdf-down.aspx?id=4216) Refer to your local government's requirements |
| **Heltec V4** | Standard Output | 10 dBm | 22 dBm (~0.15W) | | | **Heltec V4** | Standard Output | 10 dBm | 22 dBm (~0.15W) | |
| | High Output | 22 dBm | 28 dBm (~0.5W to 0.6W) | | | | High Output | 22 dBm | 28 dBm (~0.5W to 0.6W) | |
---
### 7.8. Q: How do I use Ethernet with a RAK4631?
**A:**
MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.rakwireless.com/product-categories/wisblock/rak13800/datasheet/) WisBlock Ethernet module (based on the W5100S chip).
**Hardware required:**
- RAK4631 WisBlock Core
- RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot)
- RAK13800 WisBlock Ethernet module
- Ethernet cable connected to a network with a DHCP server
**Firmware:**
Flash one of the Ethernet-enabled firmware variants:
- `RAK_4631_repeater_ethernet` - Repeater with Ethernet CLI access
- `RAK_4631_room_server_ethernet` - Room server with Ethernet CLI access
- `RAK_4631_companion_radio_ethernet` - Companion radio over Ethernet (replaces BLE)
**Connecting:**
- The device obtains an IP address via DHCP automatically on boot.
- For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. `nc <ip> 23` or PuTTY in raw mode). This gives you the same CLI available over serial/USB.
- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio).
- Use the `eth.status` CLI command to check connection status and see the assigned IP address.
---
+10 -4
View File
@@ -41,7 +41,7 @@ Maximum unescaped frame size: 512 bytes.
| Command | Value | Data | Description | | Command | Value | Data | Description |
|-------------|--------|--------------------|-------------------------------------------------------------| |-------------|--------|--------------------|-------------------------------------------------------------|
| Data | `0x00` | Raw packet | Queue packet for transmission | | Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) |
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) | | TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) | | Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) | | SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
@@ -58,6 +58,12 @@ Maximum unescaped frame size: 512 bytes.
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
### Host Output Backpressure
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
### CSMA Behavior ### CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation: The TNC implements p-persistent CSMA for half-duplex operation:
@@ -156,15 +162,15 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic
| MacFailed | `0x04` | MAC verification failed | | MacFailed | `0x04` | MAC verification failed |
| UnknownCmd | `0x05` | Unknown sub-command | | UnknownCmd | `0x05` | Unknown sub-command |
| EncryptFailed | `0x06` | Encryption failed | | EncryptFailed | `0x06` | Encryption failed |
| TxBusy | `0x07` | Transmit busy | | TxBusy | `0x07` | Radio TX busy, or host output queue full |
### Unsolicited Events ### Unsolicited Events
The TNC sends these SetHardware frames without a preceding request: The TNC sends these SetHardware frames without a preceding request:
**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. **TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame. **RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
## Data Formats ## Data Formats
+2
View File
@@ -17,6 +17,8 @@ Once you have a working app/project, you need to be able to demonstrate it exist
| 0000 - 00FF | -reserved for internal use- | | | 0000 - 00FF | -reserved for internal use- | |
| 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open |
| 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz |
| 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ |
| 0130 - 013F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion |
| FF00 - FFFF | -reserved for testing/dev- | | | FF00 - FFFF | -reserved for testing/dev- | |
(add rows, inside the range 0100 - FEFF for custom apps) (add rows, inside the range 0100 - FEFF for custom apps)
+26 -26
View File
@@ -23,7 +23,7 @@ NOTE: all 16 and 32-bit integer fields are Little Endian.
* Node hash: the first byte of the node's public key * Node hash: the first byte of the node's public key
# Node advertisement ## Node advertisement
This kind of payload notifies receivers that a node exists, and gives information about the node This kind of payload notifies receivers that a node exists, and gives information about the node
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
@@ -57,7 +57,7 @@ Appdata Flags
| `0x40` | has feature 2 | Reserved for future use. | | `0x40` | has feature 2 | Reserved for future use. |
| `0x80` | has name | appdata contains a node name | | `0x80` | has name | appdata contains a node name |
# Acknowledgement ## Acknowledgement
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra. An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
@@ -66,7 +66,7 @@ An acknowledgement that a message was received. Note that for returned path mess
| checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey | | checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey |
# Returned path, request, response, and plain text message ## Returned path, request, response, and plain text message
Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation. Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.
@@ -77,7 +77,7 @@ Returned path, request, response, and plain text messages are all formatted in t
| cipher MAC | 2 | MAC for encrypted data in next field | | cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see subsections below for details | | ciphertext | rest of payload | encrypted message, see subsections below for details |
## Returned path ### Returned path
Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message. Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.
@@ -88,7 +88,7 @@ Returned path messages provide a description of the route a packet took from the
| extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md) | | extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md) |
| extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document | | extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document |
## Request ### Request
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|------------------------------------------| |--------------|-----------------|------------------------------------------|
@@ -102,7 +102,7 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v
| `0x01` | get stats | get stats of repeater or room server | | `0x01` | get stats | get stats of repeater or room server |
| `0x02` | keepalive | keep-alive request used for maintained connections | | `0x02` | keepalive | keep-alive request used for maintained connections |
### Get stats #### Get stats
Gets information about the node, possibly including the following: Gets information about the node, possibly including the following:
@@ -125,32 +125,32 @@ Gets information about the node, possibly including the following:
* Number posted (?) * Number posted (?)
* Number of post pushes (?) * Number of post pushes (?)
### Get telemetry data #### Get telemetry data
Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
### Get Telemetry #### Get Telemetry
Not defined in `BaseChatMesh`. Not defined in `BaseChatMesh`.
### Get Min/Max/Ave (Sensor nodes) #### Get Min/Max/Ave (Sensor nodes)
Not defined in `BaseChatMesh`. Not defined in `BaseChatMesh`.
### Get Access List #### Get Access List
Not defined in `BaseChatMesh`. Not defined in `BaseChatMesh`.
### Get Neighbors #### Get Neighbors
Not defined in `BaseChatMesh`. Not defined in `BaseChatMesh`.
### Get Owner Info #### Get Owner Info
Not defined in `BaseChatMesh`. Not defined in `BaseChatMesh`.
## Response ### Response
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|---------|-----------------|-----------------------------------| |---------|-----------------|-----------------------------------|
@@ -158,7 +158,7 @@ Not defined in `BaseChatMesh`.
Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above. Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.
## Plain text message ### Plain text message
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------------|-----------------|-----------------------------------------------------------------------------------| |--------------------|-----------------|-----------------------------------------------------------------------------------|
@@ -174,7 +174,7 @@ txt_type
| `0x01` | CLI command | the command text of the message | | `0x01` | CLI command | the command text of the message |
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
# Anonymous request ## Anonymous request
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|------------------|-----------------|-------------------------------------------| |------------------|-----------------|-------------------------------------------|
@@ -183,7 +183,7 @@ txt_type
| cipher MAC | 2 | MAC for encrypted data in next field | | cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details | | ciphertext | rest of payload | encrypted message, see below for details |
## Room server login ### Room server login
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------| |----------------|-----------------|-------------------------------------------------------------------------------|
@@ -191,14 +191,14 @@ txt_type
| sync timestamp | 4 | sender's "sync messages SINCE x" timestamp | | sync timestamp | 4 | sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for room | | password | rest of message | password for room |
## Repeater/Sensor login ### Repeater/Sensor login
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------| |----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) | | timestamp | 4 | sender time (unix timestamp) |
| password | rest of message | password for repeater/sensor | | password | rest of message | password for repeater/sensor |
## Repeater - Regions request ### Repeater - Regions request
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|----------------|--------------|------------------------------| |----------------|--------------|------------------------------|
@@ -207,7 +207,7 @@ txt_type
| reply path len | 1 | path len for reply | | reply path len | 1 | path len for reply |
| reply path | (variable) | reply path | | reply path | (variable) | reply path |
## Repeater - Owner info request ### Repeater - Owner info request
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|----------------|--------------|------------------------------| |----------------|--------------|------------------------------|
@@ -216,7 +216,7 @@ txt_type
| reply path len | 1 | path len for reply | | reply path len | 1 | path len for reply |
| reply path | (variable) | reply path | | reply path | (variable) | reply path |
## Repeater - Clock and status request ### Repeater - Clock and status request
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|----------------|--------------|------------------------------| |----------------|--------------|------------------------------|
@@ -226,7 +226,7 @@ txt_type
| reply path | (variable) | reply path | | reply path | (variable) | reply path |
# Group text message ## Group text message
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|----------------------------------------------| |--------------|-----------------|----------------------------------------------|
@@ -239,7 +239,7 @@ The plaintext contained in the ciphertext matches the format described in [plain
The sender name is unverified message text. Group messages contain no sender The sender name is unverified message text. Group messages contain no sender
signature, so any channel-key holder can choose any sender name. signature, so any channel-key holder can choose any sender name.
# Group datagram ## Group datagram
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|----------------------------------------------| |--------------|-----------------|----------------------------------------------|
@@ -256,14 +256,14 @@ The data contained in the ciphertext uses the format below:
| data | rest of payload | (depends on data type) | | data | rest of payload | (depends on data type) |
# Control data ## Control data
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------| |--------------|-----------------|--------------------------------------------|
| flags | 1 | upper 4 bits is sub_type | | flags | 1 | upper 4 bits is sub_type |
| data | rest of payload | typically unencrypted data | | data | rest of payload | typically unencrypted data |
## DISCOVER_REQ (sub_type) ### DISCOVER_REQ (sub_type)
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|----------------------------------------------| |--------------|-----------------|----------------------------------------------|
@@ -272,7 +272,7 @@ The data contained in the ciphertext uses the format below:
| tag | 4 | randomly generate by sender | | tag | 4 | randomly generate by sender |
| since | 4 | (optional) epoch timestamp (0 by default) | | since | 4 | (optional) epoch timestamp (0 by default) |
## DISCOVER_RESP (sub_type) ### DISCOVER_RESP (sub_type)
| Field | Size (bytes) | Description | | Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------| |--------------|-----------------|--------------------------------------------|
@@ -282,6 +282,6 @@ The data contained in the ciphertext uses the format below:
| pubkey | 8 or 32 | node's ID (or prefix) | | pubkey | 8 or 32 | node's ID (or prefix) |
# Custom packet ## Custom packet
Custom packets have no defined format. Custom packets have no defined format.
+6 -6
View File
@@ -4,7 +4,7 @@
#include <helpers/ui/DisplayDriver.h> #include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h> #include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h> #include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h> #include <helpers/MultiSerialInterface.h>
#include <Arduino.h> #include <Arduino.h>
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
@@ -25,10 +25,10 @@ enum class UIEventType {
class AbstractUITask { class AbstractUITask {
protected: protected:
mesh::MainBoard* _board; mesh::MainBoard* _board;
BaseSerialInterface* _serial; MultiSerialInterface* _interfaceManager;
bool _connected; bool _connected;
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) {
_connected = false; _connected = false;
} }
@@ -36,9 +36,9 @@ public:
void setHasConnection(bool connected) { _connected = connected; } void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; } bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _serial->isEnabled(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); }
void enableSerial() { _serial->enable(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); }
void disableSerial() { _serial->disable(); } void disableBluetooth() { _interfaceManager->disableBluetooth(); }
virtual void msgRead(int msgcount) = 0; virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void notify(UIEventType t = UIEventType::none) = 0;
+25 -47
View File
@@ -189,17 +189,22 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
return identity_store.save("_main", identity); return identity_store.save("_main", identity);
} }
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) { void DataStore::loadPrefs(NodePrefs& prefs) {
if (_fs->exists("/new_prefs")) { if (_fs->exists("/prefs.json")) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename File file = openRead(_fs, "/prefs.json");
} else if (_fs->exists("/node_prefs")) { if (file) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon); prefs.loadSerial(file); // new Serial prefs
savePrefs(prefs, node_lat, node_lon); // save to new filename file.close();
_fs->remove("/node_prefs"); // remove old }
} else if (_fs->exists("/new_prefs")) {
loadPrefsInt("/new_prefs", prefs);
if (savePrefs(prefs) ) { // save to new format
//_fs->remove("/new_prefs"); // remove old
}
} }
} }
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs) {
File file = openRead(_fs, filename); File file = openRead(_fs, filename);
if (file) { if (file) {
uint8_t pad[8]; uint8_t pad[8];
@@ -207,12 +212,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
file.read(pad, 4); // 36 file.read(pad, 4); // 36
file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40 file.read((uint8_t *)&_prefs.node_lat, sizeof(_prefs.node_lat)); // 40
file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48 file.read((uint8_t *)&_prefs.node_lon, sizeof(_prefs.node_lon)); // 48
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 file.read((uint8_t *)&_prefs._client_repeat, sizeof(_prefs._client_repeat)); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -234,48 +239,21 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
// migrate old fields
_prefs.setRepeatEn(_prefs._client_repeat != 0);
file.close(); file.close();
} }
} }
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { bool DataStore::savePrefs(NodePrefs& _prefs) {
File file = openWrite(_fs, "/new_prefs"); File file = openWrite(_fs, "/prefs.json");
if (file) { if (file) {
uint8_t pad[8]; bool success = _prefs.saveSerial(file);
memset(pad, 0, sizeof(pad));
file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40
file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
file.write(pad, 1); // 79
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.close(); file.close();
return success;
} }
return false;
} }
void DataStore::loadContacts(DataStoreHost* host) { void DataStore::loadContacts(DataStoreHost* host) {
@@ -545,7 +523,7 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
uint32_t pos = 0, found_pos = 0; uint32_t pos = 0, found_pos = 0;
uint32_t min_timestamp = 0xFFFFFFFF; uint32_t min_timestamp = 0xFFFFFFFF;
// search for matching key OR evict by oldest timestmap // search for matching key OR evict by oldest timestamp
BlobRec tmp; BlobRec tmp;
file.seek(0); file.seek(0);
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
+3 -3
View File
@@ -19,7 +19,7 @@ class DataStore {
mesh::RTCClock* _clock; mesh::RTCClock* _clock;
IdentityStore identity_store; IdentityStore identity_store;
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); void loadPrefsInt(const char *filename, NodePrefs& prefs);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
void checkAdvBlobFile(); void checkAdvBlobFile();
#endif #endif
@@ -33,8 +33,8 @@ public:
FILESYSTEM* getSecondaryFS() const { return _fsExtra; } FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
bool loadMainIdentity(mesh::LocalIdentity &identity); bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity); bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon); void loadPrefs(NodePrefs& prefs);
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); bool savePrefs(NodePrefs& prefs);
void loadContacts(DataStoreHost* host); void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
void loadChannels(DataStoreHost* host); void loadChannels(DataStoreHost* host);
+24 -15
View File
@@ -261,6 +261,9 @@ float MyMesh::getAirtimeBudgetFactor() const {
int MyMesh::getInterferenceThreshold() const { int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved return 0; // disabled for now, until currentRSSI() problem is resolved
} }
bool MyMesh::getCADEnabled() const {
return false; // hardware CAD before TX (disabled by default, until configurable)
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const { int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0; if (_prefs.rx_delay_base <= 0.0f) return 0;
@@ -480,7 +483,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
} }
bool MyMesh::allowPacketForward(const mesh::Packet* packet) { bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
return _prefs.client_repeat != 0; return _prefs.isRepeatEn();
} }
void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) {
@@ -654,6 +657,11 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
// query other sensors -- target specific // query other sensors -- target specific
sensors.querySensors(permissions, telemetry); sensors.querySensors(permissions, telemetry);
float temperature = board.getMCUTemperature();
if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
}
memcpy(reply, &sender_timestamp, memcpy(reply, &sender_timestamp,
4); // reflect sender_timestamp back in response packet (kind of like a 'tag') 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
@@ -854,7 +862,7 @@ void MyMesh::onSendTimeout() {}
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false; _iter_started = false;
_cli_rescue = false; _cli_rescue = false;
offline_queue_len = 0; offline_queue_len = 0;
@@ -868,7 +876,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
send_unscoped = false; send_unscoped = false;
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
strcpy(_prefs.node_name, "NONAME"); strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ; _prefs.freq = LORA_FREQ;
@@ -879,6 +886,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default _prefs.gps_interval = 0; // No automatic GPS updates by default
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed //_prefs.rx_delay_base = 10.0f; enable once new algo fixed
_prefs.setRepeatEn(false);
#if defined(USE_SX1262) || defined(USE_SX1268) #if defined(USE_SX1262) || defined(USE_SX1268)
#ifdef SX126X_RX_BOOSTED_GAIN #ifdef SX126X_RX_BOOSTED_GAIN
_prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN;
@@ -923,7 +931,9 @@ void MyMesh::begin(bool has_display) {
#endif #endif
// load persisted prefs // load persisted prefs
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); _store->loadPrefs(_prefs);
sensors.node_lat = _prefs.node_lat;
sensors.node_lon = _prefs.node_lon;
// sanitise bad pref values // sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
@@ -1023,7 +1033,7 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 40; i += 40;
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
i += 20; i += 20;
out_frame[i++] = _prefs.client_repeat; // v9+ out_frame[i++] = _prefs.isRepeatEn() ? 1 : 0; // v9+
out_frame[i++] = _prefs.path_hash_mode; // v10+ out_frame[i++] = _prefs.path_hash_mode; // v10+
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_APP_START && } else if (cmd_frame[0] == CMD_APP_START &&
@@ -1385,7 +1395,7 @@ void MyMesh::handleCmdFrame(size_t len) {
_prefs.cr = cr; _prefs.cr = cr;
_prefs.freq = (float)freq / 1000.0; _prefs.freq = (float)freq / 1000.0;
_prefs.bw = (float)bw / 1000.0; _prefs.bw = (float)bw / 1000.0;
_prefs.client_repeat = repeat; _prefs.setRepeatEn(repeat != 0);
savePrefs(); savePrefs();
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1542,6 +1552,7 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE); memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE);
anon.out_path_len = 0; // default to zero-hop direct anon.out_path_len = 0; // default to zero-hop direct
anon.type = ADV_TYPE_NONE; // unknown anon.type = ADV_TYPE_NONE; // unknown
anon.lastmod = getRTCClock()->getCurrentTime();
if (addContact(anon)) recipient = &anon; if (addContact(anon)) recipient = &anon;
} }
@@ -1636,6 +1647,11 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request
telemetry.reset(); telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
float temperature = board.getMCUTemperature();
if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
}
// query other sensors -- target specific // query other sensors -- target specific
sensors.querySensors(0xFF, telemetry); sensors.querySensors(0xFF, telemetry);
@@ -1981,6 +1997,7 @@ void MyMesh::handleCmdFrame(size_t len) {
sendPacket(pkt, priority, 0); sendPacket(pkt, priority, 0);
writeOKFrame(); writeOKFrame();
} else { } else {
releasePacket(pkt);
writeErrFrame(ERR_CODE_ILLEGAL_ARG); writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} }
} else { } else {
@@ -2186,15 +2203,7 @@ void MyMesh::checkSerialInterface() {
&& !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly!
) { ) {
ContactInfo contact; ContactInfo contact;
bool found = false; if (_iter.hasNext(this, contact)) {
while (_iter.hasNext(this, contact)) {
if (contact.type != ADV_TYPE_NONE) {
found = true;
break;
}
}
if (found) {
if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter
writeContactRespFrame(RESP_CODE_CONTACT, contact); writeContactRespFrame(RESP_CODE_CONTACT, contact);
if (contact.lastmod > _most_recent_lastmod) { if (contact.lastmod > _most_recent_lastmod) {
+8 -3
View File
@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 13 #define FIRMWARE_VER_CODE 13
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Jun 2026" #define FIRMWARE_BUILD_DATE "9 Aug 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0" #define FIRMWARE_VERSION "v1.17.0"
#endif #endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -105,6 +105,7 @@ public:
protected: protected:
float getAirtimeBudgetFactor() const override; float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override; int getInterferenceThreshold() const override;
bool getCADEnabled() const override;
int calcRxDelay(float score, uint32_t air_time) const override; int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
@@ -164,7 +165,11 @@ protected:
} }
public: public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } void savePrefs() {
_prefs.node_lat = sensors.node_lat;
_prefs.node_lon = sensors.node_lon;
_store->savePrefs(_prefs);
}
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() { void applyGpsPrefs() {
+124 -23
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint> // For uint8_t, uint32_t #include <cstdint> // For uint8_t, uint32_t
#include <helpers/ConfigSerializer.h>
#define TELEM_MODE_DENY 0 #define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
@@ -8,30 +9,130 @@
#define ADVERT_LOC_NONE 0 #define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_SHARE 1
struct NodePrefs { // persisted to file class NodePrefs : public ConfigSerializer { // persisted to file
float airtime_factor; public:
float airtime_factor = 0;
char node_name[32]; char node_name[32];
float freq; double node_lat = 0, node_lon = 0;
uint8_t sf; float freq = 0;
uint8_t cr; uint8_t sf = 0;
uint8_t multi_acks; uint8_t cr = 0;
uint8_t manual_add_contacts; uint8_t multi_acks = 0;
float bw; uint8_t manual_add_contacts = 0;
int8_t tx_power_dbm; float bw = 0;
uint8_t telemetry_mode_base; int8_t tx_power_dbm = 0;
uint8_t telemetry_mode_loc; uint8_t telemetry_mode_base = 0;
uint8_t telemetry_mode_env; uint8_t telemetry_mode_loc = 0;
float rx_delay_base; uint8_t telemetry_mode_env = 0;
uint32_t ble_pin; float rx_delay_base = 0;
uint8_t advert_loc_policy; uint32_t ble_pin = 0;
uint8_t buzzer_quiet; uint8_t advert_loc_policy = 0;
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) uint8_t buzzer_quiet = 0;
uint32_t gps_interval; // GPS read interval in seconds uint8_t vibe_quiet = 0;
uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) uint32_t gps_interval = 0; // GPS read interval in seconds
uint8_t client_repeat; uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
uint8_t path_hash_mode; // which path mode to use when sending uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
uint8_t path_hash_mode = 0; // which path mode to use when sending
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31]; char default_scope_name[31];
uint8_t default_scope_key[16]; uint8_t default_scope_key[16];
private:
class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
NodePrefs* _parent;
protected:
void structure() override {
def("freq", _parent->freq);
def("bw", _parent->bw);
def("sf", _parent->sf);
def("cr", _parent->cr);
//def("cad", _parent->cad_enabled);
//def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->rx_boosted_gain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
//def("f_txdelay", _parent->tx_delay_factor); currently hard-coded
//def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded
//def("agc_int", _parent->agc_reset_interval);
def("hash_mode", _parent->path_hash_mode);
def("multi_ack", _parent->multi_acks);
}
public:
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
};
RadioPrefs radio;
class GPSPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
NodePrefs* _parent;
protected:
void structure() override {
def("en", _parent->gps_enabled); // boolean
def("int", _parent->gps_interval); // interval in seconds
def("adv_loc", _parent->advert_loc_policy);
}
public:
GPSPrefs(NodePrefs* parent) : _parent(parent) { }
};
GPSPrefs gps;
class RepeatPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
public:
uint8_t disable_fwd = 1;
protected:
void structure() override {
def("disable", disable_fwd);
//def("f_max", flood_max);
//def("f_max_uns", flood_max_unscoped);
//def("f_max_adv", flood_max_advert);
//def("loop", loop_detect);
}
};
RepeatPrefs repeat;
class CompanionPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("auto_max", _parent->autoadd_max_hops); // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
def("defs_nm", _parent->default_scope_name, sizeof(_parent->default_scope_name));
def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key));
def("pin", _parent->ble_pin);
def("buzz_q", _parent->buzzer_quiet);
def("vibe_q", _parent->vibe_quiet);
def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config
def("man_add", _parent->manual_add_contacts);
def("tel_base", _parent->telemetry_mode_base);
def("tel_loc", _parent->telemetry_mode_loc);
def("tel_env", _parent->telemetry_mode_env);
}
public:
CompanionPrefs(NodePrefs* parent) : _parent(parent) { }
};
CompanionPrefs companion;
protected:
void structure() override {
def("name", node_name, sizeof(node_name));
//def("adv_int", advert_interval);
//def("f_adv_int", flood_advert_interval);
def("lat", node_lat);
def("lon", node_lon);
def("radio", radio);
def("gps", gps);
def("repeat", repeat);
def("comp", companion);
}
public:
NodePrefs() : radio(this), gps(this), companion(this) {
node_name[0] = 0;
default_scope_name[0] = 0;
memset(default_scope_key, 0, sizeof(default_scope_key));
}
// new accessor methods
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
}; };
+102 -96
View File
@@ -12,19 +12,72 @@ static uint32_t _atoi(const char* sp) {
return n; return n;
} }
// interface manager
#include <helpers/MultiSerialInterface.h>
MultiSerialInterface interface_manager;
// include bluetooth interface
#if defined(BLE_PIN_CODE)
#ifdef ESP32
// include esp32 bluetooth interface
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface bluetooth_interface;
#elif defined(NRF52_PLATFORM)
// include nrf52 bluetooth interface
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface bluetooth_interface;
#else
#error "SerialBLEInterface is not defined for this platform"
#endif
#endif
// include wifi interface
#ifdef WIFI_SSID
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifdef ESP32
// include esp32 wifi interface
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface wifi_interface;
#else
#error "SerialWifiInterface is not defined for this platform"
#endif
#endif
// include usb interface
#if defined(ENABLE_USB_INTERFACE)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface usb_serial_interface;
#endif
// include ethernet interface
#if defined(ETHERNET_ENABLED)
#include <helpers/ethernet/EthernetInterface.h>
ETHERNET_CLASS ethernet_interface;
#endif
// include hardware serial interface
#if defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface hardware_serial_interface;
HardwareSerial companion_serial(1);
#endif
// platform file system
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h> #include <InternalFileSystem.h>
#if defined(QSPIFLASH) #if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h> #include <CustomLFS_QSPIFlash.h>
DataStore store(InternalFS, QSPIFlash, rtc_clock); DataStore store(InternalFS, QSPIFlash, rtc_clock);
#else #else
#if defined(EXTRAFS) #if defined(EXTRAFS)
#include <CustomLFS.h> #include <CustomLFS.h>
CustomLFS ExtraFS(0xD4000, 0x19000, 128); CustomLFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock); DataStore store(InternalFS, ExtraFS, rtc_clock);
#else #else
DataStore store(InternalFS, rtc_clock); DataStore store(InternalFS, rtc_clock);
#endif #endif
#endif #endif
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
#include <LittleFS.h> #include <LittleFS.h>
@@ -34,61 +87,10 @@ static uint32_t _atoi(const char* sp) {
DataStore store(SPIFFS, rtc_clock); DataStore store(SPIFFS, rtc_clock);
#endif #endif
#ifdef ESP32
#ifdef WIFI_SSID
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface serial_interface;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#elif defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
HardwareSerial companion_serial(1);
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(RP2040_PLATFORM)
//#ifdef WIFI_SSID
// #include <helpers/rp2040/SerialWifiInterface.h>
// SerialWifiInterface serial_interface;
// #ifndef TCP_PORT
// #define TCP_PORT 5000
// #endif
// #elif defined(BLE_PIN_CODE)
// #include <helpers/rp2040/SerialBLEInterface.h>
// SerialBLEInterface serial_interface;
#if defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
HardwareSerial companion_serial(1);
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(NRF52_PLATFORM)
#ifdef BLE_PIN_CODE
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#endif
#elif defined(STM32_PLATFORM)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
#else
#error "need to define a serial interface"
#endif
/* GLOBAL OBJECTS */ /* GLOBAL OBJECTS */
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
#include "UITask.h" #include "UITask.h"
UITask ui_task(&board, &serial_interface); UITask ui_task(&board, &interface_manager);
#endif #endif
StdRNG fast_rng; StdRNG fast_rng;
@@ -113,9 +115,12 @@ void halt() {
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
board.begin(); board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
DisplayDriver* disp = NULL; DisplayDriver* disp = NULL;
if (display.begin()) { if (display.begin()) {
@@ -155,13 +160,6 @@ void setup() {
false false
#endif #endif
); );
#ifdef BLE_PIN_CODE
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
LittleFS.begin(); LittleFS.begin();
store.begin(); store.begin();
@@ -172,22 +170,6 @@ void setup() {
false false
#endif #endif
); );
//#ifdef WIFI_SSID
// WiFi.begin(WIFI_SSID, WIFI_PWD);
// serial_interface.begin(TCP_PORT);
// #elif defined(BLE_PIN_CODE)
// char dev_name[32+16];
// sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
// serial_interface.begin(dev_name, the_mesh.getBLEPin());
#if defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
serial_interface.begin(companion_serial);
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#elif defined(ESP32) #elif defined(ESP32)
SPIFFS.begin(true); SPIFFS.begin(true);
store.begin(); store.begin();
@@ -198,7 +180,17 @@ void setup() {
false false
#endif #endif
); );
#else
#error "need to define filesystem"
#endif
// add bluetooth interface
#if defined(BLE_PIN_CODE)
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface);
#endif
// add wifi interface
#ifdef WIFI_SSID #ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true); WiFi.setAutoReconnect(true);
@@ -214,21 +206,31 @@ void setup() {
}); });
WiFi.begin(WIFI_SSID, WIFI_PWD); WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT); wifi_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE) interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
serial_interface.begin(companion_serial);
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#else
#error "need to define filesystem"
#endif #endif
// add usb interface
#if defined(ENABLE_USB_INTERFACE)
usb_serial_interface.begin(Serial);
interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface);
#endif
// add ethernet interface
#if defined(ETHERNET_ENABLED)
ethernet_interface.begin();
interface_manager.addInterface(InterfaceType::Ethernet, &ethernet_interface);
#endif
// add hardware serial interface
#if defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
hardware_serial_interface.begin(companion_serial);
interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface);
#endif
the_mesh.startInterface(interface_manager);
sensors.begin(); sensors.begin();
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
@@ -244,11 +246,15 @@ void setup() {
void loop() { void loop() {
the_mesh.loop(); the_mesh.loop();
interface_manager.loop();
sensors.loop(); sensors.loop();
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
ui_task.loop(); ui_task.loop();
#endif #endif
rtc_clock.tick(); rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
if (!the_mesh.hasPendingWork()) { if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM)
+62 -30
View File
@@ -53,23 +53,24 @@ public:
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
// meshcore logo // meshcore logo
display.setColor(DisplayDriver::BLUE); display.setColor(UIColor::corp_blue);
int logoWidth = 128; int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website // meshcore website
const char* website = "https://meshcore.io"; const char* website = "https://meshcore.io";
display.setColor(DisplayDriver::LIGHT); display.setColor(UIColor::primary_txt);
display.setTextSize(1); display.setTextSize(1);
uint16_t websiteWidth = display.getTextWidth(website); uint16_t websiteWidth = display.getTextWidth(website);
display.setCursor((display.width() - websiteWidth) / 2, 22); display.setCursor((display.width() - websiteWidth) / 2, 22);
display.print(website); display.print(website);
// version info // version info
display.setColor(DisplayDriver::LIGHT); display.setColor(UIColor::primary_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width()/2, 35, _version_info); display.drawTextCentered(display.width()/2, 35, _version_info);
display.setColor(UIColor::secondary_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE); display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE);
@@ -128,7 +129,7 @@ class HomeScreen : public UIScreen {
int iconHeight = 10; int iconHeight = 10;
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0; int iconY = 0;
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::title_txt);
// battery outline // battery outline
display.drawRect(iconX, iconY, iconWidth, iconHeight); display.drawRect(iconX, iconY, iconWidth, iconHeight);
@@ -143,7 +144,7 @@ class HomeScreen : public UIScreen {
// show muted icon if buzzer is muted // show muted icon if buzzer is muted
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) { if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::RED); display.setColor(UIColor::warning_txt);
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
} }
#endif #endif
@@ -188,34 +189,41 @@ public:
} }
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
display.setColor(UIColor::title_bkg);
display.fillRect(0, 0, display.width(), 12);
char tmp[80]; char tmp[80];
// node name // node name
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::title_txt);
char filtered_name[sizeof(_node_prefs->node_name)]; char filtered_name[sizeof(_node_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
display.setCursor(0, 0); display.setCursor(0, 2);
display.print(filtered_name); display.print(filtered_name);
// battery voltage // battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts()); renderBatteryIndicator(display, _task->getBattMilliVolts());
// curr page indicator // curr page indicator
if (UIColor::title_bkg == UIColor::window_bkg) {
display.setColor(UIColor::title_txt);
} else {
display.setColor(UIColor::title_bkg);
}
int y = 14; int y = 14;
int x = display.width() / 2 - 5 * (HomePage::Count-1); int x = display.width() / 2 - 5 * (HomePage::Count-1);
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
if (i == _page) { if (i == _page) {
display.fillRect(x-1, y-1, 3, 3); display.fillRect(x-1, y-1, 4, 4);
} else { } else {
display.fillRect(x, y, 1, 1); display.fillRect(x, y, 2, 2);
} }
} }
if (_page == HomePage::FIRST) { if (_page == HomePage::FIRST) {
display.setColor(DisplayDriver::YELLOW); display.setColor(UIColor::primary_txt);
display.setTextSize(2); display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount()); sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 20, tmp); display.drawTextCentered(display.width() / 2, 22, tmp);
#ifdef WIFI_SSID #ifdef WIFI_SSID
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
@@ -224,19 +232,19 @@ public:
display.drawTextCentered(display.width() / 2, 54, tmp); display.drawTextCentered(display.width() / 2, 54, tmp);
#endif #endif
if (_task->hasConnection()) { if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::warning_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 43, "< Connected >"); display.drawTextCentered(display.width() / 2, 43, "< Connected >");
} else if (the_mesh.getBLEPin() != 0) { // BT pin } else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(DisplayDriver::RED); display.setColor(UIColor::warning_txt);
display.setTextSize(2); display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, 43, tmp); display.drawTextCentered(display.width() / 2, 43, tmp);
} }
} else if (_page == HomePage::RECENT) { } else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::primary_txt);
int y = 20; int y = 20;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i]; auto a = &recent[i];
@@ -260,7 +268,7 @@ public:
display.print(tmp); display.print(tmp);
} }
} else if (_page == HomePage::RADIO) { } else if (_page == HomePage::RADIO) {
display.setColor(DisplayDriver::YELLOW); display.setColor(UIColor::primary_txt);
display.setTextSize(1); display.setTextSize(1);
// freq / sf // freq / sf
display.setCursor(0, 20); display.setCursor(0, 20);
@@ -279,15 +287,17 @@ public:
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
display.print(tmp); display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) { } else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
display.drawXbm((display.width() - 32) / 2, 18, display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off,
32, 32); 32, 32);
display.setColor(UIColor::secondary_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) { } else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
display.setColor(UIColor::secondary_txt);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) { } else if (_page == HomePage::GPS) {
@@ -305,24 +315,33 @@ public:
#else #else
strcpy(buf, gps_state ? "gps on" : "gps off"); strcpy(buf, gps_state ? "gps on" : "gps off");
#endif #endif
display.setColor(UIColor::primary_txt);
display.drawTextLeftAlign(0, y, buf); display.drawTextLeftAlign(0, y, buf);
if (nmea == NULL) { if (nmea == NULL) {
y = y + 12; y = y + 12;
display.setColor(UIColor::secondary_txt);
display.drawTextLeftAlign(0, y, "Can't access GPS"); display.drawTextLeftAlign(0, y, "Can't access GPS");
} else { } else {
display.setColor(UIColor::primary_txt);
strcpy(buf, nmea->isValid()?"fix":"no fix"); strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y = y + 12;
display.setColor(UIColor::secondary_txt);
display.drawTextLeftAlign(0, y, "sat"); display.drawTextLeftAlign(0, y, "sat");
display.setColor(UIColor::primary_txt);
sprintf(buf, "%d", nmea->satellitesCount()); sprintf(buf, "%d", nmea->satellitesCount());
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y = y + 12;
display.setColor(UIColor::secondary_txt);
display.drawTextLeftAlign(0, y, "pos"); display.drawTextLeftAlign(0, y, "pos");
display.setColor(UIColor::primary_txt);
sprintf(buf, "%.4f %.4f", sprintf(buf, "%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y = y + 12;
display.setColor(UIColor::secondary_txt);
display.drawTextLeftAlign(0, y, "alt"); display.drawTextLeftAlign(0, y, "alt");
display.setColor(UIColor::primary_txt);
sprintf(buf, "%.2f", nmea->getAltitude()/1000.); sprintf(buf, "%.2f", nmea->getAltitude()/1000.);
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y = y + 12;
@@ -390,7 +409,9 @@ public:
strcpy(name, "unk"); sprintf(buf, ""); strcpy(name, "unk"); sprintf(buf, "");
} }
display.setCursor(0, y); display.setCursor(0, y);
display.setColor(UIColor::secondary_txt);
display.print(name); display.print(name);
display.setColor(UIColor::primary_txt);
display.setCursor( display.setCursor(
display.width()-display.getTextWidth(buf)-1, y display.width()-display.getTextWidth(buf)-1, y
); );
@@ -401,11 +422,13 @@ public:
else sensors_scroll_offset = 0; else sensors_scroll_offset = 0;
#endif #endif
} else if (_page == HomePage::SHUTDOWN) { } else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
display.setTextSize(1); display.setTextSize(1);
if (_shutdown_init) { if (_shutdown_init) {
display.setColor(UIColor::warning_txt);
display.drawTextCentered(display.width() / 2, 34, "hibernating..."); display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else { } else {
display.setColor(UIColor::secondary_txt);
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
} }
@@ -426,10 +449,10 @@ public:
return true; return true;
} }
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off
_task->disableSerial(); _task->disableBluetooth();
} else { } else {
_task->enableSerial(); _task->enableBluetooth();
} }
return true; return true;
} }
@@ -498,7 +521,7 @@ public:
char tmp[16]; char tmp[16];
display.setCursor(0, 0); display.setCursor(0, 0);
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
sprintf(tmp, "Unread: %d", num_unread); sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp); display.print(tmp);
@@ -518,13 +541,13 @@ public:
display.drawRect(0, 11, display.width(), 1); // horiz line display.drawRect(0, 11, display.width(), 1); // horiz line
display.setCursor(0, 14); display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW); display.setColor(UIColor::secondary_txt);
char filtered_origin[sizeof(p->origin)]; char filtered_origin[sizeof(p->origin)];
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin)); display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
display.print(filtered_origin); display.print(filtered_origin);
display.setCursor(0, 25); display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT); display.setColor(UIColor::primary_txt);
char filtered_msg[sizeof(p->msg)]; char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg)); display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width()); display.printWordWrap(filtered_msg, display.width());
@@ -697,8 +720,7 @@ void UITask::shutdown(bool restart){
if (restart) { if (restart) {
_board->reboot(); _board->reboot();
} else { } else {
_display->turnOff(); // Power off board including radio, display, GPS and components
radio_driver.powerOff();
_board->powerOff(); _board->powerOff();
} }
} }
@@ -748,6 +770,16 @@ void UITask::loop() {
c = handleTripleClick(KEY_SELECT); c = handleTripleClick(KEY_SELECT);
} }
#endif #endif
#if defined(UI_HAS_ROTARY_INPUT)
RotaryInputEvent rotaryEv = rotary_input.poll();
if (c == 0 && _display != NULL && _display->isOn()) {
if (rotaryEv == RotaryInputEvent::Next) {
c = KEY_NEXT;
} else if (rotaryEv == RotaryInputEvent::Prev) {
c = KEY_PREV;
}
}
#endif
#if defined(PIN_USER_BTN_ANA) #if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) { if (abs(millis() - _analogue_pin_read_millis) > 10) {
int ev = analog_btn.check(); int ev = analog_btn.check();
@@ -797,9 +829,9 @@ void UITask::loop() {
_display->setTextSize(1); _display->setTextSize(1);
int y = _display->height() / 3; int y = _display->height() / 3;
int p = _display->height() / 32; int p = _display->height() / 32;
_display->setColor(DisplayDriver::DARK); _display->setColor(UIColor::popup_bkg);
_display->fillRect(p, y, _display->width() - p*2, y); _display->fillRect(p, y, _display->width() - p*2, y);
_display->setColor(DisplayDriver::LIGHT); // draw box border _display->setColor(UIColor::popup_txt); // draw box border
_display->drawRect(p, y, _display->width() - p*2, y); _display->drawRect(p, y, _display->width() - p*2, y);
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed _next_refresh = _alert_expiry; // will need refresh when alert is dismissed
@@ -836,7 +868,7 @@ void UITask::loop() {
if (_display != NULL) { if (_display != NULL) {
_display->startFrame(); _display->startFrame();
_display->setTextSize(2); _display->setTextSize(2);
_display->setColor(DisplayDriver::RED); _display->setColor(UIColor::warning_txt);
_display->drawTextCentered(_display->width() / 2, 20, "Low Battery."); _display->drawTextCentered(_display->width() / 2, 20, "Low Battery.");
_display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!"); _display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!");
_display->endFrame(); _display->endFrame();
+2 -2
View File
@@ -4,7 +4,7 @@
#include <helpers/ui/DisplayDriver.h> #include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h> #include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h> #include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h> #include <helpers/MultiSerialInterface.h>
#include <Arduino.h> #include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h> #include <helpers/sensors/LPPDataHelpers.h>
@@ -65,7 +65,7 @@ class UITask : public AbstractUITask {
public: public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
next_batt_chck = _next_refresh = 0; next_batt_chck = _next_refresh = 0;
ui_started_at = 0; ui_started_at = 0;
curr = NULL; curr = NULL;
+159 -20
View File
@@ -6,12 +6,19 @@
#define AUTO_OFF_MILLIS 15000 // 15 seconds #define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds #define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED #if defined(PIN_STATUS_LED) || defined(PIN_STATUS_LED_R)
#define LED_ON_MILLIS 20 #define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200 #define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000 #define LED_CYCLE_MILLIS 4000
#endif #endif
#if defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STATUS_LED_B)
#define STATUS_LED_RGB 1
#ifndef LOW_BATT_MILLIVOLTS
#define LOW_BATT_MILLIVOLTS 3500
#endif
#endif
#ifndef USER_BTN_PRESSED #ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW #define USER_BTN_PRESSED LOW
#endif #endif
@@ -60,6 +67,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
buzzer.startup(); buzzer.startup();
#endif #endif
#ifdef HAS_DRV2605
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
// Initialize digital button if available // Initialize digital button if available
#ifdef PIN_USER_BTN #ifdef PIN_USER_BTN
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED); _userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED);
@@ -130,6 +142,10 @@ void UITask::clearMsgPreview() {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount; _msgcount = msgcount;
#ifdef HAS_DRV2605
vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown)
#endif
if (path_len == 0xFF) { if (path_len == 0xFF) {
sprintf(_origin, "(F) %s", from_name); sprintf(_origin, "(F) %s", from_name);
} else { } else {
@@ -167,7 +183,7 @@ void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
int iconHeight = 12; int iconHeight = 12;
int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0; int iconY = 0;
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
// battery outline // battery outline
_display->drawRect(iconX, iconY, iconWidth, iconHeight); _display->drawRect(iconX, iconY, iconWidth, iconHeight);
@@ -188,7 +204,7 @@ void UITask::renderCurrScreen() {
_display->setTextSize(1.4); _display->setTextSize(1.4);
uint16_t textWidth = _display->getTextWidth(_alert); uint16_t textWidth = _display->getTextWidth(_alert);
_display->setCursor((_display->width() - textWidth) / 2, 22); _display->setCursor((_display->width() - textWidth) / 2, 22);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::warning_txt);
_display->print(_alert); _display->print(_alert);
_alert[0] = 0; _alert[0] = 0;
_need_refresh = true; _need_refresh = true;
@@ -197,30 +213,29 @@ void UITask::renderCurrScreen() {
// render message preview // render message preview
_display->setCursor(0, 0); _display->setCursor(0, 0);
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name); _display->print(_node_prefs->node_name);
_display->setCursor(0, 12); _display->setCursor(0, 12);
_display->setColor(DisplayDriver::YELLOW); _display->setColor(UIColor::secondary_txt);
_display->print(_origin); _display->print(_origin);
_display->setCursor(0, 24); _display->setCursor(0, 24);
_display->setColor(DisplayDriver::LIGHT);
_display->print(_msg); _display->print(_msg);
_display->setCursor(_display->width() - 28, 9); _display->setCursor(_display->width() - 28, 9);
_display->setTextSize(2); _display->setTextSize(2);
_display->setColor(DisplayDriver::ORANGE); _display->setColor(UIColor::primary_txt);
sprintf(tmp, "%d", _msgcount); sprintf(tmp, "%d", _msgcount);
_display->print(tmp); _display->print(tmp);
_display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114 _display->setColor(UIColor::secondary_txt); // last color will be kept on T114
} else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen } else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo // meshcore logo
_display->setColor(DisplayDriver::BLUE); _display->setColor(UIColor::corp_blue);
int logoWidth = 128; int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info // version info
_display->setColor(DisplayDriver::LIGHT); _display->setColor(UIColor::primary_txt);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t textWidth = _display->getTextWidth(_version_info); uint16_t textWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - textWidth) / 2, 22); _display->setCursor((_display->width() - textWidth) / 2, 22);
@@ -229,7 +244,7 @@ void UITask::renderCurrScreen() {
// node name // node name
_display->setCursor(0, 0); _display->setCursor(0, 0);
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name); _display->print(_node_prefs->node_name);
// battery voltage // battery voltage
@@ -237,7 +252,7 @@ void UITask::renderCurrScreen() {
// freq / sf // freq / sf
_display->setCursor(0, 20); _display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW); _display->setColor(UIColor::secondary_txt);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp); _display->print(tmp);
@@ -248,21 +263,127 @@ void UITask::renderCurrScreen() {
// BT pin // BT pin
if (!_connected && the_mesh.getBLEPin() != 0) { if (!_connected && the_mesh.getBLEPin() != 0) {
_display->setColor(DisplayDriver::RED); _display->setColor(UIColor::warning_txt);
_display->setTextSize(2); _display->setTextSize(2);
_display->setCursor(0, 43); _display->setCursor(0, 43);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
_display->print(tmp); _display->print(tmp);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
} else { } else {
_display->setColor(DisplayDriver::LIGHT); _display->setColor(UIColor::primary_txt);
} }
} }
_need_refresh = false; _need_refresh = false;
} }
#ifdef STATUS_LED_RGB
static void statusLedWrite(uint8_t r, uint8_t g, uint8_t b) {
analogWrite(PIN_STATUS_LED_R, r);
analogWrite(PIN_STATUS_LED_G, g);
analogWrite(PIN_STATUS_LED_B, b);
}
static void statusLedWheel(uint8_t pos) { // smooth hue sweep, 0..255
if (pos < 85) {
statusLedWrite(255 - pos * 3, pos * 3, 0);
} else if (pos < 170) {
pos -= 85;
statusLedWrite(0, 255 - pos * 3, pos * 3);
} else {
pos -= 170;
statusLedWrite(pos * 3, 0, 255 - pos * 3);
}
}
#endif
void UITask::userLedHandler() { void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED #ifdef STATUS_LED_RGB
static bool booted = false;
static bool flourish_active = false;
static unsigned long flourish_until = 0;
static int prev_msgcount = 0;
static int state = 0;
static unsigned long next_change = 0;
static int last_increment = 0;
static unsigned long next_batt_check = 0;
static bool low_batt = false;
unsigned long cur_time = millis();
if (!booted) { // color sweep on boot
booted = true;
flourish_until = cur_time + 1200;
flourish_active = true;
}
if (_msgcount > prev_msgcount) { // color sweep when a new message arrives
flourish_until = cur_time + 800;
flourish_active = true;
}
prev_msgcount = _msgcount;
if (flourish_active) {
if (cur_time < flourish_until) {
statusLedWheel((cur_time % 1200) * 255 / 1200);
return;
}
statusLedWrite(0, 0, 0);
flourish_active = false;
state = 0;
next_change = cur_time;
}
if (cur_time > next_batt_check) { // battery reads are not free, keep them rare
low_batt = _board->getBattMilliVolts() < LOW_BATT_MILLIVOLTS;
next_batt_check = cur_time + 60000;
}
#ifdef EXT_CHRG_DETECT
// charge status display while docked (skipped when messages are waiting,
// so the unread indication is never masked)
static unsigned long next_pwr_check = 0;
static bool ext_powered = false, ext_charging = false;
if (cur_time > next_pwr_check) {
ext_powered = _board->isExternalPowered();
ext_charging = ext_powered && digitalRead(EXT_CHRG_DETECT) == LOW;
next_pwr_check = cur_time + 1000;
}
if (ext_powered && _msgcount == 0) {
if (ext_charging) {
// amber breathing while charging
int ph = cur_time % 2000;
int v = ph < 1000 ? ph : 2000 - ph;
uint8_t lvl = (uint8_t)(v * 255 / 1000);
statusLedWrite(lvl, (uint8_t)(lvl * 35 / 100), 0);
} else {
statusLedWrite(0, 40, 0); // dim solid green: charge complete
}
state = 0;
next_change = cur_time;
return;
}
#endif
if (cur_time > next_change) {
if (state == 0) {
state = 1;
last_increment = (_msgcount > 0) ? LED_ON_MSG_MILLIS : LED_ON_MILLIS;
next_change = cur_time + last_increment;
if (low_batt) {
statusLedWrite(255, 0, 0); // red: battery low
} else if (_msgcount > 0) {
statusLedWrite(255, 90, 0); // amber: unread messages
} else if (_connected) {
statusLedWrite(0, 0, 255); // blue: app connected
} else {
statusLedWrite(0, 255, 0); // green: heartbeat
}
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
statusLedWrite(0, 0, 0);
}
}
#elif defined(PIN_STATUS_LED)
static int state = 0; static int state = 0;
static int next_change = 0; static int next_change = 0;
static int last_increment = 0; static int last_increment = 0;
@@ -307,7 +428,7 @@ void UITask::shutdown(bool restart){
if (restart) { if (restart) {
_board->reboot(); _board->reboot();
} else { } else {
radio_driver.powerOff(); // Power off board including radio, display, GPS and components
_board->powerOff(); _board->powerOff();
} }
} }
@@ -407,8 +528,26 @@ void UITask::handleButtonDoublePress() {
void UITask::handleButtonTriplePress() { void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered"); MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode #if defined(PIN_BUZZER) && defined(HAS_DRV2605)
#ifdef PIN_BUZZER // cycle alert modes: Buzz+Vibe -> Buzz only -> Vibe only -> Silent
int mode = (_node_prefs->buzzer_quiet ? 2 : 0) | (_node_prefs->vibe_quiet ? 1 : 0);
mode = (mode + 1) & 3;
_node_prefs->buzzer_quiet = (mode & 2) ? 1 : 0;
_node_prefs->vibe_quiet = (mode & 1) ? 1 : 0;
buzzer.quiet(_node_prefs->buzzer_quiet);
vibration.quiet(_node_prefs->vibe_quiet);
// audible/tactile confirmation of the new mode (no screen on some boards)
if (!_node_prefs->buzzer_quiet) notify(UIEventType::ack);
if (!_node_prefs->vibe_quiet) vibration.trigger(true);
switch (mode) {
case 0: sprintf(_alert, "Alerts: Buzz+Vibe"); break;
case 1: sprintf(_alert, "Alerts: Buzz only"); break;
case 2: sprintf(_alert, "Alerts: Vibe only"); break;
default: sprintf(_alert, "Alerts: Silent"); break;
}
the_mesh.savePrefs();
_need_refresh = true;
#elif defined(PIN_BUZZER)
if (buzzer.isQuiet()) { if (buzzer.isQuiet()) {
buzzer.quiet(false); buzzer.quiet(false);
notify(UIEventType::ack); notify(UIEventType::ack);
@@ -420,7 +559,7 @@ void UITask::handleButtonTriplePress() {
_node_prefs->buzzer_quiet = buzzer.isQuiet(); _node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs(); the_mesh.savePrefs();
_need_refresh = true; _need_refresh = true;
#endif #endif
} }
void UITask::handleButtonQuadruplePress() { void UITask::handleButtonQuadruplePress() {
+8 -1
View File
@@ -14,11 +14,18 @@
#include "Button.h" #include "Button.h"
#ifdef HAS_DRV2605
#include <helpers/ui/DRV2605Vibration.h>
#endif
class UITask : public AbstractUITask { class UITask : public AbstractUITask {
DisplayDriver* _display; DisplayDriver* _display;
SensorManager* _sensors; SensorManager* _sensors;
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
genericBuzzer buzzer; genericBuzzer buzzer;
#endif
#ifdef HAS_DRV2605
DRV2605Vibration vibration;
#endif #endif
unsigned long _next_refresh, _auto_off; unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs; NodePrefs* _node_prefs;
@@ -54,7 +61,7 @@ class UITask : public AbstractUITask {
public: public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
_next_refresh = 0; _next_refresh = 0;
ui_started_at = 0; ui_started_at = 0;
} }
@@ -104,7 +104,7 @@ public:
if (_status[0] == 0) return; if (_status[0] == 0) return;
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::primary_txt);
// if (_needs_redraw) { // if (_needs_redraw) {
// _text_width = display.getTextWidth(_status); // _text_width = display.getTextWidth(_status);
+24 -20
View File
@@ -56,24 +56,24 @@ public:
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
if (millis() < version_after) { if (millis() < version_after) {
// meshcore logo // meshcore logo
display.setColor(DisplayDriver::BLUE); display.setColor(UIColor::corp_blue);
int logoWidth = 72; int logoWidth = 72;
display.drawXbm(0, 0, meshcore_logo, 72, 36); display.drawXbm(0, 0, meshcore_logo, 72, 36);
} else { } else {
// meshcore website // meshcore website
const char* website = "meshcore.io"; const char* website = "meshcore.io";
display.setColor(DisplayDriver::LIGHT); display.setColor(UIColor::primary_txt);
display.setTextSize(1); display.setTextSize(1);
uint16_t websiteWidth = display.getTextWidth(website); uint16_t websiteWidth = display.getTextWidth(website);
display.setCursor((display.width() - websiteWidth) / 2, 9); display.setCursor((display.width() - websiteWidth) / 2, 9);
display.print(website); display.print(website);
// version info // version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width()/2, 18, _version_info); display.drawTextCentered(display.width()/2, 18, _version_info);
display.setColor(UIColor::secondary_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE); display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE);
} }
@@ -163,7 +163,7 @@ public:
// display.print(filtered_name); // display.print(filtered_name);
display.setColor(DisplayDriver::YELLOW); display.setColor(UIColor::primary_txt);
display.setTextSize(2); display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount()); sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.setCursor(0, 10); display.setCursor(0, 10);
@@ -180,19 +180,19 @@ public:
display.drawTextCentered(display.width() / 2, 54, tmp); display.drawTextCentered(display.width() / 2, 54, tmp);
#endif #endif
if (_task->hasConnection()) { if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::warning_txt);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >"); display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >");
} else if (the_mesh.getBLEPin() != 0) { // BT pin } else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(DisplayDriver::RED); display.setColor(UIColor::warning_txt);
display.setTextSize(2); display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, display.height()-8, tmp); display.drawTextCentered(display.width() / 2, display.height()-8, tmp);
} }
} else if (_page == HomePage::RECENT) { } else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::primary_txt);
int y = 8; int y = 8;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i]; auto a = &recent[i];
@@ -216,7 +216,7 @@ public:
display.print(tmp); display.print(tmp);
} }
} else if (_page == HomePage::RADIO) { } else if (_page == HomePage::RADIO) {
display.setColor(DisplayDriver::YELLOW); display.setColor(UIColor::primary_txt);
display.setTextSize(1); display.setTextSize(1);
// frequency and spreading factor // frequency and spreading factor
display.setCursor(0, 8); display.setCursor(0, 8);
@@ -238,14 +238,14 @@ public:
display.drawTextRightAlign(display.width(), 26, tmp); display.drawTextRightAlign(display.width(), 26, tmp);
} else if (_page == HomePage::BLUETOOTH) { } else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
display.drawXbm((display.width() - 32) / 2, 8, display.drawXbm((display.width() - 32) / 2, 8,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off,
32, 32); 32, 32);
display.setTextSize(1); display.setTextSize(1);
// display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) { } else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::corp_blue);
display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32);
// display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL); // display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
@@ -264,9 +264,11 @@ public:
#else #else
strcpy(buf, gps_state ? "gps on" : "gps off"); strcpy(buf, gps_state ? "gps on" : "gps off");
#endif #endif
display.setColor(UIColor::primary_txt);
display.drawTextLeftAlign(0, y, buf); display.drawTextLeftAlign(0, y, buf);
if (nmea == NULL) { if (nmea == NULL) {
// y = y + 8; // y = y + 8;
display.setColor(UIColor::warning_txt);
display.drawTextLeftAlign(0, y, "Can't access GPS"); display.drawTextLeftAlign(0, y, "Can't access GPS");
} else { } else {
if (!gps_state || !nmea->isValid()) { if (!gps_state || !nmea->isValid()) {
@@ -274,6 +276,7 @@ public:
} else { } else {
sprintf(buf, "%d sat", nmea->satellitesCount()); sprintf(buf, "%d sat", nmea->satellitesCount());
} }
display.setColor(UIColor::primary_txt);
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 8; y = y + 8;
sprintf(buf, "lat %.4f", sprintf(buf, "lat %.4f",
@@ -349,8 +352,10 @@ public:
r.skipData(type); r.skipData(type);
strcpy(name, "unk"); sprintf(buf, ""); strcpy(name, "unk"); sprintf(buf, "");
} }
display.setColor(UIColor::secondary_txt);
display.setCursor(0, y); display.setCursor(0, y);
display.print(name); display.print(name);
display.setColor(UIColor::primary_txt);
display.setCursor( display.setCursor(
display.width()-display.getTextWidth(buf)-1, y display.width()-display.getTextWidth(buf)-1, y
); );
@@ -361,7 +366,7 @@ public:
else sensors_scroll_offset = 0; else sensors_scroll_offset = 0;
#endif #endif
} else if (_page == HomePage::SHUTDOWN) { } else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::GREEN); display.setColor(UIColor::secondary_txt);
display.setTextSize(1); display.setTextSize(1);
if (_shutdown_init) { if (_shutdown_init) {
display.drawTextCentered(display.width() / 2, 20, "hibernating..."); display.drawTextCentered(display.width() / 2, 20, "hibernating...");
@@ -386,10 +391,10 @@ public:
return true; return true;
} }
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off
_task->disableSerial(); _task->disableBluetooth();
} else { } else {
_task->enableSerial(); _task->enableBluetooth();
} }
return true; return true;
} }
@@ -566,8 +571,7 @@ void UITask::shutdown(bool restart){
if (restart) { if (restart) {
_board->reboot(); _board->reboot();
} else { } else {
_display->turnOff(); // Power off board including radio, display, GPS and components
radio_driver.powerOff();
_board->powerOff(); _board->powerOff();
} }
} }
@@ -673,7 +677,7 @@ void UITask::loop() {
_cached_batt_mv, _cached_batt_mv,
isBuzzerQuiet(), isBuzzerQuiet(),
getGPSState(), getGPSState(),
isSerialEnabled()); isBluetoothEnabled());
bool status_dirty = _statusBar.needsRedraw(); bool status_dirty = _statusBar.needsRedraw();
bool content_dirty = (millis() >= _next_refresh && curr); bool content_dirty = (millis() >= _next_refresh && curr);
@@ -693,9 +697,9 @@ void UITask::loop() {
_display->setTextSize(1); _display->setTextSize(1);
int y = _display->height() / 3; int y = _display->height() / 3;
int p = _display->height() / 32; int p = _display->height() / 32;
_display->setColor(DisplayDriver::DARK); _display->setColor(UIColor::popup_bkg);
_display->fillRect(p, y, _display->width() - p*2, y); _display->fillRect(p, y, _display->width() - p*2, y);
_display->setColor(DisplayDriver::LIGHT); // draw box border _display->setColor(UIColor::popup_txt); // draw box border
_display->drawRect(p, y, _display->width() - p*2, y); _display->drawRect(p, y, _display->width() - p*2, y);
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed _next_refresh = _alert_expiry; // will need refresh when alert is dismissed
+2 -2
View File
@@ -4,7 +4,7 @@
#include <helpers/ui/DisplayDriver.h> #include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h> #include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h> #include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h> #include <helpers/MultiSerialInterface.h>
#include <Arduino.h> #include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h> #include <helpers/sensors/LPPDataHelpers.h>
@@ -71,7 +71,7 @@ class UITask : public AbstractUITask {
public: public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
next_batt_chck = _next_refresh = 0; next_batt_chck = _next_refresh = 0;
_cached_batt_mv = 0; _cached_batt_mv = 0;
ui_started_at = 0; ui_started_at = 0;
+166 -33
View File
@@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r
_getStatsCallback = nullptr; _getStatsCallback = nullptr;
_config = {0, 0, 0, 0, 0}; _config = {0, 0, 0, 0, 0};
_signal_report_enabled = true; _signal_report_enabled = true;
resetOutputQueue();
} }
void KissModem::begin() { void KissModem::begin() {
@@ -30,37 +31,168 @@ void KissModem::begin() {
_rx_active = false; _rx_active = false;
_has_pending_tx = false; _has_pending_tx = false;
_tx_state = TX_IDLE; _tx_state = TX_IDLE;
resetOutputQueue();
} }
void KissModem::writeByte(uint8_t b) { void KissModem::resetOutputQueue() {
if (b == KISS_FEND) { _tx_frame_head = 0;
_serial.write(KISS_FESC); _tx_frame_tail = 0;
_serial.write(KISS_TFEND); _tx_frame_count = 0;
} else if (b == KISS_FESC) { _tx_busy_error_pending = false;
_serial.write(KISS_FESC); _tx_done_pending = false;
_serial.write(KISS_TFESC); _tx_done_result = 0;
} else { }
_serial.write(b);
void KissModem::popTxFrame() {
_tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
_tx_frame_count--;
}
uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) {
if (b == KISS_FEND || b == KISS_FESC) {
if (idx + 2 > max_len) {
return 0;
}
dest[idx++] = KISS_FESC;
dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC;
return idx;
} }
if (idx + 1 > max_len) {
return 0;
}
dest[idx++] = b;
return idx;
} }
void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) {
_serial.write(KISS_FEND); if (max_len < KISS_FRAME_BOUNDARY_BYTES) {
writeByte(type); return 0;
}
uint16_t idx = 0;
dest[idx++] = KISS_FEND;
idx = appendEscapedByte(dest, idx, max_len, type);
if (idx == 0) {
return 0;
}
for (uint16_t i = 0; i < len; i++) { for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]); idx = appendEscapedByte(dest, idx, max_len, data[i]);
if (idx == 0) {
return 0;
}
} }
_serial.write(KISS_FEND);
if (idx + 1 > max_len) {
return 0;
}
dest[idx++] = KISS_FEND;
return idx;
} }
void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { bool KissModem::tryFlushFrames() {
_serial.write(KISS_FEND); while (_tx_frame_count > 0) {
writeByte(KISS_CMD_SETHARDWARE); const uint8_t idx = _tx_frame_head;
writeByte(sub_cmd); const uint16_t frame_len = _tx_frame_len[idx];
for (uint16_t i = 0; i < len; i++) { uint16_t written_len = _tx_frame_written[idx];
writeByte(data[i]);
if (written_len >= frame_len) {
popTxFrame();
continue;
}
const int available = _serial.availableForWrite();
if (available <= 0) {
return false;
}
const uint16_t remaining = frame_len - written_len;
const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining;
if (chunk_len == 0) {
return false;
}
size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len);
if (chunk_written == 0) {
return false;
}
written_len += (uint16_t)chunk_written;
_tx_frame_written[idx] = written_len;
if (written_len < frame_len) {
return false;
}
popTxFrame();
} }
_serial.write(KISS_FEND); return true;
}
bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) {
if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) {
if (mark_busy_error) {
_tx_busy_error_pending = true;
}
return false;
}
const uint8_t idx = _tx_frame_tail;
uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx]));
if (frame_len == 0) {
return false;
}
_tx_frame_len[idx] = frame_len;
_tx_frame_written[idx] = 0;
_tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
_tx_frame_count++;
tryFlushFrames();
return true;
}
bool KissModem::queuePendingBusyError() {
if (!_tx_busy_error_pending) {
return true;
}
const uint8_t err = HW_ERR_TX_BUSY;
if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) {
return false;
}
_tx_busy_error_pending = false;
return true;
}
bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) {
if (len > KISS_MAX_FRAME_SIZE) {
return false;
}
_tx_hw_payload[0] = sub_cmd;
if (len > 0) {
memcpy(_tx_hw_payload + 1, data, len);
}
return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error);
}
bool KissModem::queuePendingTxDone() {
if (!_tx_done_pending) {
return true;
}
if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) {
return false;
}
_tx_done_pending = false;
return true;
}
void KissModem::setTxDonePending(uint8_t result) {
_tx_done_result = result;
_tx_done_pending = true;
_tx_state = TX_DONE_PENDING;
}
bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
return queueHardwareFrame(sub_cmd, data, len, true);
} }
void KissModem::writeHardwareError(uint8_t error_code) { void KissModem::writeHardwareError(uint8_t error_code) {
@@ -68,6 +200,8 @@ void KissModem::writeHardwareError(uint8_t error_code) {
} }
void KissModem::loop() { void KissModem::loop() {
tryFlushFrames();
while (_serial.available()) { while (_serial.available()) {
uint8_t b = _serial.read(); uint8_t b = _serial.read();
@@ -106,6 +240,8 @@ void KissModem::loop() {
} }
processTx(); processTx();
tryFlushFrames();
queuePendingBusyError();
} }
void KissModem::processFrame() { void KissModem::processFrame() {
@@ -295,10 +431,7 @@ void KissModem::processTx() {
_tx_timer = millis(); _tx_timer = millis();
_tx_state = TX_SENDING; _tx_state = TX_SENDING;
} else { } else {
uint8_t result = 0x00; setTxDonePending(0x00);
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
} }
} }
break; break;
@@ -306,14 +439,15 @@ void KissModem::processTx() {
case TX_SENDING: case TX_SENDING:
if (_radio.isSendComplete()) { if (_radio.isSendComplete()) {
_radio.onSendFinished(); _radio.onSendFinished();
uint8_t result = 0x01; setTxDonePending(0x01);
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) { } else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
_radio.onSendFinished(); _radio.onSendFinished();
uint8_t result = 0x00; setTxDonePending(0x00);
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); }
break;
case TX_DONE_PENDING:
if (queuePendingTxDone()) {
_has_pending_tx = false; _has_pending_tx = false;
_tx_state = TX_IDLE; _tx_state = TX_IDLE;
} }
@@ -322,8 +456,7 @@ void KissModem::processTx() {
} }
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
writeFrame(KISS_CMD_DATA, packet, len); if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) {
if (_signal_report_enabled) {
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
writeHardwareFrame(HW_RESP_RX_META, meta, 2); writeHardwareFrame(HW_RESP_RX_META, meta, 2);
} }
+32 -4
View File
@@ -13,6 +13,14 @@
#define KISS_MAX_FRAME_SIZE 512 #define KISS_MAX_FRAME_SIZE 512
#define KISS_MAX_PACKET_SIZE 255 #define KISS_MAX_PACKET_SIZE 255
#define KISS_FRAME_BOUNDARY_BYTES 2
#define KISS_TYPE_BYTES 1
#define KISS_HW_SUBCMD_BYTES 1
#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES)
#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES)
#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE)
#define KISS_TX_FRAME_QUEUE_DEPTH 2
#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES)
#define KISS_CMD_DATA 0x00 #define KISS_CMD_DATA 0x00
#define KISS_CMD_TXDELAY 0x01 #define KISS_CMD_TXDELAY 0x01
@@ -94,7 +102,8 @@ enum TxState {
TX_WAIT_CLEAR, TX_WAIT_CLEAR,
TX_SLOT_WAIT, TX_SLOT_WAIT,
TX_DELAY, TX_DELAY,
TX_SENDING TX_SENDING,
TX_DONE_PENDING
}; };
class KissModem { class KissModem {
@@ -130,10 +139,28 @@ class KissModem {
RadioConfig _config; RadioConfig _config;
bool _signal_report_enabled; bool _signal_report_enabled;
uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE];
uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH];
uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH];
uint8_t _tx_frame_head;
uint8_t _tx_frame_tail;
uint8_t _tx_frame_count;
bool _tx_busy_error_pending;
bool _tx_done_pending;
uint8_t _tx_done_result;
uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE];
void writeByte(uint8_t b); static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b);
void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len);
void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); void resetOutputQueue();
void popTxFrame();
bool tryFlushFrames();
bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true);
bool queuePendingBusyError();
bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error);
bool queuePendingTxDone();
void setTxDonePending(uint8_t result);
bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void writeHardwareError(uint8_t error_code); void writeHardwareError(uint8_t error_code);
void processFrame(); void processFrame();
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len); void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
@@ -182,4 +209,5 @@ public:
bool isTxBusy() const { return _tx_state != TX_IDLE; } bool isTxBusy() const { return _tx_state != TX_IDLE; }
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */ /** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; } bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; }
}; };
+7 -1
View File
@@ -20,6 +20,8 @@
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 #define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
#define AGC_RESET_INTERVAL_MS 30000 #define AGC_RESET_INTERVAL_MS 30000
#define USB_TX_TIMEOUT_MS 50
#define USB_TX_BUFFER_SIZE 1024
StdRNG rng; StdRNG rng;
mesh::LocalIdentity identity; mesh::LocalIdentity identity;
@@ -111,6 +113,10 @@ void setup() {
uint32_t start = millis(); uint32_t start = millis();
while (!Serial && millis() - start < 3000) delay(10); while (!Serial && millis() - start < 3000) delay(10);
delay(100); delay(100);
#if defined(ESP32) && ARDUINO_USB_MODE
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
#endif
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
#endif #endif
@@ -126,7 +132,7 @@ void setup() {
void loop() { void loop() {
modem->loop(); modem->loop();
if (!modem->isActuallyTransmitting()) { if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) {
if (!modem->isTxBusy()) { if (!modem->isTxBusy()) {
if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) {
radio_driver.resetAGC(); radio_driver.resetAGC();
+25 -24
View File
@@ -147,11 +147,10 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++;
reply_path_hash_size = (*data >> 6) + 1; if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding
data++;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); mesh::Packet::writePath(reply_path, data, reply_path_len);
// data += (uint8_t)reply_path_len * reply_path_hash_size; // data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
@@ -166,11 +165,10 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send
uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++;
reply_path_hash_size = (*data >> 6) + 1; if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding
data++;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); mesh::Packet::writePath(reply_path, data, reply_path_len);
// data += (uint8_t)reply_path_len * reply_path_hash_size; // data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
@@ -186,11 +184,10 @@ uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender
uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++;
reply_path_hash_size = (*data >> 6) + 1; if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding
data++;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); mesh::Packet::writePath(reply_path, data, reply_path_len);
// data += (uint8_t)reply_path_len * reply_path_hash_size; // data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
@@ -549,8 +546,7 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1); return getRNG()->nextInt(0, 5*t + 1);
} }
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) {
// just try to determine region for packet (apply later in allowPacketForward())
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
@@ -562,8 +558,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
} else { } else {
recv_pkt_region = NULL; recv_pkt_region = NULL;
} }
// do normal processing return Mesh::onRecvPacket(pkt);
return false;
} }
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
@@ -576,7 +571,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
data[len] = 0; // ensure null terminator data[len] = 0; // ensure null terminator
uint8_t reply_len; uint8_t reply_len;
reply_path_len = -1; reply_path_len = 0xFF;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood()); reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
} else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) { } else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) {
@@ -596,13 +591,12 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else if (reply_path_len < 0) { } else if (reply_path_len == 0xFF) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else { } else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63); if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
if (reply) sendDirect(reply, reply_path, path_len, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -867,13 +861,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
set_radio_at = revert_radio_at = 0; set_radio_at = revert_radio_at = 0;
_logging = false; _logging = false;
region_load_active = false; region_load_active = false;
recv_pkt_region = NULL;
#if MAX_NEIGHBOURS #if MAX_NEIGHBOURS
memset(neighbours, 0, sizeof(neighbours)); memset(neighbours, 0, sizeof(neighbours));
#endif #endif
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.tx_delay_factor = 0.5f; // was 0.25f
@@ -893,6 +887,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_unscoped = 64; _prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8; _prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
// bridge defaults // bridge defaults
_prefs.bridge_enabled = 1; // enabled _prefs.bridge_enabled = 1; // enabled
@@ -917,6 +912,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.rx_boosted_gain = 1; // enabled by default; _prefs.rx_boosted_gain = 1; // enabled by default;
#endif #endif
#endif #endif
_prefs.radio_fem_rxgain = 1;
pending_discover_tag = 0; pending_discover_tag = 0;
pending_discover_until = 0; pending_discover_until = 0;
@@ -965,6 +961,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
updateAdvertTimer(); updateAdvertTimer();
updateFloodAdvertTimer(); updateFloodAdvertTimer();
@@ -1059,9 +1056,13 @@ void MyMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm); radio_driver.setTxPower(power_dbm);
} }
#if defined(USE_SX1262) || defined(USE_SX1268) bool MyMesh::setRxBoostedGain(bool enable) {
void MyMesh::setRxBoostedGain(bool enable) { return radio_driver.setRxBoostedGainMode(enable);
radio_driver.setRxBoostedGainMode(enable); }
#if defined(USE_LR2021)
bool MyMesh::configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) {
return radio_driver.configSideDetectors(sideDetSFs, num, bw);
} }
#endif #endif
+14 -8
View File
@@ -11,6 +11,7 @@
#include <LittleFS.h> #include <LittleFS.h>
#elif defined(ESP32) #elif defined(ESP32)
#include <SPIFFS.h> #include <SPIFFS.h>
using File = fs::File;
#endif #endif
#ifdef WITH_RS232_BRIDGE #ifdef WITH_RS232_BRIDGE
@@ -69,11 +70,11 @@ struct NeighbourInfo {
}; };
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Jun 2026" #define FIRMWARE_BUILD_DATE "9 Aug 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0" #define FIRMWARE_VERSION "v1.17.0"
#endif #endif
#define FIRMWARE_ROLE "repeater" #define FIRMWARE_ROLE "repeater"
@@ -91,8 +92,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
CommonCLI _cli; CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD]; uint8_t reply_data[MAX_PACKET_PAYLOAD];
uint8_t reply_path[MAX_PATH_SIZE]; uint8_t reply_path[MAX_PATH_SIZE];
int8_t reply_path_len; uint8_t reply_path_len;
uint8_t reply_path_hash_size;
TransportKeyStore key_store; TransportKeyStore key_store;
RegionMap region_map, temp_map; RegionMap region_map, temp_map;
RegionEntry* load_stack[8]; RegionEntry* load_stack[8];
@@ -150,6 +150,9 @@ protected:
int getInterferenceThreshold() const override { int getInterferenceThreshold() const override {
return _prefs.interference_threshold; return _prefs.interference_threshold;
} }
bool getCADEnabled() const override {
return _prefs.cad_enabled;
}
int getAGCResetInterval() const override { int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
} }
@@ -163,7 +166,7 @@ protected:
} }
#endif #endif
bool filterRecvFloodPacket(mesh::Packet* pkt) override; mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override; int searchPeersByHash(const uint8_t* hash) override;
@@ -249,7 +252,10 @@ public:
// To check if there is pending work // To check if there is pending work
bool hasPendingWork() const; bool hasPendingWork() const;
#if defined(USE_SX1262) || defined(USE_SX1268) bool setRxBoostedGain(bool enable) override;
void setRxBoostedGain(bool enable) override;
#endif #if defined(USE_LR2021)
virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) override;
#endif
}; };
+56 -32
View File
@@ -1,4 +1,5 @@
#include "UITask.h" #include "UITask.h"
#include "target.h"
#include <Arduino.h> #include <Arduino.h>
#include <helpers/CommonCLI.h> #include <helpers/CommonCLI.h>
@@ -9,6 +10,8 @@
#define AUTO_OFF_MILLIS 20000 // 20 seconds #define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds
#define POWEROFF_DELAY 3000
// 'meshcore', 128x13px // 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = { static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
@@ -29,9 +32,14 @@ static const uint8_t meshcore_logo [] PROGMEM = {
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH; _prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS; _auto_off = millis() + AUTO_OFF_MILLIS;
_started_at = millis();
_node_prefs = node_prefs; _node_prefs = node_prefs;
_display->turnOn(); _display->turnOn();
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
user_btn.begin();
#endif
// strip off dash and commit hash by changing dash to null terminator // strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3 // e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version); char *version = strdup(firmware_version);
@@ -41,47 +49,56 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
} }
// v1.2.3 (1 Jan 2025) // v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date); snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
} }
void UITask::renderCurrScreen() { void UITask::renderCurrScreen() {
char tmp[80]; char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo // meshcore logo
_display->setColor(DisplayDriver::BLUE); _display->setColor(UIColor::corp_blue);
int logoWidth = 128; int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website // meshcore website
const char* website = "https://meshcore.io"; const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT); _display->setColor(UIColor::primary_txt);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website); _display->drawTextCentered(_display->width() / 2, 22, website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info // version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info); _display->drawTextCentered(_display->width() / 2, 35, _version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_display->print(_version_info);
// node type // node type
const char* node_type = "< Repeater >"; const char* node_type = "< Repeater >";
uint16_t typeWidth = _display->getTextWidth(node_type); _display->drawTextCentered(_display->width() / 2, 48, node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 48); } else if (_powering_off_at > 0) {
_display->print(node_type); // meshcore logo
} else { // home screen _display->setColor(UIColor::corp_blue);
// node name int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(UIColor::primary_txt);
_display->setTextSize(1);
_display->drawTextCentered(_display->width()/ 2, 22, website);
// Powering off
const char* poweroff_string = "Turning OFF";
uint16_t poffWidth = _display->getTextWidth(poweroff_string);
_display->setCursor((_display->width() - poffWidth) / 2, 48);
_display->drawTextCentered(_display->width()/2, 48, poweroff_string);
} else {
_display->setCursor(0, 0); _display->setCursor(0, 0);
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name); _display->print(_node_prefs->node_name);
// freq / sf // freq / sf
_display->setCursor(0, 20); _display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp); _display->print(tmp);
@@ -93,21 +110,19 @@ void UITask::renderCurrScreen() {
} }
void UITask::loop() { void UITask::loop() {
#ifdef PIN_USER_BTN #if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
if (millis() >= _next_read) { int ev = user_btn.check();
int btnState = digitalRead(PIN_USER_BTN); if (ev == BUTTON_EVENT_CLICK) {
if (btnState != _prevBtnState) { if (_display->isOn()) {
if (btnState == USER_BTN_PRESSED) { // pressed? // TODO: any action ?
if (_display->isOn()) { } else {
// TODO: any action ? _display->turnOn();
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
} }
_next_read = millis() + 200; // 5 reads per second _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_display->turnOn();
Serial.println("Powering Off");
_powering_off_at = millis() + POWEROFF_DELAY;
} }
#endif #endif
@@ -123,4 +138,13 @@ void UITask::loop() {
_display->turnOff(); _display->turnOff();
} }
} }
if (_powering_off_at > 0) { // power off timer armed
#ifdef LED_PIN
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
#endif
if (millis() > _powering_off_at) {
_board->powerOff(); // should not return
}
}
} }
+4 -1
View File
@@ -4,15 +4,18 @@
#include <helpers/CommonCLI.h> #include <helpers/CommonCLI.h>
class UITask { class UITask {
mesh::MainBoard* _board;
DisplayDriver* _display; DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off; unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState; int _prevBtnState;
NodePrefs* _node_prefs; NodePrefs* _node_prefs;
char _version_info[32]; char _version_info[32];
unsigned long _powering_off_at = 0;
unsigned long _started_at = 0;
void renderCurrScreen(); void renderCurrScreen();
public: public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop(); void loop();
+45 -2
View File
@@ -5,7 +5,12 @@
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
#include "UITask.h" #include "UITask.h"
static UITask ui_task(display); static UITask ui_task(board, display);
#endif
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Repeater CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif #endif
StdRNG fast_rng; StdRNG fast_rng;
@@ -18,6 +23,9 @@ void halt() {
} }
static char command[160]; static char command[160];
#ifdef ETHERNET_ENABLED
static char ethernet_command[160];
#endif
// For power saving // For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
@@ -33,6 +41,10 @@ void setup() {
board.begin(); board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
// give some extra time for serial to settle so // give some extra time for serial to settle so
// boot debug messages can be seen on terminal // boot debug messages can be seen on terminal
@@ -86,6 +98,9 @@ void setup() {
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0; command[0] = 0;
#ifdef ETHERNET_ENABLED
ethernet_command[0] = 0;
#endif
sensors.begin(); sensors.begin();
@@ -95,6 +110,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif #endif
#ifdef ETHERNET_ENABLED
ethernet_start_task();
#endif
// send out initial zero hop Advertisement to the mesh // send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1 #if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false); the_mesh.sendSelfAdvertisement(16000, false);
@@ -104,6 +123,7 @@ void setup() {
} }
void loop() { void loop() {
// Handle Serial CLI
int len = strlen(command); int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) { while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read(); char c = Serial.read();
@@ -122,7 +142,14 @@ void loop() {
Serial.print('\n'); Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator command[len - 1] = 0; // replace newline with C string null terminator
char reply[160]; char reply[160];
reply[0] = 0;
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleCommand(0, command, reply);
}
#else
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
#endif
if (reply[0]) { if (reply[0]) {
Serial.print(" -> "); Serial.println(reply); Serial.print(" -> "); Serial.println(reply);
} }
@@ -130,7 +157,20 @@ void loop() {
command[0] = 0; // reset command buffer command[0] = 0; // reset command buffer
} }
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) #ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
char reply[160];
reply[0] = 0;
if (!ethernet_handle_command(ethernet_command, reply)) {
the_mesh.handleCommand(0, ethernet_command, reply);
}
ethernet_send_reply(reply);
ethernet_command[0] = 0;
}
#endif
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS)
// Hold the user button to power off the SenseCAP Solar repeater. // Hold the user button to power off the SenseCAP Solar repeater.
int btnState = digitalRead(PIN_USER_BTN); int btnState = digitalRead(PIN_USER_BTN);
if (btnState == LOW) { if (btnState == LOW) {
@@ -152,6 +192,9 @@ void loop() {
#endif #endif
rtc_clock.tick(); rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
+49 -9
View File
@@ -39,18 +39,35 @@ struct ServerStats {
}; };
void MyMesh::addPost(ClientInfo *client, const char *postData) { void MyMesh::addPost(ClientInfo *client, const char *postData) {
// TODO: suggested postData format: <title>/<descrption> storePost(client->id, postData);
posts[next_post_idx].author = client->id; // add to cyclic queue }
StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); void MyMesh::addSystemPost(const char *postData) {
if (!postData || postData[0] == 0) return;
MESH_DEBUG_PRINTLN("room.post: addSystemPost: %s", postData);
storePost(self_id, postData);
}
void MyMesh::storePost(const mesh::Identity &author, const char *postData) {
int idx = next_post_idx;
// TODO: suggested postData format: <title>/<descrption>
posts[idx].author = author; // add to cyclic queue
StrHelper::strncpy(posts[idx].text, postData, MAX_POST_TEXT_LEN);
posts[idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
MESH_DEBUG_PRINTLN("room.post: storePost idx=%d text=%s", idx, posts[idx].text);
MESH_DEBUG_PRINTLN("room.post: timestamp=%u", posts[idx].post_timestamp);
next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS;
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);
_num_posted++; // stats _num_posted++; // stats
MESH_DEBUG_PRINTLN("room.post: next_post_idx=%d num_posted=%d push scheduled", next_post_idx, _num_posted);
} }
void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
MESH_DEBUG_PRINTLN("room.post: pushPostToClient text=%s", post.text);
int len = 0; int len = 0;
memcpy(&reply_data[len], &post.post_timestamp, 4); memcpy(&reply_data[len], &post.post_timestamp, 4);
len += 4; // this is a PAST timestamp... but should be accepted by client len += 4; // this is a PAST timestamp... but should be accepted by client
@@ -290,8 +307,7 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
return true; return true;
} }
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) {
// just try to determine region for packet (apply later in allowPacketForward())
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
@@ -303,8 +319,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
} else { } else {
recv_pkt_region = NULL; recv_pkt_region = NULL;
} }
// do normal processing return Mesh::onRecvPacket(pkt);
return false;
} }
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
@@ -627,9 +642,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_logging = false; _logging = false;
region_load_active = false; region_load_active = false;
set_radio_at = revert_radio_at = 0; set_radio_at = revert_radio_at = 0;
recv_pkt_region = NULL;
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0 _prefs.rx_delay_base = 0.0f; // off by default, was 10.0
_prefs.tx_delay_factor = 0.5f; // was 0.25f; _prefs.tx_delay_factor = 0.5f; // was 0.25f;
@@ -650,6 +665,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_unscoped = 64; _prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8; _prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
#ifdef ROOM_PASSWORD #ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif #endif
@@ -659,6 +675,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_interval = 0; _prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
#if defined(USE_SX1262) || defined(USE_SX1268)
#ifdef SX126X_RX_BOOSTED_GAIN
_prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN;
#else
_prefs.rx_boosted_gain = 1; // enabled by default;
#endif
#endif
_prefs.radio_fem_rxgain = 1;
next_post_idx = 0; next_post_idx = 0;
next_client_idx = 0; next_client_idx = 0;
next_push = 0; next_push = 0;
@@ -699,6 +724,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
updateAdvertTimer(); updateAdvertTimer();
updateFloodAdvertTimer(); updateFloodAdvertTimer();
@@ -805,6 +832,10 @@ void MyMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm); radio_driver.setTxPower(power_dbm);
} }
bool MyMesh::setRxBoostedGain(bool enable) {
return radio_driver.setRxBoostedGainMode(enable);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, ""); IdentityStore store(*_fs, "");
@@ -934,6 +965,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
Serial.printf("\n"); Serial.printf("\n");
} }
reply[0] = 0; reply[0] = 0;
} else if (strncmp(command, "room.post", 9) == 0) {
char* msg = command + 9;
while (*msg == ' ') msg++;
if (*msg == 0) {
snprintf(reply, MAX_POST_TEXT_LEN, "ERR empty message");
} else {
addSystemPost(msg);
snprintf(reply, MAX_POST_TEXT_LEN, "OK");
}
} else{ } else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
} }
+9 -3
View File
@@ -27,11 +27,11 @@
/* ------------------------------ Config -------------------------------- */ /* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Jun 2026" #define FIRMWARE_BUILD_DATE "9 Aug 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0" #define FIRMWARE_VERSION "v1.17.0"
#endif #endif
#ifndef LORA_FREQ #ifndef LORA_FREQ
@@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
int matching_peer_indexes[MAX_CLIENTS]; int matching_peer_indexes[MAX_CLIENTS];
void addPost(ClientInfo* client, const char* postData); void addPost(ClientInfo* client, const char* postData);
void storePost(const mesh::Identity& author, const char* postData);
void pushPostToClient(ClientInfo* client, PostInfo& post); void pushPostToClient(ClientInfo* client, PostInfo& post);
uint8_t getUnsyncedCount(ClientInfo* client); uint8_t getUnsyncedCount(ClientInfo* client);
bool processAck(const uint8_t *data); bool processAck(const uint8_t *data);
@@ -144,6 +145,9 @@ protected:
int getInterferenceThreshold() const override { int getInterferenceThreshold() const override {
return _prefs.interference_threshold; return _prefs.interference_threshold;
} }
bool getCADEnabled() const override {
return _prefs.cad_enabled;
}
int getAGCResetInterval() const override { int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
} }
@@ -151,7 +155,7 @@ protected:
return _prefs.multi_acks; return _prefs.multi_acks;
} }
bool filterRecvFloodPacket(mesh::Packet* pkt) override; mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
bool allowPacketForward(const mesh::Packet* packet) override; bool allowPacketForward(const mesh::Packet* packet) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
@@ -173,6 +177,7 @@ public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs); void begin(FILESYSTEM* fs);
void addSystemPost(const char* postData);
const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
@@ -203,6 +208,7 @@ public:
void dumpLogFile() override; void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override; void setTxPower(int8_t power_dbm) override;
bool setRxBoostedGain(bool enable) override;
void formatNeighborsReply(char *reply) override { void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported"); strcpy(reply, "not supported");
+5 -6
View File
@@ -41,27 +41,27 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
} }
// v1.2.3 (1 Jan 2025) // v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date); snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
} }
void UITask::renderCurrScreen() { void UITask::renderCurrScreen() {
char tmp[80]; char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo // meshcore logo
_display->setColor(DisplayDriver::BLUE); _display->setColor(UIColor::corp_blue);
int logoWidth = 128; int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website // meshcore website
const char* website = "https://meshcore.io"; const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT); _display->setColor(UIColor::primary_txt);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website); uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website); _display->print(website);
// version info // version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info); uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 35); _display->setCursor((_display->width() - versionWidth) / 2, 35);
@@ -76,12 +76,11 @@ void UITask::renderCurrScreen() {
// node name // node name
_display->setCursor(0, 0); _display->setCursor(0, 0);
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name); _display->print(_node_prefs->node_name);
// freq / sf // freq / sf
_display->setCursor(0, 20); _display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp); _display->print(tmp);
+42
View File
@@ -3,6 +3,11 @@
#include "MyMesh.h" #include "MyMesh.h"
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Room Server CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
#include "UITask.h" #include "UITask.h"
static UITask ui_task(display); static UITask ui_task(display);
@@ -17,6 +22,9 @@ void halt() {
} }
static char command[MAX_POST_TEXT_LEN+1]; static char command[MAX_POST_TEXT_LEN+1];
#ifdef ETHERNET_ENABLED
static char ethernet_command[MAX_POST_TEXT_LEN+1];
#endif
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
@@ -24,6 +32,10 @@ void setup() {
board.begin(); board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
if (display.begin()) { if (display.begin()) {
display.startFrame(); display.startFrame();
@@ -67,6 +79,9 @@ void setup() {
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0; command[0] = 0;
#ifdef ETHERNET_ENABLED
ethernet_command[0] = 0;
#endif
sensors.begin(); sensors.begin();
@@ -76,6 +91,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif #endif
#ifdef ETHERNET_ENABLED
ethernet_start_task();
#endif
// send out initial zero hop Advertisement to the mesh // send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1 #if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false); the_mesh.sendSelfAdvertisement(16000, false);
@@ -101,7 +120,14 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator command[len - 1] = 0; // replace newline with C string null terminator
char reply[160]; char reply[160];
reply[0] = 0;
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleCommand(0, command, reply);
}
#else
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
#endif
if (reply[0]) { if (reply[0]) {
Serial.print(" -> "); Serial.println(reply); Serial.print(" -> "); Serial.println(reply);
} }
@@ -109,10 +135,26 @@ void loop() {
command[0] = 0; // reset command buffer command[0] = 0; // reset command buffer
} }
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
char reply[160];
reply[0] = 0;
if (!ethernet_handle_command(ethernet_command, reply)) {
the_mesh.handleCommand(0, ethernet_command, reply);
}
ethernet_send_reply(reply);
ethernet_command[0] = 0;
}
#endif
the_mesh.loop(); the_mesh.loop();
sensors.loop(); sensors.loop();
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
ui_task.loop(); ui_task.loop();
#endif #endif
rtc_clock.tick(); rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
} }
+8 -1
View File
@@ -135,7 +135,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
File file = _fs->open("/contacts", "w", true); File file = _fs->open("/contacts", "w", true);
#endif #endif
if (file) { if (file) {
ContactsIterator iter; ContactsIterator iter = startContactsIterator();
ContactInfo c; ContactInfo c;
uint8_t unused = 0; uint8_t unused = 0;
uint32_t reserved = 0; uint32_t reserved = 0;
@@ -560,6 +560,10 @@ void setup() {
board.begin(); board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
if (!radio_init()) { halt(); } if (!radio_init()) { halt(); }
fast_rng.begin(radio_driver.getRngSeed()); fast_rng.begin(radio_driver.getRngSeed());
@@ -591,4 +595,7 @@ void setup() {
void loop() { void loop() {
the_mesh.loop(); the_mesh.loop();
rtc_clock.tick(); rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
} }
+6 -1
View File
@@ -323,6 +323,9 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
int SensorMesh::getInterferenceThreshold() const { int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold; return _prefs.interference_threshold;
} }
bool SensorMesh::getCADEnabled() const {
return _prefs.cad_enabled;
}
int SensorMesh::getAGCResetInterval() const { int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
} }
@@ -707,7 +710,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
set_radio_at = revert_radio_at = 0; set_radio_at = revert_radio_at = 0;
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.tx_delay_factor = 0.5f; // was 0.25f
@@ -726,11 +728,13 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.disable_fwd = true; _prefs.disable_fwd = true;
_prefs.flood_max = 64; _prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
// GPS defaults // GPS defaults
_prefs.gps_enabled = 0; _prefs.gps_enabled = 0;
_prefs.gps_interval = 0; _prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;
memset(default_scope.key, 0, sizeof(default_scope.key)); memset(default_scope.key, 0, sizeof(default_scope.key));
} }
@@ -766,6 +770,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
updateAdvertTimer(); updateAdvertTimer();
updateFloodAdvertTimer(); updateFloodAdvertTimer();
+3 -2
View File
@@ -34,11 +34,11 @@
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "6 Jun 2026" #define FIRMWARE_BUILD_DATE "9 Aug 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0" #define FIRMWARE_VERSION "v1.17.0"
#endif #endif
#define FIRMWARE_ROLE "sensor" #define FIRMWARE_ROLE "sensor"
@@ -120,6 +120,7 @@ protected:
uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override; int getInterferenceThreshold() const override;
bool getCADEnabled() const override;
int getAGCResetInterval() const override; int getAGCResetInterval() const override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override; int searchPeersByHash(const uint8_t* hash) override;
+5 -6
View File
@@ -41,27 +41,27 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
} }
// v1.2.3 (1 Jan 2025) // v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date); snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
} }
void UITask::renderCurrScreen() { void UITask::renderCurrScreen() {
char tmp[80]; char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo // meshcore logo
_display->setColor(DisplayDriver::BLUE); _display->setColor(UIColor::corp_blue);
int logoWidth = 128; int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website // meshcore website
const char* website = "https://meshcore.io"; const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT); _display->setColor(UIColor::primary_txt);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website); uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website); _display->print(website);
// version info // version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1); _display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info); uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 35); _display->setCursor((_display->width() - versionWidth) / 2, 35);
@@ -76,12 +76,11 @@ void UITask::renderCurrScreen() {
// node name // node name
_display->setCursor(0, 0); _display->setCursor(0, 0);
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN); _display->setColor(UIColor::primary_txt);
_display->print(_node_prefs->node_name); _display->print(_node_prefs->node_name);
// freq / sf // freq / sf
_display->setCursor(0, 20); _display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp); _display->print(tmp);
+7
View File
@@ -58,6 +58,10 @@ void setup() {
board.begin(); board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
if (display.begin()) { if (display.begin()) {
display.startFrame(); display.startFrame();
@@ -147,4 +151,7 @@ void loop() {
ui_task.loop(); ui_task.loop();
#endif #endif
rtc_clock.tick(); rtc_clock.tick();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
} }
+24 -3
View File
@@ -19,7 +19,8 @@ monitor_speed = 115200
lib_deps = lib_deps =
SPI SPI
Wire Wire
jgromes/RadioLib @ ^7.6.0 ;jgromes/RadioLib @ ^7.7.1
https://github.com/jgromes/RadioLib.git#6d8934836678d8894e3d556550475b37dce3e2b6
rweather/Crypto @ ^0.4.0 rweather/Crypto @ ^0.4.0
adafruit/RTClib @ ^2.1.3 adafruit/RTClib @ ^2.1.3
melopero/Melopero RV3028 @ ^1.1.0 melopero/Melopero RV3028 @ ^1.1.0
@@ -90,16 +91,17 @@ build_flags = ${arduino_base.build_flags}
-D NRF52_PLATFORM -D NRF52_PLATFORM
-D LFS_NO_ASSERT=1 -D LFS_NO_ASSERT=1
-D EXTRAFS=1 -D EXTRAFS=1
-D USE_CC310_HW_CRYPTO=1
lib_deps = lib_deps =
${arduino_base.lib_deps} ${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS#0.2.2 https://github.com/oltaco/CustomLFS#0.2.3
; ----------------- RP2040 --------------------- ; ----------------- RP2040 ---------------------
[rp2040_base] [rp2040_base]
extends = arduino_base extends = arduino_base
upload_protocol = picotool upload_protocol = picotool
board_build.core = earlephilhower board_build.core = earlephilhower
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#4e22a0d ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9 platform = https://github.com/maxgerhardt/platform-raspberrypi.git ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9
build_flags = ${arduino_base.build_flags} build_flags = ${arduino_base.build_flags}
-D RP2040_PLATFORM -D RP2040_PLATFORM
@@ -157,12 +159,31 @@ lib_deps =
[env:native] [env:native]
platform = native platform = native
test_framework = googletest
build_flags = -std=c++17 build_flags = -std=c++17
-I src -I src
-I test/mocks -I test/mocks
test_build_src = yes test_build_src = yes
test_ignore = test_kiss_modem
build_src_filter = build_src_filter =
-<*> -<*>
+<../src/Utils.cpp> +<../src/Utils.cpp>
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
lib_deps =
google/googletest @ 1.17.0
[env:native_kiss_modem]
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks
-I src
-I examples/kiss_modem
test_build_src = yes
test_filter = test_kiss_modem
build_src_filter =
-<*>
+<../examples/kiss_modem/KissModem.cpp>
lib_deps = lib_deps =
google/googletest @ 1.17.0 google/googletest @ 1.17.0
+1
View File
@@ -66,6 +66,7 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
void Dispatcher::loop() { void Dispatcher::loop() {
if (millisHasNowPassed(next_floor_calib_time)) { if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
_radio->setCADEnabled(getCADEnabled());
next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL);
} }
_radio->loop(); _radio->loop();
+3
View File
@@ -65,6 +65,8 @@ public:
virtual void triggerNoiseFloorCalibrate(int threshold) { } virtual void triggerNoiseFloorCalibrate(int threshold) { }
virtual void setCADEnabled(bool enable) { }
virtual void resetAGC() { } virtual void resetAGC() { }
virtual bool isInRecvMode() const = 0; virtual bool isInRecvMode() const = 0;
@@ -166,6 +168,7 @@ protected:
virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const; virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
+19 -1
View File
@@ -4,6 +4,11 @@
#include <ed_25519.h> #include <ed_25519.h>
#include <Ed25519.h> #include <Ed25519.h>
#ifdef USE_CC310_HW_CRYPTO
#include <Adafruit_nRFCrypto.h>
#include "nrf_cc310/include/crys_ec_edw_api.h"
#endif
namespace mesh { namespace mesh {
Identity::Identity() { Identity::Identity() {
@@ -15,7 +20,20 @@ Identity::Identity(const char* pub_hex) {
} }
bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const { bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const {
#if 0 #ifdef USE_CC310_HW_CRYPTO
// nRF52840 CryptoCell CC310 hardware Ed25519 verification. The software
// implementations need ~3KB of stack (which can overflow the Adafruit core's
// 4KB loop task stack from the advert receive path); the hardware path
// needs much less, around 600-700bytes. The CC310 workspace is static, faster,
// should save power at scale as well.
static CRYS_ECEDW_TempBuff_t cc310_tmp;
nRFCrypto.begin();
CRYSError_t rc = CRYS_ECEDW_Verify((uint8_t*)sig, CRYS_ECEDW_SIGNATURE_BYTES,
(uint8_t*)pub_key, CRYS_ECEDW_MOD_SIZE_IN_BYTES,
(uint8_t*)message, (size_t)msg_len, &cc310_tmp);
nRFCrypto.end();
return rc == CRYS_OK;
#elif 0
// NOTE: memory corruption bug was found in this function!! // NOTE: memory corruption bug was found in this function!!
return ed25519_verify(sig, message, msg_len, pub_key); return ed25519_verify(sig, message, msg_len, pub_key);
#else #else
+31 -16
View File
@@ -55,7 +55,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint16_t offset = (uint16_t)pkt->path_len << path_sz; uint16_t offset = (uint16_t)pkt->path_len << path_sz;
if (offset >= len) { // TRACE has reached end of given path if (offset >= len) { // TRACE has reached end of given path
onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len);
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
// append SNR (Not hash!) // append SNR (Not hash!)
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
@@ -89,14 +90,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt); return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->hasSeen(pkt)) { // don't retransmit! if (!_tables->wasSeen(pkt)) { // don't retransmit!
_tables->markSeen(pkt);
removeSelfFromPath(pkt); removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0); routeDirectRecvAcks(pkt, 0);
} }
return ACTION_RELEASE; return ACTION_RELEASE;
} }
if (!_tables->hasSeen(pkt)) { if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
removeSelfFromPath(pkt); removeSelfFromPath(pkt);
uint32_t d = getDirectRetransmitDelay(pkt); uint32_t d = getDirectRetransmitDelay(pkt);
@@ -117,7 +120,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
if (i > pkt->payload_len) { if (i > pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) { } else if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
onAckRecv(pkt, ack_crc); onAckRecv(pkt, ack_crc);
action = routeRecvPacket(pkt); action = routeRecvPacket(pkt);
} }
@@ -134,7 +138,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + CIPHER_MAC_SIZE >= pkt->payload_len) { if (i + CIPHER_MAC_SIZE >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) { } else if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
// NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins. // NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins.
// For flood mode, the path may not be the 'best' in terms of hops. // For flood mode, the path may not be the 'best' in terms of hops.
// FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?) // FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?)
@@ -155,6 +160,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0; int k = 0;
uint8_t path_len = data[k++]; uint8_t path_len = data[k++];
if (!Packet::isValidPathLen(path_len)) {
MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, bad path_len: %u", getLogDateTime(), (uint32_t)path_len);
break; // reject bad encoding
}
uint8_t hash_size = (path_len >> 6) + 1; uint8_t hash_size = (path_len >> 6) + 1;
uint8_t hash_count = path_len & 63; uint8_t hash_count = path_len & 63;
uint8_t* path = &data[k]; k += hash_size*hash_count; uint8_t* path = &data[k]; k += hash_size*hash_count;
@@ -193,7 +202,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) { if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) { } else if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
if (self_id.isHashMatch(&dest_hash)) { if (self_id.isHashMatch(&dest_hash)) {
Identity sender(sender_pub_key); Identity sender(sender_pub_key);
@@ -220,7 +230,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) { if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) { } else if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
// scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM) // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM)
GroupChannel channels[4]; GroupChannel channels[4];
int num = searchChannelsByHash(&channel_hash, channels, 4); int num = searchChannelsByHash(&channel_hash, channels, 4);
@@ -251,7 +262,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime());
} else if (self_id.matches(id.pub_key)) { } else if (self_id.matches(id.pub_key)) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) { } else if (!_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
uint8_t* app_data = &pkt->payload[i]; uint8_t* app_data = &pkt->payload[i];
int app_data_len = pkt->payload_len - i; int app_data_len = pkt->payload_len - i;
if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; } if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; }
@@ -278,7 +290,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
break; break;
} }
case PAYLOAD_TYPE_RAW_CUSTOM: { case PAYLOAD_TYPE_RAW_CUSTOM: {
if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) {
_tables->markSeen(pkt);
onRawDataRecv(pkt); onRawDataRecv(pkt);
//action = routeRecvPacket(pkt); don't flood route these (yet) //action = routeRecvPacket(pkt); don't flood route these (yet)
} }
@@ -296,7 +309,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
tmp.payload_len = pkt->payload_len - 1; tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) { if (!_tables->wasSeen(&tmp)) {
_tables->markSeen(&tmp);
uint32_t ack_crc; uint32_t ack_crc;
memcpy(&ack_crc, tmp.payload, 4); memcpy(&ack_crc, tmp.payload, 4);
@@ -353,7 +367,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
tmp.payload_len = pkt->payload_len - 1; tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) { // don't retransmit! if (!_tables->wasSeen(&tmp)) { // don't retransmit!
_tables->markSeen(&tmp);
removeSelfFromPath(&tmp); removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
} }
@@ -633,7 +648,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si
packet->header |= ROUTE_TYPE_FLOOD; packet->header |= ROUTE_TYPE_FLOOD;
packet->setPathHashSizeAndCount(path_hash_size, 0); packet->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
uint8_t pri; uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
@@ -662,7 +677,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m
packet->transport_codes[1] = transport_codes[1]; packet->transport_codes[1] = transport_codes[1];
packet->setPathHashSizeAndCount(path_hash_size, 0); packet->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
uint8_t pri; uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
@@ -695,7 +710,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
pri = 0; pri = 0;
} }
} }
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, pri, delay_millis); sendPacket(packet, pri, delay_millis);
} }
@@ -705,7 +720,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) {
packet->path_len = 0; // path_len of zero means Zero Hop packet->path_len = 0; // path_len of zero means Zero Hop
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis); sendPacket(packet, 0, delay_millis);
} }
@@ -718,7 +733,7 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay
packet->path_len = 0; // path_len of zero means Zero Hop packet->path_len = 0; // path_len of zero means Zero Hop
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis); sendPacket(packet, 0, delay_millis);
} }
+4 -3
View File
@@ -15,8 +15,9 @@ public:
*/ */
class MeshTables { class MeshTables {
public: public:
virtual bool hasSeen(const Packet* packet) = 0; virtual bool wasSeen(const Packet* packet) = 0;
virtual void clear(const Packet* packet) = 0; // remove this packet hash from table virtual void markSeen(const Packet* packet) = 0;
virtual void clear(const Packet* packet) = 0; // remove this packet hash from table
}; };
/** /**
@@ -100,7 +101,7 @@ protected:
* \param auth_code a code to authenticate the packet * \param auth_code a code to authenticate the packet
* \param flags zero for now * \param flags zero for now
* \param path_snrs single byte SNR*4 for each hop in the path * \param path_snrs single byte SNR*4 for each hop in the path
* \param path_hashes hashes if each repeater in the path * \param path_hashes hashes of each repeater in the path
* \param path_len length of the path_snrs[] and path_hashes[] arrays * \param path_len length of the path_snrs[] and path_hashes[] arrays
*/ */
virtual void onTraceRecv(Packet* packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t* path_snrs, const uint8_t* path_hashes, uint8_t path_len) { } virtual void onTraceRecv(Packet* packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t* path_snrs, const uint8_t* path_hashes, uint8_t path_len) { }
+3
View File
@@ -64,6 +64,9 @@ public:
virtual uint8_t getStartupReason() const = 0; virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
// Power management interface (boards with power management override these) // Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; } virtual bool isExternalPowered() { return false; }
+1 -1
View File
@@ -25,7 +25,7 @@ namespace mesh {
#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type(uint16), data_len, blob) #define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type(uint16), data_len, blob)
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) #define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) #define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop #define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNR for each hop
#define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets #define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets
#define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet #define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet
//... //...
+97 -1
View File
@@ -2,6 +2,13 @@
#include <AES.h> #include <AES.h>
#include <SHA256.h> #include <SHA256.h>
#ifdef USE_CC310_HW_CRYPTO
#include <Adafruit_nRFCrypto.h>
#include "nrf_cc310/include/crys_hash.h"
#include "nrf_cc310/include/crys_hmac.h"
#include "nrf_cc310/include/ssi_aes.h"
#endif
#ifdef ARDUINO #ifdef ARDUINO
#include <Arduino.h> #include <Arduino.h>
#endif #endif
@@ -15,19 +22,58 @@ uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) {
} }
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) { void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) {
#ifdef USE_CC310_HW_CRYPTO
static CRYS_HASH_Result_t result;
nRFCrypto.begin();
CRYS_HASH(CRYS_HASH_SHA256_mode, (uint8_t*)msg, (size_t)msg_len, result);
nRFCrypto.end();
memcpy(hash, result, hash_len);
#else
SHA256 sha; SHA256 sha;
sha.update(msg, msg_len); sha.update(msg, msg_len);
sha.finalize(hash, hash_len); sha.finalize(hash, hash_len);
#endif
} }
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len) { void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len) {
#ifdef USE_CC310_HW_CRYPTO
static CRYS_HASHUserContext_t ctx;
static CRYS_HASH_Result_t result;
nRFCrypto.begin();
CRYS_HASH_Init(&ctx, CRYS_HASH_SHA256_mode);
CRYS_HASH_Update(&ctx, (uint8_t*)frag1, (size_t)frag1_len);
CRYS_HASH_Update(&ctx, (uint8_t*)frag2, (size_t)frag2_len);
CRYS_HASH_Finish(&ctx, result);
nRFCrypto.end();
memcpy(hash, result, hash_len);
#else
SHA256 sha; SHA256 sha;
sha.update(frag1, frag1_len); sha.update(frag1, frag1_len);
sha.update(frag2, frag2_len); sha.update(frag2, frag2_len);
sha.finalize(hash, hash_len); sha.finalize(hash, hash_len);
#endif
} }
int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
#ifdef USE_CC310_HW_CRYPTO
static SaSiAesUserContext_t ctx;
SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE };
uint8_t* dp = dest;
const uint8_t* sp = src;
size_t dummy_out = 0;
nRFCrypto.begin();
SaSi_AesInit(&ctx, SASI_AES_DECRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE);
SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData));
while (sp - src < src_len) {
SaSi_AesBlock(&ctx, (uint8_t*)sp, 16, dp);
dp += 16; sp += 16;
}
SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out);
SaSi_AesFree(&ctx);
nRFCrypto.end();
return sp - src;
#else
AES128 aes; AES128 aes;
uint8_t* dp = dest; uint8_t* dp = dest;
const uint8_t* sp = src; const uint8_t* sp = src;
@@ -39,9 +85,34 @@ int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s
} }
return sp - src; // will always be multiple of 16 return sp - src; // will always be multiple of 16
#endif
} }
int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
#ifdef USE_CC310_HW_CRYPTO
static SaSiAesUserContext_t ctx;
SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE };
uint8_t* dp = dest;
size_t dummy_out = 0;
nRFCrypto.begin();
SaSi_AesInit(&ctx, SASI_AES_ENCRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE);
SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData));
while (src_len >= 16) {
SaSi_AesBlock(&ctx, (uint8_t*)src, 16, dp);
dp += 16; src += 16; src_len -= 16;
}
if (src_len > 0) { // remaining partial block — zero-pad to 16 bytes
uint8_t tmp[16] = {};
memcpy(tmp, src, src_len);
SaSi_AesBlock(&ctx, tmp, 16, dp);
dp += 16;
}
SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out);
SaSi_AesFree(&ctx);
nRFCrypto.end();
return dp - dest;
#else
AES128 aes; AES128 aes;
uint8_t* dp = dest; uint8_t* dp = dest;
@@ -58,15 +129,27 @@ int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s
dp += 16; dp += 16;
} }
return dp - dest; // will always be multiple of 16 return dp - dest; // will always be multiple of 16
#endif
} }
int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len); int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len);
#ifdef USE_CC310_HW_CRYPTO
static CRYS_HMACUserContext_t hmac_ctx;
static CRYS_HASH_Result_t hmac_result;
nRFCrypto.begin();
CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE);
CRYS_HMAC_Update(&hmac_ctx, dest + CIPHER_MAC_SIZE, enc_len);
CRYS_HMAC_Finish(&hmac_ctx, hmac_result);
nRFCrypto.end();
memcpy(dest, hmac_result, CIPHER_MAC_SIZE);
#else
SHA256 sha; SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE); sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(dest + CIPHER_MAC_SIZE, enc_len); sha.update(dest + CIPHER_MAC_SIZE, enc_len);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE);
#endif
return CIPHER_MAC_SIZE + enc_len; return CIPHER_MAC_SIZE + enc_len;
} }
@@ -75,12 +158,25 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin
if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes
uint8_t hmac[CIPHER_MAC_SIZE]; uint8_t hmac[CIPHER_MAC_SIZE];
#ifdef USE_CC310_HW_CRYPTO
{
static CRYS_HMACUserContext_t hmac_ctx;
static CRYS_HASH_Result_t hmac_result;
nRFCrypto.begin();
CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE);
CRYS_HMAC_Update(&hmac_ctx, (uint8_t*)(src + CIPHER_MAC_SIZE), src_len - CIPHER_MAC_SIZE);
CRYS_HMAC_Finish(&hmac_ctx, hmac_result);
nRFCrypto.end();
memcpy(hmac, hmac_result, CIPHER_MAC_SIZE);
}
#else
{ {
SHA256 sha; SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE); sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE);
} }
#endif
if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) {
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
} }
@@ -150,4 +246,4 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep
return num; return num;
} }
} }
+8 -6
View File
@@ -1,4 +1,5 @@
#include <helpers/AdvertDataHelpers.h> #include <helpers/AdvertDataHelpers.h>
#include <helpers/UTF8Helpers.h>
uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) { uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) {
app_data[0] = _type; app_data[0] = _type;
@@ -16,11 +17,12 @@
app_data[0] |= ADV_FEAT2_MASK; app_data[0] |= ADV_FEAT2_MASK;
memcpy(&app_data[i], &_extra2, 2); i += 2; memcpy(&app_data[i], &_extra2, 2); i += 2;
} }
if (_name && *_name != 0) { if (_name && *_name != 0) {
app_data[0] |= ADV_NAME_MASK; size_t name_len = mesh::validUtf8PrefixLength(_name, MAX_ADVERT_DATA_SIZE - i);
const char* sp = _name; if (name_len > 0) {
while (*sp && i < MAX_ADVERT_DATA_SIZE) { app_data[0] |= ADV_NAME_MASK;
app_data[i++] = *sp++; memcpy(&app_data[i], _name, name_len);
i += name_len;
} }
} }
return i; return i;
@@ -84,4 +86,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_
} }
} }
} }
} }
+1
View File
@@ -42,6 +42,7 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) {
} }
if (i2c_probe(wire, PCF8563_ADDRESS)) { if (i2c_probe(wire, PCF8563_ADDRESS)) {
MESH_DEBUG_PRINTLN("PCF8563: Found");
rtc_8563_success = rtc_8563.begin(&wire); rtc_8563_success = rtc_8563.begin(&wire);
} }
+32 -20
View File
@@ -68,29 +68,36 @@ void BaseChatMesh::bootstrapRTCfromContacts() {
} }
ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) { ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
if (num_contacts < MAX_CONTACTS) { int oldest_idx = -1;
return &contacts[num_contacts++]; uint32_t oldest_lastmod = 0xFFFFFFFF;
} else if (transient_only || shouldOverwriteWhenFull()) { if (transient_only) {
// Find oldest non-favourite contact by oldest lastmod timestamp // only allocate from first N
int oldest_idx = -1; for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
uint32_t oldest_lastmod = 0xFFFFFFFF; if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) {
for (int i = 0; i < num_contacts; i++) { oldest_lastmod = contacts[i].lastmod;
if (transient_only) { oldest_idx = i;
if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) { }
oldest_lastmod = contacts[i].lastmod; }
oldest_idx = i; if (oldest_idx >= 0) {
} // NOTE: do NOT call onContactOverwrite()
} else { return &contacts[oldest_idx];
}
} else {
if (num_contacts < MAX_ANON_CONTACTS+MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0; bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod && contacts[i].type != ADV_TYPE_NONE) { if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod; oldest_lastmod = contacts[i].lastmod;
oldest_idx = i; oldest_idx = i;
} }
} }
} if (oldest_idx >= 0) {
if (oldest_idx >= 0) { onContactOverwrite(contacts[oldest_idx].id.pub_key);
onContactOverwrite(contacts[oldest_idx].id.pub_key); return &contacts[oldest_idx];
return &contacts[oldest_idx]; }
} }
} }
return NULL; // no space, no overwrite or all contacts are all favourites return NULL; // no space, no overwrite or all contacts are all favourites
@@ -139,6 +146,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
packet->header = save; packet->header = save;
} }
if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ?
memset(from, 0, sizeof(*from)); // clear the anon/temp slot
from = NULL; // do normal 'add' flow
}
bool is_new = false; // true = not in contacts[], false = exists in contacts[] bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) { if (from == NULL) {
if (!shouldAutoAddContactType(parser.getType())) { if (!shouldAutoAddContactType(parser.getType())) {
@@ -930,11 +942,11 @@ bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) {
} }
ContactsIterator BaseChatMesh::startContactsIterator() { ContactsIterator BaseChatMesh::startContactsIterator() {
return ContactsIterator(); return ContactsIterator(MAX_ANON_CONTACTS); // start at offset, skip the anon entries
} }
bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) { bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) {
if (next_idx >= mesh->getNumContacts()) return false; if (next_idx >= mesh->getTotalContactSlots()) return false;
dest = mesh->contacts[next_idx++]; dest = mesh->contacts[next_idx++];
return true; return true;
+12 -5
View File
@@ -28,8 +28,9 @@ public:
class BaseChatMesh; class BaseChatMesh;
class ContactsIterator { class ContactsIterator {
int next_idx = 0; int next_idx;
public: public:
ContactsIterator(int start) { next_idx = start; }
bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest); bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest);
}; };
@@ -79,8 +80,9 @@ class BaseChatMesh : public mesh::Mesh {
protected: protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables) : mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{ {
num_contacts = 0; resetContacts();
#ifdef MAX_GROUP_CHANNELS #ifdef MAX_GROUP_CHANNELS
memset(channels, 0, sizeof(channels)); memset(channels, 0, sizeof(channels));
num_channels = 0; num_channels = 0;
@@ -91,7 +93,11 @@ protected:
} }
void bootstrapRTCfromContacts(); void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void resetContacts() {
memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0)
num_contacts = MAX_ANON_CONTACTS; // seed the first contacts for anon requests
}
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp); void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact
@@ -166,7 +172,8 @@ public:
ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len); ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len);
bool removeContact(ContactInfo& contact); bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact); bool addContact(const ContactInfo& contact);
int getNumContacts() const { return num_contacts; } int getTotalContactSlots() const { return num_contacts; }
int getNumContacts() const { return num_contacts - MAX_ANON_CONTACTS; } // don't include the reserved slots at start
bool getContactByIdx(uint32_t idx, ContactInfo& contact); bool getContactByIdx(uint32_t idx, ContactInfo& contact);
ContactsIterator startContactsIterator(); ContactsIterator startContactsIterator();
ChannelDetails* addChannel(const char* name, const char* psk_base64); ChannelDetails* addChannel(const char* name, const char* psk_base64);
+1
View File
@@ -14,6 +14,7 @@ public:
virtual bool isEnabled() const = 0; virtual bool isEnabled() const = 0;
virtual bool isConnected() const = 0; virtual bool isConnected() const = 0;
virtual void loop() {};
virtual bool isWriteBusy() const = 0; virtual bool isWriteBusy() const = 0;
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
+106 -80
View File
@@ -28,16 +28,25 @@ static bool isValidName(const char *n) {
} }
void CommonCLI::loadPrefs(FILESYSTEM* fs) { void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) { if (fs->exists("/prefs.json")) {
loadPrefsInt(fs, "/com_prefs"); // new filename #if defined(RP2040_PLATFORM)
} else if (fs->exists("/node_prefs")) { File file = fs->open("/prefs.json", "r");
loadPrefsInt(fs, "/node_prefs"); #else
savePrefs(fs); // save to new filename File file = fs->open("/prefs.json");
fs->remove("/node_prefs"); // remove old #endif
if (file) {
_prefs->loadSerial(file); // new Serial prefs
file.close();
}
} else if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs");
if (savePrefs(fs)) { // save to new Serial prefs
// fs->remove("/com_prefs"); // remove old
}
} }
} }
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy prefs loader
#if defined(RP2040_PLATFORM) #if defined(RP2040_PLATFORM)
File file = fs->open(filename, "r"); File file = fs->open(filename, "r");
#else #else
@@ -88,10 +97,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
// next: 293 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
// sanitise bad pref values // sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -121,73 +132,28 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// sanitise settings // sanitise settings
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
file.close(); file.close();
} }
} }
void CommonCLI::savePrefs(FILESYSTEM* fs) { bool CommonCLI::savePrefs(FILESYSTEM* fs) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
fs->remove("/com_prefs"); fs->remove("/prefs.json");
File file = fs->open("/com_prefs", FILE_O_WRITE); File file = fs->open("/prefs.json", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM) #elif defined(RP2040_PLATFORM)
File file = fs->open("/com_prefs", "w"); File file = fs->open("/prefs.json", "w");
#else #else
File file = fs->open("/com_prefs", "w", true); File file = fs->open("/prefs.json", "w", true);
#endif #endif
if (file) { if (file) {
uint8_t pad[8]; bool success = _prefs->saveSerial(file);
memset(pad, 0, sizeof(pad));
file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end)
file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.write(pad, 4); // 108 : 4 byte unused
file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
file.write(pad, 1); // 123
file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
file.write(pad, 3); // 153
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
// next: 293
file.close(); file.close();
return success;
} }
return false;
} }
#define MIN_LOCAL_ADVERT_INTERVAL 60 #define MIN_LOCAL_ADVERT_INTERVAL 60
@@ -500,6 +466,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->interference_threshold = atoi(&config[11]); _prefs->interference_threshold = atoi(&config[11]);
savePrefs(); savePrefs();
strcpy(reply, "OK"); strcpy(reply, "OK");
} else if (memcmp(config, "cad ", 4) == 0) {
_prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) { } else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4; _prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs(); savePrefs();
@@ -561,13 +531,37 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs(); savePrefs();
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110)
} else if (memcmp(config, "radio.rxgain ", 13) == 0) { } else if (memcmp(config, "radio.rxgain ", 13) == 0) {
_prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; bool enabled = memcmp(&config[13], "on", 2) == 0;
strcpy(reply, "OK"); _prefs->rx_boosted_gain = enabled;
savePrefs(); savePrefs();
_callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); if (_callbacks->setRxBoostedGain(enabled)) {
#endif strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemLnaEnabled(true)) {
_prefs->radio_fem_rxgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemLnaEnabled(false)) {
_prefs->radio_fem_rxgain = 0;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio ", 6) == 0) { } else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]); strcpy(tmp, &config[6]);
const char *parts[4]; const char *parts[4];
@@ -757,8 +751,30 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} }
} else { } else {
_prefs->adc_multiplier = 0.0f; _prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board"); strcpy(reply, "Error: unsupported");
}; };
#if defined(USE_LR2021)
} else if (memcmp(config, "extra.sf ", 9) == 0) {
strcpy(tmp, &config[9]);
const char *parts[4];
uint8_t sideDetSFs[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
if (num > 3) {
sprintf(reply, "Invalid extra SF config");
} else {
for (int i = 0; i < num; i++) {
sideDetSFs[i] = atoi(parts[i]);
}
sideDetSFs[num] = 0;
if (_callbacks->configSideDetectors(sideDetSFs, num, _prefs->bw)) {
for (int i = 0; i <= num; i++) _prefs->extra_sf[i] = sideDetSFs[i];
savePrefs();
sprintf(reply, "OK - extra SFs set");
} else {
sprintf(reply, "Invalid extra SF config");
}
}
#endif
} else { } else {
strcpy(reply, "unknown config: "); strcpy(reply, "unknown config: ");
StrHelper::strncpy(&reply[16], config, 160-17); StrHelper::strncpy(&reply[16], config, 160-17);
@@ -776,6 +792,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
} else if (memcmp(config, "int.thresh", 10) == 0) { } else if (memcmp(config, "int.thresh", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "cad", 3) == 0) {
sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off");
} else if (memcmp(config, "agc.reset.interval", 18) == 0) { } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks", 10) == 0) { } else if (memcmp(config, "multi.acks", 10) == 0) {
@@ -801,10 +819,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
} else if (memcmp(config, "lon", 3) == 0) { } else if (memcmp(config, "lon", 3) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110)
} else if (memcmp(config, "radio.rxgain", 12) == 0) { } else if (memcmp(config, "radio.rxgain", 12) == 0) {
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
#endif } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio", 5) == 0) { } else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16]; char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq)); strcpy(freq, StrHelper::ftoa(_prefs->freq));
@@ -890,12 +912,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "> unknown"); strcpy(reply, "> unknown");
} }
#else #else
strcpy(reply, "ERROR: unsupported"); strcpy(reply, "Error: unsupported");
#endif #endif
} else if (memcmp(config, "adc.multiplier", 14) == 0) { } else if (memcmp(config, "adc.multiplier", 14) == 0) {
float adc_mult = _board->getAdcMultiplier(); float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) { if (adc_mult == 0.0f) {
strcpy(reply, "Error: unsupported by this board"); strcpy(reply, "Error: unsupported");
} else { } else {
sprintf(reply, "> %.3f", adc_mult); sprintf(reply, "> %.3f", adc_mult);
} }
@@ -913,19 +935,23 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "ERROR: Power management not supported"); strcpy(reply, "ERROR: Power management not supported");
#endif #endif
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> Reset: %s; Shutdown: %s", sprintf(reply, "> Reset: %s; Shutdown: %s",
_board->getResetReasonString(_board->getResetReason()), _board->getResetReasonString(_board->getResetReason()),
_board->getShutdownReasonString(_board->getShutdownReason())); _board->getShutdownReasonString(_board->getShutdownReason()));
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
#ifdef NRF52_POWER_MANAGEMENT #ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> %u mV", _board->getBootVoltage()); sprintf(reply, "> %u mV", _board->getBootVoltage());
#else #else
strcpy(reply, "ERROR: Power management not supported"); strcpy(reply, "ERROR: Power management not supported");
#endif #endif
} else if (memcmp(config, "extra.sf", 8) == 0) {
char* tmp = reply;
for (int i = 0; i < 3 && _prefs->extra_sf[i] != 0; i++) {
tmp += sprintf(tmp, "%s%d", (i == 0) ? "" : ",", _prefs->extra_sf[i]);
}
if (tmp == reply) {
sprintf(reply, "No extra SF configured");
}
} else { } else {
sprintf(reply, "??: %s", config); sprintf(reply, "??: %s", config);
} }
+171 -38
View File
@@ -5,6 +5,7 @@
#include <helpers/SensorManager.h> #include <helpers/SensorManager.h>
#include <helpers/ClientACL.h> #include <helpers/ClientACL.h>
#include <helpers/RegionMap.h> #include <helpers/RegionMap.h>
#include <helpers/ConfigSerializer.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE #define WITH_BRIDGE
@@ -19,50 +20,176 @@
#define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3 #define LOOP_DETECT_STRICT 3
struct NodePrefs { // persisted to file class NodePrefs : public ConfigSerializer {
float airtime_factor; public:
// in-memory backing data
float airtime_factor = 0;
char node_name[32]; char node_name[32];
double node_lat, node_lon; double node_lat = 0, node_lon = 0;
char password[16]; char password[16];
float freq; float freq = 0;
int8_t tx_power_dbm; int8_t tx_power_dbm = 0;
uint8_t disable_fwd; uint8_t disable_fwd = 0;
uint8_t advert_interval; // minutes / 2 uint8_t advert_interval = 0; // minutes / 2
uint8_t flood_advert_interval; // hours uint8_t flood_advert_interval = 0; // hours
float rx_delay_base; float rx_delay_base = 0;
float tx_delay_factor; float tx_delay_factor = 0;
char guest_password[16]; char guest_password[16];
float direct_tx_delay_factor; float direct_tx_delay_factor = 0;
uint32_t guard; uint32_t guard;
uint8_t sf; uint8_t sf = 0;
uint8_t cr; uint8_t cr = 0;
uint8_t allow_read_only; uint8_t allow_read_only = 0;
uint8_t multi_acks; uint8_t multi_acks = 0;
float bw; float bw = 0;
uint8_t flood_max; uint8_t flood_max = 0;
uint8_t flood_max_unscoped; uint8_t flood_max_unscoped = 0;
uint8_t flood_max_advert; uint8_t flood_max_advert = 0;
uint8_t interference_threshold; uint8_t interference_threshold = 0;
uint8_t agc_reset_interval; // secs / 4 uint8_t agc_reset_interval = 0; // secs / 4
// Bridge settings // Bridge settings
uint8_t bridge_enabled; // boolean uint8_t bridge_enabled = 0; // boolean
uint16_t bridge_delay; // milliseconds (default 500 ms) uint16_t bridge_delay = 0; // milliseconds (default 500 ms)
uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx) uint8_t bridge_pkt_src = 0; // 0 = logTx, 1 = logRx (default logTx)
uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200) uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200)
uint8_t bridge_channel; // 1-14 (ESP-NOW only) uint8_t bridge_channel = 0; // 1-14 (ESP-NOW only)
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
// Power setting // Power setting
uint8_t powersaving_enabled; // boolean uint8_t powersaving_enabled = 0; // boolean
// Gps settings // Gps settings
uint8_t gps_enabled; uint8_t gps_enabled = 0;
uint32_t gps_interval; // in seconds uint32_t gps_interval = 0; // in seconds
uint8_t advert_loc_policy; uint8_t advert_loc_policy = 0;
uint32_t discovery_mod_timestamp; uint32_t discovery_mod_timestamp = 0;
float adc_multiplier; float adc_multiplier = 0;
char owner_info[120]; char owner_info[120];
uint8_t rx_boosted_gain; // power settings uint8_t rx_boosted_gain = 0; // power settings
uint8_t path_hash_mode; // which path mode to use when sending uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting
uint8_t loop_detect; uint8_t path_hash_mode = 0; // which path mode to use when sending
uint8_t loop_detect = 0;
uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean)
uint8_t extra_sf[4];
private:
class RadioPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("freq", _parent->freq);
def("bw", _parent->bw);
def("sf", _parent->sf);
def("cr", _parent->cr);
def("cad", _parent->cad_enabled);
def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->rx_boosted_gain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
def("f_txdelay", _parent->tx_delay_factor);
def("d_txdelay", _parent->direct_tx_delay_factor);
def("agc_int", _parent->agc_reset_interval);
def("hash_mode", _parent->path_hash_mode);
def("multi_ack", _parent->multi_acks);
}
public:
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
};
RadioPrefs radio;
class BridgePrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("en", _parent->bridge_enabled); // boolean
def("delay", _parent->bridge_delay); // milliseconds (default 500 ms)
def("src", _parent->bridge_pkt_src); // 0 = logTx, 1 = logRx (default logTx)
def("baud", _parent->bridge_baud); // 9600, 19200, 38400, 57600, 115200 (default 115200)
def("ch", _parent->bridge_channel); // 1-14 (ESP-NOW only)
def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); // for XOR encryption of bridge packets (ESP-NOW only)
}
public:
BridgePrefs(NodePrefs* parent) : _parent(parent) { }
};
BridgePrefs bridge;
class GPSPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("en", _parent->gps_enabled); // boolean
def("int", _parent->gps_interval); // interval in seconds
def("adv_loc", _parent->advert_loc_policy);
}
public:
GPSPrefs(NodePrefs* parent) : _parent(parent) { }
};
GPSPrefs gps;
class PowerPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("adc_mult", _parent->adc_multiplier);
def("pwr_sav_en", _parent->powersaving_enabled);
}
public:
PowerPrefs(NodePrefs* parent) : _parent(parent) { }
};
PowerPrefs power;
class RepeatPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("disable", _parent->disable_fwd);
def("f_max", _parent->flood_max);
def("f_max_uns", _parent->flood_max_unscoped);
def("f_max_adv", _parent->flood_max_advert);
def("loop", _parent->loop_detect);
}
public:
RepeatPrefs(NodePrefs* parent) : _parent(parent) { }
};
RepeatPrefs repeat;
class RoomPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("rd_only", _parent->allow_read_only);
}
public:
RoomPrefs(NodePrefs* parent) : _parent(parent) { }
};
RoomPrefs room;
protected:
void structure() override {
def("name", node_name, sizeof(node_name));
def("pass", password, sizeof(password));
def("guest", guest_password, sizeof(guest_password));
def("owner", owner_info, sizeof(owner_info));
def("adv_int", advert_interval);
def("f_adv_int", flood_advert_interval);
def("lat", node_lat);
def("lon", node_lon);
def("radio", radio);
def("bridge", bridge);
def("gps", gps);
def("repeat", repeat);
def("room", room);
def("power", power);
}
public:
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) {
node_name[0] = 0;
password[0] = 0;
guest_password[0] = 0;
bridge_secret[0] = 0;
owner_info[0] = 0;
}
}; };
class CommonCLICallbacks { class CommonCLICallbacks {
@@ -109,9 +236,15 @@ public:
// no op by default // no op by default
}; };
virtual void setRxBoostedGain(bool enable) { virtual bool setRxBoostedGain(bool enable) {
// no op by default return false; // CommonCLI reports unsupported if not overridden by wrapper
}; };
#if defined(USE_LR2021)
virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) {
return false; // Override in wrapper
}
#endif
}; };
class CommonCLI { class CommonCLI {
@@ -137,7 +270,7 @@ public:
: _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
void loadPrefs(FILESYSTEM* _fs); void loadPrefs(FILESYSTEM* _fs);
void savePrefs(FILESYSTEM* _fs); bool savePrefs(FILESYSTEM* _fs);
void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data); uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
}; };
+331
View File
@@ -0,0 +1,331 @@
#include "ConfigSerializer.h"
bool ConfigSerializer::saveSerial(Stream& s) {
Context context(&s, OP::WRITE);
_context = &context; // set the context for structure() call
s.print("{"); // root object
_first = true;
structure();
if (s.print("}") != 1) context.success = false; // failure detect
_context = NULL;
return context.success;
}
#define TOK_ERROR -1
#define TOK_EOF 0
#define TOK_KEY 1
#define TOK_VALUE 2
#define TOK_START_OBJ 3
#define TOK_END_OBJ 4
#define TOK_WHITESPACE 5
static bool is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static bool is_key_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}
static bool is_value_char(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.';
}
#define EXPECT_OPEN_BRACE 0
#define EXPECT_KEY 1
#define EXPECT_VAL_OR_OBJ 2
#define EXPECT_STRING_VAL 3
#define EXPECT_STRING_ESCAPE 4
#define EXPECT_COMMA_OR_CLOSE 5
#define EXPECT_COMMA_OR_KEY 6
#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7
int ConfigSerializer::Context::readNext() {
char c;
if (pending) {
c = pending;
pending = 0;
} else {
if (_f->available() == 0) return TOK_EOF;
int n = _f->read();
if (n < 0) return TOK_EOF;
c = (char)n;
}
switch (rd_mode) {
case EXPECT_OPEN_BRACE:
if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
if (is_whitespace(c)) return TOK_WHITESPACE;
return TOK_ERROR;
case EXPECT_COMMA_OR_KEY_OR_CLOSE:
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
case EXPECT_COMMA_OR_KEY:
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
case EXPECT_KEY:
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
return TOK_ERROR;
case EXPECT_VAL_OR_OBJ:
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; }
return TOK_ERROR;
case EXPECT_STRING_ESCAPE:
if ((c == 'n') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\n'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
if ((c == 'r') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\r'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
return TOK_ERROR; // unsupport escape
case EXPECT_STRING_VAL:
if (c == '"') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; }
if (c == '\\') { rd_mode = EXPECT_STRING_ESCAPE; return TOK_WHITESPACE; }
if (rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
return TOK_ERROR;
case EXPECT_COMMA_OR_CLOSE:
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
if (is_whitespace(c)) return TOK_WHITESPACE;
return TOK_ERROR;
}
return TOK_ERROR; // unknown mode
}
bool ConfigSerializer::loadSerial(Stream& s) {
Context context(&s, OP::READ);
_context = &context; // set the context for structure() call
uint8_t sp = 0; // object nesting stack pointer
int next_tok;
// parse the Json file
while ((next_tok = context.readNext()) > TOK_EOF) {
if (next_tok == TOK_KEY) {
context.setKey(sp, context.getToken());
} else if (next_tok == TOK_VALUE) {
_depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth)
structure();
} else if (next_tok == TOK_START_OBJ) {
if (sp < CONFIG_MAX_DEPTH - 1) {
sp++;
} else {
//Serial.printf("Error: max nesting reached"); // TODO: debug logging
context.success = false;
break;
}
} else if (next_tok == TOK_END_OBJ) {
if (sp > 0) {
sp--;
} else {
//Serial.printf("Error: too many closing '}'"); // TODO: debug logging
context.success = false;
break;
}
}
}
if (sp != 0 || next_tok == TOK_ERROR) {
context.success = false; // unmatched { }, or other parse error
}
_context = NULL;
return context.success;
}
void ConfigSerializer::writeComma() {
if (_first) {
_first = false;
} else {
_context->file()->print(","); // comma separated properties
}
}
#include <Utils.h>
void ConfigSerializer::def(const char* key, void* value, size_t len) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":\"");
mesh::Utils::printHex(*_context->file(), (uint8_t*) value, len);
_context->file()->print("\"");
} else {
if (_context->keyMatch(_depth, key)) {
memset(value, 0, len);
mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, char* value, size_t max_len) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":\"");
char c;
while ((c = *value++) != 0) { // TODO: handle UTF-8 encoding
if (c == '"') {
_context->file()->print("\\\"");
} else if (c == '\\') {
_context->file()->print("\\\\");
} else if (c == '\n') {
_context->file()->print("\\n");
} else if (c == '\r') {
_context->file()->print("\\r");
} else {
_context->file()->print(c);
}
}
_context->file()->print("\"");
} else {
if (_context->keyMatch(_depth, key)) {
strncpy(value, _context->getToken(), max_len - 1);
value[max_len - 1] = 0;
}
}
}
void ConfigSerializer::def(const char* key, int32_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print(value);
} else {
if (_context->keyMatch(_depth, key)) {
value = atol(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, uint32_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print(value);
} else {
if (_context->keyMatch(_depth, key)) {
value = atol(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, int16_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print((int32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
value = atol(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, uint16_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print((uint32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
value = atoi(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, uint8_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print((uint32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
value = atoi(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, int8_t& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print((int32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
value = atoi(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, bool& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
_context->file()->print(value ? "true" : "false");
} else {
if (_context->keyMatch(_depth, key)) {
value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number
}
}
}
void ConfigSerializer::def(const char* key, double& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
if (value == 0.0) {
_context->file()->print("0"); // shorter encoding
} else {
_context->file()->print(value, 6); // REVISIT: how many dec places?
}
} else {
if (_context->keyMatch(_depth, key)) {
value = atof(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, float& value) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":");
if (value == 0.0f) {
_context->file()->print("0"); // shorter encoding
} else {
_context->file()->print(value, 4); // REVISIT: how many dec places?
}
} else {
if (_context->keyMatch(_depth, key)) {
value = (float) atof(_context->getToken());
}
}
}
void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) {
if (_context->op() == OP::WRITE) {
writeComma();
_context->file()->print(key);
_context->file()->print(":{");
sub_obj._context = _context; // inherit the Context
sub_obj._first = true;
sub_obj.structure(); // recurse into sub object
if (_context->file()->print("}") != 1) _context->success = false; // failure detect
} else {
if (_context->keyMatch(_depth, key)) {
sub_obj._context = _context; // inherit the Context
sub_obj._depth = _depth + 1;
sub_obj.structure(); // recurse into sub object
}
}
}
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <Arduino.h>
#ifndef CONFIG_MAX_DEPTH
#define CONFIG_MAX_DEPTH 8
#endif
#ifndef CONFIG_MAX_KEYLEN
#define CONFIG_MAX_KEYLEN 16
#endif
#ifndef CONFIG_MAX_TOKEN_LEN
#define CONFIG_MAX_TOKEN_LEN 128
#endif
class ConfigSerializer {
bool _first;
int8_t _depth;
enum OP { READ, WRITE };
class Context {
Stream* _f;
OP _op;
uint8_t rd_len;
uint8_t rd_mode;
char pending;
char rd_buf[CONFIG_MAX_TOKEN_LEN];
char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN];
public:
bool success = true;
Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; }
OP op() const { return _op; }
Stream* file() const { return _f; }
int readNext();
const char* getToken() const { return rd_buf; }
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
};
Context* _context = NULL;
void writeComma();
protected:
ConfigSerializer() { }
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
void def(const char* key, void* value, size_t len); // binary blob (encoded in hex)
void def(const char* key, int32_t& value);
void def(const char* key, int16_t& value);
void def(const char* key, int8_t& value);
void def(const char* key, uint32_t& value);
void def(const char* key, uint16_t& value);
void def(const char* key, uint8_t& value);
void def(const char* key, float& value);
void def(const char* key, double& value);
void def(const char* key, bool& value);
void def(const char* key, ConfigSerializer& sub_obj);
virtual void structure() = 0;
public:
bool loadSerial(Stream& s);
bool saveSerial(Stream& s);
};
+42
View File
@@ -1,6 +1,7 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "ESP32Board.h" #include "ESP32Board.h"
#include <target.h>
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only #if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#include <WiFi.h> #include <WiFi.h>
@@ -44,4 +45,45 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
} }
#endif #endif
void ESP32Board::powerOff() {
enterDeepSleep(0); // Do not wakeup
}
void ESP32Board::enterDeepSleep(uint32_t secs) {
// Power off the display if any
#ifdef DISPLAY_CLASS
display.turnOff();
#endif
// Power off LoRa
radio_driver.powerOff();
// Keep LoRa inactive during deepsleep
digitalWrite(P_LORA_NSS, HIGH);
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
gpio_hold_en((gpio_num_t)P_LORA_NSS);
#else
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
#endif
// Power off GPS if any
if (sensors.getLocationProvider() != NULL) {
sensors.getLocationProvider()->stop();
}
// Flush serial buffers
Serial.flush();
delay(100);
// Clear stale wakeup sources to avoid ghost wakeup
// This is required when Power Management and automatic lightsleep are enabled
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000ULL);
}
// Finally set ESP32 into deepsleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
#endif #endif
+40
View File
@@ -14,6 +14,7 @@
#include <Wire.h> #include <Wire.h>
#include "soc/rtc.h" #include "soc/rtc.h"
#include "esp_system.h" #include "esp_system.h"
#include <driver/rtc_io.h>
class ESP32Board : public mesh::MainBoard { class ESP32Board : public mesh::MainBoard {
protected: protected:
@@ -62,6 +63,9 @@ public:
return raw / 4; return raw / 4;
} }
virtual void powerOff() override;
void enterDeepSleep(uint32_t secs);
uint32_t getIRQGpio() override { uint32_t getIRQGpio() override {
return P_LORA_DIO_1; // default for SX1262 return P_LORA_DIO_1; // default for SX1262
} }
@@ -155,6 +159,42 @@ public:
void setInhibitSleep(bool inhibit) { void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit; inhibit_sleep = inhibit;
} }
uint32_t getResetReason() const override {
return esp_reset_reason();
}
// https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html
const char* getResetReasonString(uint32_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN:
return "Unknown or first boot";
case ESP_RST_POWERON:
return "Power-on reset";
case ESP_RST_EXT:
return "External reset";
case ESP_RST_SW:
return "Software reset";
case ESP_RST_PANIC:
return "Panic / exception reset";
case ESP_RST_INT_WDT:
return "Interrupt watchdog reset";
case ESP_RST_TASK_WDT:
return "Task watchdog reset";
case ESP_RST_WDT:
return "Other watchdog reset";
case ESP_RST_DEEPSLEEP:
return "Wake from deep sleep";
case ESP_RST_BROWNOUT:
return "Brownout (low voltage)";
case ESP_RST_SDIO:
return "SDIO reset";
default:
static char buf[40];
snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason);
return buf;
}
}
}; };
class ESP32RTCClock : public mesh::RTCClock { class ESP32RTCClock : public mesh::RTCClock {
+12
View File
@@ -0,0 +1,12 @@
#pragma once
class ExternalWatchdogManager {
protected:
unsigned long last_feed_watchdog;
public:
ExternalWatchdogManager() { last_feed_watchdog = 0; }
virtual bool begin() { return false; }
virtual void loop() { }
virtual unsigned long getIntervalMs() const { return 0; }
virtual void feed() { }
};
-30
View File
@@ -15,8 +15,6 @@
#include "ESP32Board.h" #include "ESP32Board.h"
#include <driver/rtc_io.h>
class MeshadventurerBoard : public ESP32Board { class MeshadventurerBoard : public ESP32Board {
public: public:
@@ -35,34 +33,6 @@ public:
} }
} }
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void powerOff() override {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t getBattMilliVolts() override { uint16_t getBattMilliVolts() override {
analogReadResolution(12); analogReadResolution(12);
+200
View File
@@ -0,0 +1,200 @@
#pragma once
#include "BaseSerialInterface.h"
#ifndef MAX_INTERFACES
// ble, usb, wifi, ethernet
#define MAX_INTERFACES 4
#endif
enum class InterfaceType : uint8_t {
NONE,
Bluetooth,
USB,
WiFi,
Ethernet,
HardwareSerial
};
class MultiSerialInterface : public BaseSerialInterface {
private:
struct RegisteredInterface {
InterfaceType type = InterfaceType::NONE;
BaseSerialInterface* instance = nullptr;
};
bool _enabled = false;
RegisteredInterface _interfaces[MAX_INTERFACES] = {};
public:
bool addInterface(InterfaceType type, BaseSerialInterface* iface) {
// make sure an interface was provided
if(iface == nullptr){
return false;
}
// put it in the first free slot
for(int i = 0; i < MAX_INTERFACES; i++){
if(_interfaces[i].instance == nullptr){
_interfaces[i].instance = iface;
_interfaces[i].type = type;
return true;
}
}
// no free slots available
return false;
}
bool removeInterface(BaseSerialInterface* iface) {
// make sure an interface was provided
if(iface == nullptr){
return false;
}
// find and remove interface
for(int i = 0; i < MAX_INTERFACES; i++){
if(_interfaces[i].instance == iface){
_interfaces[i] = {};
return true;
}
}
// interface not found
return false;
}
void enableBluetooth() {
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
iface.instance->enable();
}
}
}
void disableBluetooth() {
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
iface.instance->disable();
}
}
}
bool isBluetoothEnabled() {
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
return iface.instance->isEnabled();
}
}
return false;
}
// enable all interfaces
void enable() override {
_enabled = true;
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->enable();
}
}
}
// disable all interfaces
void disable() override {
_enabled = false;
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->disable();
}
}
}
bool isEnabled() const override {
return _enabled;
}
bool isConnected() const override {
// not connected when disabled
if(!_enabled){
return false;
}
// check if any interface is connected
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isConnected()) {
return true;
}
}
// nothing connected
return false;
}
// loop all interfaces
void loop() override {
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->loop();
}
}
}
bool isWriteBusy() const override {
// not busy when disabled
if(!_enabled){
return false;
}
// check if any interface is busy
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled() && iface.instance->isWriteBusy()){
return true;
}
}
// nothing busy
return false;
}
size_t writeFrame(const uint8_t src[], size_t len) override {
// don't write when disabled or nothing provided
if(!_enabled || len == 0){
return 0;
}
// write frame to all enabled interfaces
bool allSuccessful = true;
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled()){
if(iface.instance->writeFrame(src, len) != len){
allSuccessful = false;
}
}
}
// report success if all writes completed successfully
return allSuccessful ? len : 0;
}
size_t checkRecvFrame(uint8_t dest[]) override {
// don't read when disabled
if(!_enabled){
return 0;
}
// try to read a frame from any enabled interface
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled()){
size_t frameSize = iface.instance->checkRecvFrame(dest);
if(frameSize > 0){
return frameSize;
}
}
}
// no frame received
return 0;
}
};
+82 -8
View File
@@ -1,5 +1,6 @@
#if defined(NRF52_PLATFORM) #if defined(NRF52_PLATFORM)
#include "NRF52Board.h" #include "NRF52Board.h"
#include <target.h>
#include <bluefruit.h> #include <bluefruit.h>
#include <nrf_soc.h> #include <nrf_soc.h>
@@ -279,16 +280,29 @@ void NRF52Board::sleep(uint32_t secs) {
// Temperature from NRF52 MCU // Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() { float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
long startTime = millis(); if (sd_enabled) {
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us uint32_t err_code;
if(millis() - startTime > 5) { // To wait 5ms just in case int32_t temp;
NRF_TEMP->TASKS_STOP = 1; err_code = sd_temp_get(&temp);
if (err_code == NRF_SUCCESS) {
return (float)temp * 0.25f;
} else {
return NAN; return NAN;
} }
} else {
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 NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
@@ -297,6 +311,65 @@ float NRF52Board::getMCUTemperature() {
return temp * 0.25f; // Convert to *C return temp * 0.25f; // Convert to *C
} }
void NRF52Board::shutdownPeripherals() {
// Power off the display if any
#ifdef DISPLAY_CLASS
if (display.isOn()) {
display.turnOff();
}
#endif
// Prep LoRa radio for power down
#ifdef P_LORA_RESET
digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low
pinMode(P_LORA_RESET, OUTPUT);
digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us)
delayMicroseconds(200);
digitalWrite(P_LORA_RESET, HIGH);
#endif
#if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio
#endif
#ifdef P_LORA_BUSY
pinMode(P_LORA_BUSY, INPUT);
uint32_t started_at = millis();
while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready
#endif
#ifdef P_LORA_NSS
pinMode(P_LORA_NSS, OUTPUT);
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off LoRa
radio_driver.powerOff();
// Keep LoRa inactive during deepsleep
#ifdef P_LORA_NSS
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off GPS if any
if(sensors.getLocationProvider() != NULL) {
sensors.getLocationProvider()->stop();
}
// Flush serial buffers
Serial.flush();
delay(100);
}
void NRF52Board::powerOff() {
shutdownPeripherals();
// Enter SYSTEMOFF
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) { // SoftDevice is enabled
sd_power_system_off();
} else { // SoftDevice is not enable
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
}
}
bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) { bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
static const char BOOTLOADER_MARKER[] = "UF2 Bootloader "; static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
@@ -323,7 +396,8 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
Bluefruit.begin(1, 0); if (!Bluefruit.begin(1, 0)) return false;
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4); Bluefruit.setTxPower(4);
// Set the BLE device name // Set the BLE device name
+2
View File
@@ -50,6 +50,8 @@ public:
virtual uint8_t getStartupReason() const override { return startup_reason; } virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override; virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); } virtual void reboot() override { NVIC_SystemReset(); }
virtual void shutdownPeripherals();
virtual void powerOff() override;
virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual bool startOTAUpdate(const char *id, char reply[]) override;
virtual void sleep(uint32_t secs) override; virtual void sleep(uint32_t secs) override;
+6 -4
View File
@@ -93,13 +93,15 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
while (num_regions < MAX_REGION_ENTRIES) { while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions]; auto r = &regions[num_regions];
success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); int n = file.read((uint8_t *) &r->id, sizeof(r->id));
if (n == 0) break; // clean EOF
success = (n == sizeof(r->id));
success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.read(pad, sizeof(pad)) == sizeof(pad); success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF if (!success) break; // partial read or corruption
if (r->id >= next_id) { // make sure next_id is valid if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1; next_id = r->id + 1;
@@ -108,7 +110,7 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
} }
} }
file.close(); file.close();
return true; return success;
} }
} }
return false; // failed return false; // failed
@@ -139,7 +141,7 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
} }
} }
file.close(); file.close();
return true; return success;
} }
return false; // failed return false; // failed
} }
+10 -6
View File
@@ -31,27 +31,31 @@ public:
} }
#endif #endif
bool hasSeen(const mesh::Packet* packet) override { bool wasSeen(const mesh::Packet* packet) override {
uint8_t hash[MAX_HASH_SIZE]; uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash); packet->calculatePacketHash(hash);
const uint8_t* sp = _hashes; const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
if (packet->isRouteDirect()) { if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats _direct_dups++;
} else { } else {
_flood_dups++; _flood_dups++;
} }
return true; return true;
} }
} }
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table
return false; return false;
} }
void markSeen(const mesh::Packet* packet) override {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES;
}
void clear(const mesh::Packet* packet) override { void clear(const mesh::Packet* packet) override {
uint8_t hash[MAX_HASH_SIZE]; uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash); packet->calculatePacketHash(hash);
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh {
inline bool isUtf8Continuation(uint8_t byte) {
return (byte & 0xC0) == 0x80;
}
inline size_t validUtf8PrefixLength(const char* text, size_t max_bytes) {
if (text == nullptr) return 0;
size_t offset = 0;
while (text[offset] != '\0') {
const uint8_t first = static_cast<uint8_t>(text[offset]);
size_t sequence_length = 0;
if (first <= 0x7F) {
sequence_length = 1;
} else if (first >= 0xC2 && first <= 0xDF) {
sequence_length = 2;
} else if (first >= 0xE0 && first <= 0xEF) {
sequence_length = 3;
} else if (first >= 0xF0 && first <= 0xF4) {
sequence_length = 4;
} else {
break;
}
if (offset + sequence_length > max_bytes) break;
bool complete = true;
for (size_t i = 1; i < sequence_length; i++) {
if (text[offset + i] == '\0' || !isUtf8Continuation(static_cast<uint8_t>(text[offset + i]))) {
complete = false;
break;
}
}
if (!complete) break;
if (sequence_length == 3) {
const uint8_t second = static_cast<uint8_t>(text[offset + 1]);
if ((first == 0xE0 && second < 0xA0) || (first == 0xED && second > 0x9F)) break;
} else if (sequence_length == 4) {
const uint8_t second = static_cast<uint8_t>(text[offset + 1]);
if ((first == 0xF0 && second < 0x90) || (first == 0xF4 && second > 0x8F)) break;
}
offset += sequence_length;
}
return offset;
}
} // namespace mesh
+2 -1
View File
@@ -39,7 +39,8 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
return; return;
} }
if (!_seen_packets.hasSeen(packet)) { if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network. // bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay); _mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
} else { } else {
+1 -1
View File
@@ -110,7 +110,7 @@ protected:
* @brief Common packet handling for received packets * @brief Common packet handling for received packets
* *
* Implements the standard pattern used by all bridges: * Implements the standard pattern used by all bridges:
* - Check if packet was seen before using _seen_packets.hasSeen() * - Check if packet was seen before using _seen_packets.wasSeen()
* - Queue packet for mesh processing if not seen before * - Queue packet for mesh processing if not seen before
* - Free packet if already seen to prevent duplicates * - Free packet if already seen to prevent duplicates
* *
+3 -2
View File
@@ -32,7 +32,7 @@ void ESPNowBridge::begin() {
// Initialize WiFi in station mode // Initialize WiFi in station mode
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Set wifi channel // Set Wi-Fi channel
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) { if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel); BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
return; return;
@@ -167,7 +167,8 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
return; return;
} }
if (!_seen_packets.hasSeen(packet)) { if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
// Create a temporary buffer just for size calculation and reuse for actual writing // Create a temporary buffer just for size calculation and reuse for actual writing
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE]; uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
uint16_t meshPacketLen = packet->writeTo(sizingBuffer); uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
+2 -1
View File
@@ -115,7 +115,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
return; return;
} }
if (!_seen_packets.hasSeen(packet)) { if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
uint8_t buffer[MAX_SERIAL_PACKET_SIZE]; uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
uint16_t len = packet->writeTo(buffer + 4); uint16_t len = packet->writeTo(buffer + 4);
+1 -1
View File
@@ -38,7 +38,7 @@ public:
* These two functions do nothing for ESP-NOW, but are needed for the * These two functions do nothing for ESP-NOW, but are needed for the
* Radio interface. * Radio interface.
*/ */
virtual void setRxBoostedGainMode(bool) { } virtual bool setRxBoostedGainMode(bool) { }
virtual bool getRxBoostedGainMode() const { return false; } virtual bool getRxBoostedGainMode() const { return false; }
uint32_t intID(); uint32_t intID();
+18 -20
View File
@@ -103,10 +103,9 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param
void SerialBLEInterface::onDisconnect(BLEServer* pServer) { void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
BLE_DEBUG_PRINTLN("onDisconnect()"); BLE_DEBUG_PRINTLN("onDisconnect()");
deviceConnected = false;
if (_isEnabled) { if (_isEnabled) {
adv_restart_time = millis() + ADVERT_RESTART_DELAY; adv_restart_time = millis() + ADVERT_RESTART_DELAY;
// loop() will detect this on next loop, and set deviceConnected to false
} }
} }
@@ -118,17 +117,24 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat
if (len > MAX_FRAME_SIZE) { if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len);
} else if (recv_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
} else { } else {
recv_queue[recv_queue_len].len = len; Frame frame = {};
memcpy(recv_queue[recv_queue_len].buf, rxValue, len); frame.len = len;
recv_queue_len++; memcpy(frame.buf, rxValue, len);
if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
}
} }
} }
// ---------- public methods // ---------- public methods
void SerialBLEInterface::clearBuffers() {
xQueueReset(recv_queue);
send_queue_len = 0;
}
void SerialBLEInterface::enable() { void SerialBLEInterface::enable() {
if (_isEnabled) return; if (_isEnabled) return;
@@ -202,21 +208,13 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
} }
} }
if (recv_queue_len > 0) { // check recv queue Frame frame;
size_t len = recv_queue[0].len; // take from top of queue if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) {
memcpy(dest, recv_queue[0].buf, len); memcpy(dest, frame.buf, frame.len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); return frame.len;
recv_queue_len--;
for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue
recv_queue[i] = recv_queue[i + 1];
}
return len;
} }
if (pServer->getConnectedCount() == 0) deviceConnected = false;
if (deviceConnected != oldDeviceConnected) { if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting if (!deviceConnected) { // disconnecting
clearBuffers(); clearBuffers();
+10 -4
View File
@@ -5,6 +5,8 @@
#include <BLEServer.h> #include <BLEServer.h>
#include <BLEUtils.h> #include <BLEUtils.h>
#include <BLE2902.h> #include <BLE2902.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
BLEServer *pServer; BLEServer *pServer;
@@ -24,12 +26,13 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
}; };
#define FRAME_QUEUE_SIZE 4 #define FRAME_QUEUE_SIZE 4
int recv_queue_len; StaticQueue_t recv_queue_state;
Frame recv_queue[FRAME_QUEUE_SIZE]; uint8_t recv_queue_storage[FRAME_QUEUE_SIZE * sizeof(Frame)];
QueueHandle_t recv_queue;
int send_queue_len; int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE]; Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } void clearBuffers();
protected: protected:
// BLESecurityCallbacks methods // BLESecurityCallbacks methods
@@ -58,7 +61,10 @@ public:
_isEnabled = false; _isEnabled = false;
_last_write = 0; _last_write = 0;
last_conn_id = 0; last_conn_id = 0;
send_queue_len = recv_queue_len = 0; recv_queue = xQueueCreateStatic(
FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state
);
send_queue_len = 0;
} }
/** /**
+10
View File
@@ -16,6 +16,16 @@ void TBeamBoard::begin() {
ESP32Board::begin(); ESP32Board::begin();
#ifdef TBEAM_SUPREME_SX1262
// On the T-Beam S3 Supreme the PMU + RTC sit on Wire1 (GPIO 42/41, brought
// up by XPowersLib), while the SH1106 OLED and the BME280/QMC6310 sensors
// sit on the primary bus, Wire (GPIO PIN_BOARD_SDA/PIN_BOARD_SCL). Nothing
// else initialises Wire on this board, so the display driver would
// otherwise talk on the wrong (default) pins and display.begin() fails,
// leaving the screen blank. Bring the OLED bus up here.
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
power_init(); power_init();
//Configure user button //Configure user button
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#if defined(ETHERNET_ENABLED)
#if defined(ETHERNET_USE_CH390)
#include "helpers/ethernet/ch390/CH390EthernetInterface.h"
#elif defined(ETHERNET_USE_RAK13800)
#include "helpers/ethernet/RAK13800/RAK13800EthernetInterface.h"
#else
#error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!"
#endif
#endif
@@ -0,0 +1,109 @@
#include "RAK13800EthernetInterface.h"
#include "../../nrf52/EthernetMac.h"
#include <SPI.h>
#include <EthernetUdp.h>
#define PIN_SPI1_MISO (29) // (0 + 29)
#define PIN_SPI1_MOSI (30) // (0 + 30)
#define PIN_SPI1_SCK (3) // (0 + 3)
SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);
#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26
bool RAK13800EthernetInterface::begin() {
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset — the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
#ifdef PIN_ETHERNET_RESET
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
#endif
// generate mac address
uint8_t mac[6];
generateEthernetMac(mac);
ETHERNET_DEBUG_PRINTLN(
"Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0],
mac[1],
mac[2],
mac[3],
mac[4],
mac[5]);
ETHERNET_SPI_PORT.begin();
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
// Use static IP if build flags are defined, otherwise DHCP
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS)
IPAddress ip(ETHERNET_STATIC_IP);
IPAddress gateway(ETHERNET_STATIC_GATEWAY);
IPAddress subnet(ETHERNET_STATIC_SUBNET);
IPAddress dns(ETHERNET_STATIC_DNS);
Ethernet.begin(mac, ip, dns, gateway, subnet);
#else
if (Ethernet.begin(mac) == 0) {
ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware.");
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found.");
} else if (Ethernet.linkStatus() == LinkOFF) {
ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected.");
} else {
ETHERNET_DEBUG_PRINTLN("DHCP failed for unknown reason.");
}
return false;
}
#endif
ETHERNET_DEBUG_PRINTLN("Ethernet begin complete");
ETHERNET_DEBUG_PRINT_IP("IP Address", Ethernet.localIP());
ETHERNET_DEBUG_PRINT_IP("Subnet Mask", Ethernet.subnetMask());
ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP());
ETHERNET_DEBUG_PRINT_IP("DNS", Ethernet.dnsServerIP());
server.begin();
ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT);
return true;
}
int RAK13800EthernetInterface::available() {
return client.available();
}
int RAK13800EthernetInterface::read() {
return client.read();
}
size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) {
return client.write(buf, size);
}
bool RAK13800EthernetInterface::isConnected() const {
return _isConnected;
}
void RAK13800EthernetInterface::loop() {
Ethernet.maintain();
auto newClient = server.accept();
if (newClient) {
IPAddress remoteIp = newClient.remoteIP();
uint16_t remotePort = newClient.remotePort();
ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort);
if (client) {
ETHERNET_DEBUG_PRINTLN("Closing previous client");
client.stop();
}
client = newClient;
onClientConnected();
}
_isConnected = client.connected();
}
@@ -0,0 +1,27 @@
#pragma once
#include "../SerialEthernetInterface.h"
#include <SPI.h>
#include <RAK13800_W5100S.h>
class RAK13800EthernetInterface : public SerialEthernetInterface {
bool _isConnected;
EthernetServer server;
EthernetClient client;
public:
RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) {
_isConnected = false;
}
bool begin();
void loop() override;
// BaseSerialInterface methods
bool isConnected() const override;
int available() override;
int read() override;
size_t write(const uint8_t *buf, size_t size) override;
};
@@ -0,0 +1,140 @@
#include "SerialEthernetInterface.h"
#define RECV_STATE_IDLE 0
#define RECV_STATE_HDR_FOUND 1
#define RECV_STATE_LEN1_FOUND 2
#define RECV_STATE_LEN2_FOUND 3
bool SerialEthernetInterface::begin() {
return true;
}
void SerialEthernetInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
}
void SerialEthernetInterface::disable() {
_isEnabled = false;
}
size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len);
return 0;
}
if (isConnected() && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len; // add to send queue
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
bool SerialEthernetInterface::isWriteBusy() const {
return false;
}
void SerialEthernetInterface::onClientConnected() {
_state = RECV_STATE_IDLE;
_frame_len = 0;
_rx_len = 0;
}
size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
if (isConnected()) {
if (send_queue_len > 0) { // first, check send queue
_last_write = millis();
int len = send_queue[0].len;
#if ETHERNET_RAW_LINE
ETHERNET_DEBUG_PRINTLN("TX line len=%d", len);
client.write(send_queue[0].buf, len);
client.write("\r\n", 2);
#else
uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames
pkt[0] = '>';
pkt[1] = (len & 0xFF); // LSB
pkt[2] = (len >> 8); // MSB
memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len);
ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len);
#if ETHERNET_DEBUG_LOGGING && ARDUINO
ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len);
#endif
write(pkt, 3 + len);
#endif
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
} else {
while (available()) {
int c = read();
if (c < 0) break;
#if ETHERNET_RAW_LINE
if (c == '\r' || c == '\n') {
if (_rx_len == 0) {
continue;
}
uint16_t out_len = _rx_len;
if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE;
memcpy(dest, _rx_buf, out_len);
_rx_len = 0;
return out_len;
}
if (_rx_len < MAX_FRAME_SIZE) {
_rx_buf[_rx_len] = (uint8_t)c;
_rx_len++;
}
#else
switch (_state) {
case RECV_STATE_IDLE:
if (c == '<') {
_state = RECV_STATE_HDR_FOUND;
}
break;
case RECV_STATE_HDR_FOUND:
_frame_len = (uint8_t)c;
_state = RECV_STATE_LEN1_FOUND;
break;
case RECV_STATE_LEN1_FOUND:
_frame_len |= ((uint16_t)c) << 8;
_rx_len = 0;
_state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE;
break;
default:
if (_rx_len < MAX_FRAME_SIZE) {
_rx_buf[_rx_len] = (uint8_t)c;
}
_rx_len++;
if (_rx_len >= _frame_len) {
if (_frame_len > MAX_FRAME_SIZE) {
_frame_len = MAX_FRAME_SIZE;
}
#if ETHERNET_DEBUG_LOGGING && ARDUINO
ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len);
#endif
memcpy(dest, _rx_buf, _frame_len);
_state = RECV_STATE_IDLE;
return _frame_len;
}
}
#endif
}
}
}
return 0;
}
@@ -0,0 +1,75 @@
#pragma once
#include "../BaseSerialInterface.h"
#ifndef ETHERNET_TCP_PORT
#define ETHERNET_TCP_PORT 5000
#endif
// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets
class SerialEthernetInterface : public BaseSerialInterface {
bool _isEnabled;
unsigned long _last_write;
uint8_t _state;
uint16_t _frame_len;
uint16_t _rx_len;
uint8_t _rx_buf[MAX_FRAME_SIZE];
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 4
int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() {
send_queue_len = 0;
_state = 0;
_frame_len = 0;
_rx_len = 0;
}
protected:
public:
SerialEthernetInterface() {
_isEnabled = false;
_last_write = 0;
send_queue_len = 0;
_state = 0;
_frame_len = 0;
_rx_len = 0;
}
bool begin();
void onClientConnected();
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
virtual int available() = 0;
virtual int read() = 0;
virtual size_t write(const uint8_t *buf, size_t size) = 0;
};
#if ETHERNET_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__)
#define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__)
#define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3])
#else
#define ETHERNET_DEBUG_PRINT(...) {}
#define ETHERNET_DEBUG_PRINTLN(...) {}
#define ETHERNET_DEBUG_PRINT_IP(...) {}
#endif
@@ -0,0 +1,94 @@
#include "CH390EthernetInterface.h"
void onWiFiEvent(WiFiEvent_t event) {
switch(event){
case ARDUINO_EVENT_ETH_START:
ETHERNET_DEBUG_PRINTLN("Ethernet Started");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
ETHERNET_DEBUG_PRINTLN("Ethernet Connected");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
ETHERNET_DEBUG_PRINTLN("Ethernet Disconnected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
ETHERNET_DEBUG_PRINTLN("Ethernet Got IP");
ETHERNET_DEBUG_PRINT_IP("IP Address", CH390.localIP());
ETHERNET_DEBUG_PRINT_IP("Subnet Mask", CH390.subnetMask());
ETHERNET_DEBUG_PRINT_IP("Gateway", CH390.gatewayIP());
ETHERNET_DEBUG_PRINT_IP("DNS", CH390.dnsIP());
ETHERNET_DEBUG_PRINTLN("MAC Address: %s", CH390.macAddress().c_str());
break;
default:
break;
}
}
bool CH390EthernetInterface::begin() {
// listen to ethernet events
WiFi.onEvent(onWiFiEvent);
// Init CH390
ch390_config_t config = CH390_DEFAULT_CONFIG();
config.spi_miso_gpio = ETH_MISO_PIN;
config.spi_mosi_gpio = ETH_MOSI_PIN;
config.spi_sck_gpio = ETH_SCLK_PIN;
config.spi_cs_gpio = ETH_CS_PIN;
config.int_gpio = ETH_INT_PIN;
if (!CH390.begin(config)) {
ETHERNET_DEBUG_PRINTLN("Failed to initialize CH390 hardware.");
return false;
}
// Setup Static IP if build flags are present
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET)
IPAddress ip(ETHERNET_STATIC_IP);
IPAddress gw(ETHERNET_STATIC_GATEWAY);
IPAddress sn(ETHERNET_STATIC_SUBNET);
CH390.config(ip, gw, sn);
#endif
// Start Server
server.begin(ETHERNET_TCP_PORT);
ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT);
return true;
}
int CH390EthernetInterface::available() {
return client.available();
}
int CH390EthernetInterface::read() {
return client.read();
}
size_t CH390EthernetInterface::write(const uint8_t *buf, size_t size) {
return client.write(buf, size);
}
bool CH390EthernetInterface::isConnected() const {
return _isConnected;
}
void CH390EthernetInterface::loop() {
if (server.hasClient()) {
auto newClient = server.available();
if (newClient) {
IPAddress remoteIp = newClient.remoteIP();
uint16_t remotePort = newClient.remotePort();
ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort);
if (client) {
ETHERNET_DEBUG_PRINTLN("Closing previous client");
client.stop();
}
client = newClient;
onClientConnected();
}
}
_isConnected = client.connected();
}
@@ -0,0 +1,30 @@
#pragma once
#include "../SerialEthernetInterface.h"
#include <SPI.h>
#include <WiFi.h>
#include <WiFiServer.h>
#include <WiFiClient.h>
#include <ESP32_CH390.h>
class CH390EthernetInterface : public SerialEthernetInterface {
bool _isConnected;
WiFiServer server;
WiFiClient client;
public:
CH390EthernetInterface(){
_isConnected = false;
}
bool begin();
void loop() override;
// BaseSerialInterface methods
bool isConnected() const override;
int available() override;
int read() override;
size_t write(const uint8_t *buf, size_t size) override;
};
+157
View File
@@ -0,0 +1,157 @@
#pragma once
#ifdef ETHERNET_ENABLED
#include <Arduino.h>
#include <SPI.h>
#include <RAK13800_W5100S.h>
#include <helpers/nrf52/EthernetMac.h>
#define PIN_SPI1_MISO (29)
#define PIN_SPI1_MOSI (30)
#define PIN_SPI1_SCK (3)
static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);
#define PIN_ETHERNET_POWER_EN WB_IO2
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26
#ifndef ETHERNET_TCP_PORT
#define ETHERNET_TCP_PORT 23 // telnet port for CLI access
#endif
#ifndef ETHERNET_CLI_BANNER
#define ETHERNET_CLI_BANNER "MeshCore CLI"
#endif
#define ETHERNET_RETRY_INTERVAL_MS 30000
static EthernetServer ethernet_server(ETHERNET_TCP_PORT);
static EthernetClient ethernet_client;
static volatile bool ethernet_running = false;
// FreeRTOS task: handles hw init, DHCP, and retries in the background
static void ethernet_task(void* param) {
(void)param;
Serial.println("ETH: Initializing hardware");
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset — the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
ETHERNET_SPI_PORT.begin();
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
uint8_t mac[6];
generateEthernetMac(mac);
Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Retry loop: keep trying until we get an IP
while (!ethernet_running) {
Serial.println("ETH: Attempting DHCP...");
if (Ethernet.begin(mac, 10000, 2000) == 0) {
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("ETH: Hardware not found, giving up");
vTaskDelete(NULL);
return;
}
if (Ethernet.linkStatus() == LinkOFF) {
Serial.println("ETH: Cable not connected, will retry");
} else {
Serial.println("ETH: DHCP failed, will retry");
}
vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS));
continue;
}
IPAddress ip = Ethernet.localIP();
Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT);
ethernet_server.begin();
ethernet_running = true;
}
// DHCP succeeded, task is done
vTaskDelete(NULL);
}
static void ethernet_start_task() {
xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL);
}
// Format ethernet status into reply buffer. Returns true if command was handled.
static bool ethernet_handle_command(const char* command, char* reply) {
if (strcmp(command, "eth.status") == 0) {
if (!ethernet_running) {
strcpy(reply, "ETH: not connected");
} else {
IPAddress ip = Ethernet.localIP();
sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT);
}
return true;
}
return false;
}
// Check for new TCP client connections, replacing any existing connection.
// Use accept() (not available()) so we only see newly-accepted sockets;
// available() also returns existing connected sockets that have data, which
// would force us to disambiguate every inbound packet from a real new client.
static void ethernet_check_client() {
auto newClient = ethernet_server.accept();
if (newClient) {
if (ethernet_client) ethernet_client.stop();
ethernet_client = newClient;
IPAddress ip = ethernet_client.remoteIP();
Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
ethernet_client.println(ETHERNET_CLI_BANNER);
}
}
// Call from loop() to maintain DHCP and check for new clients
static void ethernet_loop_maintain() {
if (ethernet_running) {
ethernet_check_client();
Ethernet.maintain();
}
}
// Read a line from the Ethernet client into the command buffer.
// Returns true when a complete line is ready to process (command is null-terminated).
// The caller should process the command and then reset ethernet_command[0] = 0.
static bool ethernet_read_line(char* ethernet_command, size_t buf_size) {
if (!ethernet_running || !ethernet_client || !ethernet_client.connected()) return false;
int elen = strlen(ethernet_command);
while (ethernet_client.available() && elen < (int)buf_size - 1) {
char c = ethernet_client.read();
if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF)
if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; }
ethernet_command[elen++] = c;
ethernet_command[elen] = 0;
}
if (elen == (int)buf_size - 1) {
ethernet_command[buf_size - 1] = '\r';
}
if (elen > 0 && ethernet_command[elen - 1] == '\r') {
ethernet_command[elen - 1] = 0;
ethernet_client.println();
return true;
}
return false;
}
// Send a reply to the Ethernet client
static void ethernet_send_reply(const char* reply) {
if (reply[0]) {
ethernet_client.print(" -> "); ethernet_client.println(reply);
}
}
#endif // ETHERNET_ENABLED
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
static inline void generateEthernetMac(uint8_t mac[6]) {
uint32_t device_id = NRF_FICR->DEVICEID[0];
mac[0] = 0x02;
mac[1] = 0x92;
mac[2] = 0x1F;
mac[3] = (device_id >> 16) & 0xFF;
mac[4] = (device_id >> 8) & 0xFF;
mac[5] = device_id & 0xFF;
}
+63 -10
View File
@@ -2,10 +2,12 @@
#include <RadioLib.h> #include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomLLCC68 : public LLCC68 { class CustomLLCC68 : public LLCC68 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
public: public:
CustomLLCC68(Module *mod) : LLCC68(mod) { } CustomLLCC68(Module *mod) : LLCC68(mod) { }
@@ -66,11 +68,11 @@ class CustomLLCC68 : public LLCC68 {
setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif #endif
#if defined(SX126X_RXEN) || defined(SX126X_TXEN) #if defined(SX126X_RXEN) || defined(SX126X_TXEN)
#ifndef SX1262X_RXEN #ifndef SX126X_RXEN
#define SX1262X_RXEN RADIOLIB_NC #define SX126X_RXEN RADIOLIB_NC
#endif #endif
#ifndef SX1262X_TXEN #ifndef SX126X_TXEN
#define SX1262X_TXEN RADIOLIB_NC #define SX126X_TXEN RADIOLIB_NC
#endif #endif
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif #endif
@@ -78,10 +80,61 @@ class CustomLLCC68 : public LLCC68 {
return true; // success return true; // success
} }
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags
return LLCC68::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
bool isReceiving() { bool isReceiving() {
uint16_t irq = getIrqFlags(); uint32_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2
return detected; bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4
bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5
uint32_t now = millis();
if (hdrErr) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
} }
bool getRxBoostedGainMode() { bool getRxBoostedGainMode() {
+6 -2
View File
@@ -14,6 +14,10 @@ public:
((CustomLLCC68 *)_radio)->setBandwidth(bw); ((CustomLLCC68 *)_radio)->setBandwidth(bw);
((CustomLLCC68 *)_radio)->setCodingRate(cr); ((CustomLLCC68 *)_radio)->setCodingRate(cr);
updatePreamble(sf); updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLLCC68 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLLCC68 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
} }
bool isReceivingPacket() override { bool isReceivingPacket() override {
@@ -33,8 +37,8 @@ public:
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
void setRxBoostedGainMode(bool en) override { bool setRxBoostedGainMode(bool en) override {
((CustomLLCC68 *)_radio)->setRxBoostedGainMode(en); return ((CustomLLCC68 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
} }
bool getRxBoostedGainMode() const override { bool getRxBoostedGainMode() const override {
return ((CustomLLCC68 *)_radio)->getRxBoostedGainMode(); return ((CustomLLCC68 *)_radio)->getRxBoostedGainMode();
+58 -4
View File
@@ -4,6 +4,10 @@
#include "MeshCore.h" #include "MeshCore.h"
class CustomLR1110 : public LR1110 { class CustomLR1110 : public LR1110 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false; bool _rx_boosted = false;
public: public:
@@ -31,11 +35,61 @@ class CustomLR1110 : public LR1110 {
bool getRxBoostedGainMode() const { return _rx_boosted; } bool getRxBoostedGainMode() const { return _rx_boosted; }
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags.
return LR1110::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
bool isReceiving() { bool isReceiving() {
uint16_t irq = getIrqStatus(); uint32_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
return detected; bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
} }
uint8_t getSpreadingFactor() const { return spreadingFactor; } uint8_t getSpreadingFactor() const { return spreadingFactor; }
}; };
+10 -2
View File
@@ -14,6 +14,9 @@ public:
((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setBandwidth(bw);
((CustomLR1110 *)_radio)->setCodingRate(cr); ((CustomLR1110 *)_radio)->setCodingRate(cr);
updatePreamble(sf); updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
} }
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
@@ -26,6 +29,11 @@ public:
return rssi; return rssi;
} }
uint32_t getEstAirtimeFor(int len_bytes) override {
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
return airtime < 200 ? 200 : airtime; // at least 200 millis
}
void onSendFinished() override { void onSendFinished() override {
RadioLibWrapper::onSendFinished(); RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
@@ -36,8 +44,8 @@ public:
uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); }
void setRxBoostedGainMode(bool en) override { bool setRxBoostedGainMode(bool en) override {
((CustomLR1110 *)_radio)->setRxBoostedGainMode(en); return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
} }
bool getRxBoostedGainMode() const override { bool getRxBoostedGainMode() const override {
return ((CustomLR1110 *)_radio)->getRxBoostedGainMode(); return ((CustomLR1110 *)_radio)->getRxBoostedGainMode();
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
class CustomLR2021 : public LR2021 {
bool _rx_boosted = false;
public:
CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; }
bool std_init(SPIClass* spi = NULL)
{
#ifdef LR2021_TCXO_VOLTAGE
float tcxo = LR2021_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
if (spi) {
spi->setMISO(P_LORA_MISO);
//spi->setCS(P_LORA_NSS); // Setting CS results in freeze
spi->setSCK(P_LORA_SCLK);
spi->setMOSI(P_LORA_MOSI);
spi->begin();
}
#else
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
#endif
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) {
tcxo = 0.0f;
status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
}
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
setCRC(2);
explicitHeader();
#ifdef LR2021_RX_BOOSTED_GAIN
setRxBoostedGainMode(LR2021_RX_BOOSTED_GAIN);
#endif
return true; // success
}
float getFreqMHz() const { return freqMHz; }
bool getRxBoostedGainMode() const { return _rx_boosted; }
bool isReceiving() {
uint32_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR2021_IRQ_SYNCWORD_VALID) || (irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED));
return detected;
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
};
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include "CustomLR2021.h"
#include "RadioLibWrappers.h"
#ifndef USE_LR2021
#define USE_LR2021
#endif
#ifndef LR2021_RX_BOOST_LEVEL
#define LR2021_RX_BOOST_LEVEL 7
#endif
class CustomLR2021Wrapper : public RadioLibWrapper {
public:
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomLR2021 *)_radio)->setFrequency(freq);
((CustomLR2021 *)_radio)->setSpreadingFactor(sf);
((CustomLR2021 *)_radio)->setBandwidth(bw);
((CustomLR2021 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
applySideDetectorConfig();
}
bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num, float bw) override {
LR2021LoRaSideDetector_t tmp[3];
uint8_t sf = getSpreadingFactor();
if (sf >= 10 && num > 1) { return false; } // only 1 side detector allowed when primary SF >= 10
for (int i = 0; i < num; i++) {
if (sideDetSFs[i] > 12 || sideDetSFs[i] < 5) { return false; } // must be valid SF
if (sideDetSFs[i] <= sf) { return false; } // must be < primary SF
if (sideDetSFs[i] > sf + 4) { return false; } // span must not be > 4
tmp[i].sf = sideDetSFs[i];
float tSym = calcTsym(tmp[i].sf, bw);
if (tSym >= 16.0f) {
tmp[i].ldro = true;
} else {
tmp[i].ldro = false;
}
tmp[i].invertIQ = false;
tmp[i].syncWord = 0x12;
}
int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(tmp, num);
RadioLibWrapper::idle(); // trigger startReceive()
MESH_DEBUG_PRINTLN("setSideDetector() returned %d", status);
if (status == RADIOLIB_ERR_NONE) {
for (int i = 0; i < num; i++) { _sideDet[i] = tmp[i]; }
_numSideDet = num;
} else {
return false;
}
return true;
}
int16_t applySideDetectorConfig() {
int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(_sideDet, _numSideDet);
RadioLibWrapper::idle(); // trigger startReceive()
return status;
}
float calcTsym(uint8_t sf, float bw) {
float tSym = (float)(uint32_t(1) << sf) / (float)bw;
return tSym;
}
bool isReceivingPacket() override {
return ((CustomLR2021 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR2021 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR2021 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR2021 *)_radio)->getSNR(); }
uint8_t getSpreadingFactor() const override { return ((CustomLR2021 *)_radio)->getSpreadingFactor(); }
bool setRxBoostedGainMode(bool en) override {
((CustomLR2021 *)_radio)->standby(); // LR2021 must be in standby to accept setRxBoostedGainMode
int16_t status = ((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL: 0);
RadioLibWrapper::idle(); // trigger startReceive()
return status == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
return ((CustomLR2021 *)_radio)->getRxBoostedGainMode();
}
protected:
LR2021LoRaSideDetector_t _sideDet[3];
size_t _numSideDet = 0;
};
+61 -7
View File
@@ -2,16 +2,70 @@
#include <RadioLib.h> #include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomSTM32WLx : public STM32WLx { class CustomSTM32WLx : public STM32WLx {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
public: public:
CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { }
bool isReceiving() { int16_t startReceive() override {
uint16_t irq = getIrqFlags(); // include the PREAMBLE_DETECTED irq bit in reported flags
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); return STM32WLx::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
return detected;
} }
bool isReceiving() {
uint32_t irq = getIrqFlags();
bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2
bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4
bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5
uint32_t now = millis();
if (hdrErr) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
}; };

Some files were not shown because too many files have changed in this diff Show More