mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
Merge upstream companion-v1.18.0
Fixes and features from upstream: MACThenDecrypt / command buffer overflow fixes, ACK forwarding delay, transport code 0 as unscoped, hidden nodes don't answer DISCOVER, LR2021/LR1121 fixes, GPS cold-start detection and waitFor()/drain(), NMEA fix-age counters, ESP32 RTC after a crash, remote CLI by text message (TXT_TYPE_CLI_COMMAND), PUSH_CODE_BINARY_RESPONSE, board.loop(), new boards (Wio Tracker L1 1W, ThinkNode M4/M8, Muzi Base, Waveshare LR1121, Pico W). Kept ours: NodePrefs, main.cpp, every UI, GxEPDDisplay. The companion CLI stays MyMeshCli.h's handleCliCommand(); upstream's handleCommand() calls it. The board's FEM gains go through attachDynamicPrefs() with an adapter over our NodePrefs fields. Upstream's JSON prefs and MultiSerialInterface come as their own stages. Also: the display defaults were inside RADIO_FEM_TXGAIN's #else (a missing #endif). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x4405"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0029"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x002A"
|
||||
]
|
||||
],
|
||||
"usb_product": "muzi_base",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "MUZI_BASE",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": [
|
||||
"jlink"
|
||||
],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "Muzi Base",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink"
|
||||
]
|
||||
},
|
||||
"url": "https://github.com/muzi-works",
|
||||
"vendor": "MuziWorks"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x4405"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x0029"
|
||||
],
|
||||
[
|
||||
"0x239A",
|
||||
"0x002A"
|
||||
]
|
||||
],
|
||||
"usb_product": "elecrow_thinknode_m4",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "ELECROW-ThinkNode-M4",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": [
|
||||
"jlink"
|
||||
],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "elecrow nrf",
|
||||
"upload": {
|
||||
"maximum_ram_size": 235520,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink"
|
||||
]
|
||||
},
|
||||
"url": "https://github.com/Elecrow-RD",
|
||||
"vendor": "ELECROW"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"]
|
||||
],
|
||||
"usb_product": "elecrow_thinknode_m8",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "ELECROW-ThinkNode-M8",
|
||||
"variants_dir": "variants",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "elecrow thinknode m8",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "",
|
||||
"vendor": "ELECROW"
|
||||
}
|
||||
@@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []):
|
||||
src_filter.append("+<helpers/stm32/*>")
|
||||
elif item == "ESP32":
|
||||
src_filter.append("+<helpers/esp32/*>")
|
||||
src_filter.append("+<helpers/wifi/*>")
|
||||
elif item == "NRF52_PLATFORM":
|
||||
src_filter.append("+<helpers/nrf52/*>")
|
||||
elif item == "RP2040_PLATFORM":
|
||||
src_filter.append("+<helpers/rp2040/*>")
|
||||
src_filter.append("+<helpers/wifi/*>")
|
||||
|
||||
# DISPLAY HANDLING
|
||||
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
|
||||
|
||||
+11
-6
@@ -211,7 +211,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR`
|
||||
|
||||
**Default:** `869.525,250,11,5`
|
||||
**Default:** `869.618,62.5,8,5`
|
||||
|
||||
**Note:** Requires reboot to apply
|
||||
|
||||
@@ -256,7 +256,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
**Parameters:**
|
||||
- `frequency`: Frequency in MHz
|
||||
|
||||
**Default:** `869.525`
|
||||
**Default:** `869.618`
|
||||
|
||||
**Note:** Requires reboot to apply
|
||||
**Serial Only:** `set freq <frequency>`
|
||||
@@ -537,7 +537,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
**Parameters:**
|
||||
- `value`: Direct transmit delay factor (0-2)
|
||||
|
||||
**Default:** `0.2`
|
||||
**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor)
|
||||
|
||||
**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.
|
||||
|
||||
@@ -628,7 +628,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
- `set agc.reset.interval <value>`
|
||||
|
||||
**Parameters:**
|
||||
- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
|
||||
- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
|
||||
|
||||
**Default:** `0.0`
|
||||
|
||||
@@ -654,7 +654,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
**Parameters:**
|
||||
- `hours`: Interval in hours (3-168)
|
||||
|
||||
**Default:** `12` (Repeater) - `0` (Sensor)
|
||||
**Default:** `47` (Repeater, Room Server) - `0`, disabled (Sensor)
|
||||
|
||||
---
|
||||
|
||||
@@ -666,7 +666,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
**Parameters:**
|
||||
- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
|
||||
|
||||
**Default:** `0`
|
||||
**Default:** `2` on a factory-fresh node, then `0` (disabled) once configured.
|
||||
|
||||
**Note:** A new install ships with a 2 minute zero-hop advert interval. Saving
|
||||
any setting resets an interval below the 60 minute minimum to `0`, on the
|
||||
assumption that the node has now been deliberately configured. To keep zero-hop
|
||||
adverts running, set an explicit value in the 60-240 range.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+39
-15
@@ -206,7 +206,7 @@ Byte 1: 0x03
|
||||
**Command Format**:
|
||||
```
|
||||
Byte 0: 0x1F
|
||||
Byte 1: Channel Index (0-7)
|
||||
Byte 1: Channel Index (0 .. max_channels-1)
|
||||
```
|
||||
|
||||
**Example** (get channel 1):
|
||||
@@ -225,7 +225,7 @@ Byte 1: Channel Index (0-7)
|
||||
**Command Format**:
|
||||
```
|
||||
Byte 0: 0x20
|
||||
Byte 1: Channel Index (0-7)
|
||||
Byte 1: Channel Index (0 .. max_channels-1)
|
||||
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
|
||||
Bytes 34-49: Secret (16 bytes)
|
||||
```
|
||||
@@ -233,8 +233,12 @@ Bytes 34-49: Secret (16 bytes)
|
||||
**Total Length**: 50 bytes
|
||||
|
||||
**Channel Index**:
|
||||
- Index 0: Reserved for public channels (no secret)
|
||||
- Indices 1-7: Available for private channels
|
||||
- Valid range is `0` to `max_channels - 1`. Read `max_channels` from byte 3 of
|
||||
the `PACKET_DEVICE_INFO` response; do not assume 8. Builds ship with
|
||||
`MAX_GROUP_CHANNELS` set to 1, 8 or 40 depending on the board.
|
||||
- Index 0: pre-populated by the firmware with the built-in "Public" channel.
|
||||
It can be overwritten like any other slot.
|
||||
- An out-of-range index is rejected with `PACKET_ERROR` / `ERR_CODE_NOT_FOUND`.
|
||||
|
||||
**Channel Name**:
|
||||
- UTF-8 encoded
|
||||
@@ -242,8 +246,18 @@ Bytes 34-49: Secret (16 bytes)
|
||||
- Padded with null bytes (0x00) if shorter
|
||||
|
||||
**Secret Field** (16 bytes):
|
||||
- For **private channels**: 16-byte secret
|
||||
- For **public channels**: All zeros (0x00)
|
||||
- Always a real 16-byte key; the firmware derives the channel hash from
|
||||
`SHA256(secret)` (`BaseChatMesh.cpp:936-942`). There is no "no secret" form.
|
||||
- Do **not** write an all-zero secret. `SHA256` over 16 zero bytes is a fixed,
|
||||
globally identical value, so a zero-secret channel is a well-known channel
|
||||
with an all-zero AES key that any node can read — not a private one. The
|
||||
firmware skips *unnamed* slots when matching inbound group traffic for
|
||||
exactly this reason (`BaseChatMesh.cpp:392-401`), but a slot that has been
|
||||
given a name and a zero secret is not skipped, and will absorb null-key
|
||||
group traffic from any node on the mesh.
|
||||
- The built-in public channel uses the well-known key
|
||||
`izOH6cXN6mrJ5e26oRXNcg==` (base64), i.e.
|
||||
`8b3387e9c5cdea6ac9e5edbaa115cd72` in hex.
|
||||
|
||||
**Example** (create channel "YourChannelName" at index 1 with secret):
|
||||
```
|
||||
@@ -265,7 +279,7 @@ Bytes 34-49: Secret (16 bytes)
|
||||
```
|
||||
Byte 0: 0x03
|
||||
Byte 1: 0x00
|
||||
Byte 2: Channel Index (0-7)
|
||||
Byte 2: Channel Index (0 .. max_channels-1)
|
||||
Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)
|
||||
Bytes 7+: Message Text (UTF-8, variable length)
|
||||
```
|
||||
@@ -288,7 +302,7 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
||||
**Command Format**:
|
||||
```
|
||||
Byte 0: 0x3E
|
||||
Byte 1: Channel Index (0-7)
|
||||
Byte 1: Channel Index (0 .. max_channels-1)
|
||||
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
|
||||
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
|
||||
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
|
||||
@@ -306,13 +320,23 @@ Remaining bytes: Binary payload (variable length)
|
||||
- Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below.
|
||||
|
||||
**Limits**:
|
||||
- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes.
|
||||
- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`).
|
||||
- Maximum payload length is **165** bytes — `MAX_GROUP_DATA_LENGTH =
|
||||
MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3` (`src/MeshCore.h:21`). This is the
|
||||
radio-side limit and the one clients should enforce.
|
||||
- Two different bounds are checked, and they do not agree. The host-frame check
|
||||
uses the larger `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167`
|
||||
(`companion_radio/MyMesh.cpp:1265`); the radio-side check uses 165
|
||||
(`BaseChatMesh.cpp:544`).
|
||||
- Payloads **above 167** are rejected with `ERR_CODE_ILLEGAL_ARG` (6).
|
||||
- Payloads of **166 or 167** pass the frame check, then fail the radio-side
|
||||
check and return `ERR_CODE_TABLE_FULL` (3). Despite that code's usual
|
||||
meaning, such a send can never succeed — do not retry it. Keep payloads at
|
||||
165 bytes or fewer and this case cannot arise.
|
||||
|
||||
**Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of:
|
||||
- `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx`
|
||||
- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH`
|
||||
- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later
|
||||
- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` (167)
|
||||
- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full (retry later), **or** a payload of 166-167 bytes that exceeds the radio-side limit (permanent; see Limits above)
|
||||
|
||||
**Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram).
|
||||
|
||||
@@ -341,7 +365,7 @@ Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarde
|
||||
Byte 0: 0x1B (packet type)
|
||||
Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB)
|
||||
Bytes 2-3: Reserved (clients MUST ignore)
|
||||
Byte 4: Channel Index (0-7)
|
||||
Byte 4: Channel Index (0 .. max_channels-1)
|
||||
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
|
||||
Bytes 6-7: Data Type (uint16 little-endian)
|
||||
Byte 8: Data Length
|
||||
@@ -550,7 +574,7 @@ def parse_contact_message(data):
|
||||
**Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08):
|
||||
```
|
||||
Byte 0: 0x08 (packet type)
|
||||
Byte 1: Channel Index (0-7)
|
||||
Byte 1: Channel Index (0 .. max_channels-1)
|
||||
Byte 2: Path Length
|
||||
Byte 3: Text Type
|
||||
Bytes 4-7: Timestamp (32-bit little-endian)
|
||||
@@ -562,7 +586,7 @@ Bytes 8+: Message Text (UTF-8)
|
||||
Byte 0: 0x11 (packet type)
|
||||
Byte 1: SNR (signed byte, multiplied by 4)
|
||||
Bytes 2-3: Reserved
|
||||
Byte 4: Channel Index (0-7)
|
||||
Byte 4: Channel Index (0 .. max_channels-1)
|
||||
Byte 5: Path Length
|
||||
Byte 6: Text Type
|
||||
Bytes 7-10: Timestamp (32-bit little-endian)
|
||||
|
||||
+9
-9
@@ -537,18 +537,18 @@ Mac: python3 should be already installed.
|
||||
|
||||
Then it should be the same for all platforms:
|
||||
```
|
||||
python3 -m venv meshcore
|
||||
cd meshcore && source bin/activate
|
||||
python3 -m venv meshcore-venv
|
||||
cd meshcore-venv && source bin/activate
|
||||
pip install -U platformio
|
||||
git clone https://github.com/ripplebiz/MeshCore.git
|
||||
cd MeshCore
|
||||
git clone https://github.com/meshcore-dev/meshcore.git
|
||||
cd meshcore
|
||||
```
|
||||
open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5`
|
||||
save, then run:
|
||||
open platformio.ini and in `[arduino_base]` edit `LORA_FREQ` (it defaults to
|
||||
`869.618`) to the frequency for your region, save, then run:
|
||||
```
|
||||
pio run -e RAK_4631_Repeater
|
||||
pio run -e RAK_4631_repeater
|
||||
```
|
||||
then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater`
|
||||
then you'll find `firmware.zip` in `.pio/build/RAK_4631_repeater`
|
||||
|
||||
### 5.10. Q: Are there other MeshCore related open source projects?
|
||||
|
||||
@@ -592,7 +592,7 @@ For ESP-based devices (e.g. Heltec V3) you need:
|
||||
1. Download the firmware file from <https://flasher.meshcore.io>.
|
||||
- Go to the website in a browser and find the section that has the firmware you need.
|
||||
- Click the Download button, right-click on the file you need, for example:
|
||||
- `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin`
|
||||
- `Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin`
|
||||
- Non-merged bin keeps the existing Bluetooth pairing database.
|
||||
- `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin`
|
||||
- Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations.
|
||||
|
||||
@@ -18,10 +18,10 @@ Standard KISS framing per the KA9Q/K3MC specification.
|
||||
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
|
||||
|
||||
```
|
||||
┌──────┬───────────┬──────────────┬──────┐
|
||||
│ FEND │ Type Byte │ Data (escaped)│ FEND │
|
||||
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
|
||||
└──────┴───────────┴──────────────┴──────┘
|
||||
┌──────┬───────────┬────────────────┬──────┐
|
||||
│ FEND │ Type Byte │ Data (escaped) │ FEND │
|
||||
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
|
||||
└──────┴───────────┴────────────────┴──────┘
|
||||
```
|
||||
|
||||
### Type Byte
|
||||
@@ -82,10 +82,10 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The
|
||||
### Frame Format
|
||||
|
||||
```
|
||||
┌──────┬──────┬─────────────┬──────────────┬──────┐
|
||||
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
|
||||
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
|
||||
└──────┴──────┴─────────────┴──────────────┴──────┘
|
||||
┌──────┬──────┬─────────────┬────────────────┬──────┐
|
||||
│ FEND │ 0x06 │ Sub-command │ Data (escaped) │ FEND │
|
||||
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
|
||||
└──────┴──────┴─────────────┴────────────────┴──────┘
|
||||
```
|
||||
|
||||
### Request Sub-commands (Host to TNC)
|
||||
|
||||
@@ -193,11 +193,19 @@ Power management status can be queried via the CLI:
|
||||
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
|
||||
| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
|
||||
|
||||
On boards without power management enabled, all commands except `get pwrmgt.support` return:
|
||||
On boards without power management enabled, `get pwrmgt.source` and
|
||||
`get pwrmgt.bootmv` return:
|
||||
```
|
||||
ERROR: Power management not supported
|
||||
```
|
||||
|
||||
`get pwrmgt.support` returns `unsupported`. `get pwrmgt.bootreason` is not
|
||||
compiled out at all and answers on every board: `getResetReason()` and
|
||||
`getShutdownReason()` are virtuals on the base board class, so a board that
|
||||
does not override them reports `Not available` rather than an error. ESP32
|
||||
boards override the reset half with `esp_reset_reason()`, so they return a real
|
||||
reset reason and `Not available` for the shutdown reason.
|
||||
|
||||
## Debug Output
|
||||
|
||||
When `MESH_DEBUG=1` is enabled, the power management module outputs:
|
||||
|
||||
@@ -19,6 +19,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist
|
||||
| 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 |
|
||||
| 0140 | Nelos | support@nelos.app — https://nelos.app |
|
||||
| FF00 - FFFF | -reserved for testing/dev- | |
|
||||
|
||||
(add rows, inside the range 0100 - FEFF for custom apps)
|
||||
|
||||
+46
-9
@@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the
|
||||
| Field | Size (bytes) | Description |
|
||||
|--------------|-----------------|------------------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | request sub type |
|
||||
| request data | rest of payload | application-defined request payload body |
|
||||
|
||||
For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
|
||||
@@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v
|
||||
| Value | Name | Description |
|
||||
|--------|-----------|----------------------------------------------------|
|
||||
| `0x01` | get stats | get stats of repeater or room server |
|
||||
| `0x02` | keepalive | keep-alive request used for maintained connections |
|
||||
| `0x02` | keepalive | (deprecated) |
|
||||
| `0x03` | get telemetry | request node telemetry |
|
||||
| `0x04` | get min/max/avg | get sensor node stats on time series data |
|
||||
| `0x05` | get acl | node ACL query |
|
||||
| `0x06` | get neighbors | node neighbors query |
|
||||
| `0x08` | subscribe | subscribe to telemetry push |
|
||||
| `0x09` | ubsubscribe | unsubscribe from telemetry push |
|
||||
|
||||
#### Get stats
|
||||
|
||||
@@ -125,21 +132,50 @@ Gets information about the node, possibly including the following:
|
||||
* Number posted (?)
|
||||
* Number of post pushes (?)
|
||||
|
||||
#### Get telemetry data
|
||||
|
||||
Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
|
||||
|
||||
#### Get Telemetry
|
||||
|
||||
Not defined in `BaseChatMesh`.
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|--------------|------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | 0x03 (request sub type) |
|
||||
| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) |
|
||||
|
||||
#### Get Min/Max/Ave (Sensor nodes)
|
||||
|
||||
Not defined in `BaseChatMesh`.
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|--------------|------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | 0x04 (request sub type) |
|
||||
| start | 4 | starting time, seconds ago |
|
||||
| end | 4 | ending time, seconds ago |
|
||||
| reserved | 2 | should be zeroes |
|
||||
|
||||
#### Subscribe to Telemetry push - (Sensor nodes)
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|--------------|------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | 0x08 (request sub type) |
|
||||
| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads |
|
||||
| timeout secs | 2 | subscription timeout (seconds) |
|
||||
| reserved | 1 | should be zero |
|
||||
| min deltas len | 1 | byte length of LPP encoded min_deltas |
|
||||
| min deltas | (variable) | LPP encoded min_deltas |
|
||||
|
||||
#### Unsubscribe from Telemetry push - (Sensor nodes)
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|--------------|------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | 0x09 (request sub type) |
|
||||
|
||||
#### Get Access List
|
||||
|
||||
Not defined in `BaseChatMesh`.
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|--------------|------------------------------|
|
||||
| timestamp | 4 | sender time (unix timestamp) |
|
||||
| req type | 1 | 0x05 (request sub type) |
|
||||
| reserved | 2 | should be zeroes |
|
||||
|
||||
#### Get Neighbors
|
||||
|
||||
@@ -171,8 +207,9 @@ txt_type
|
||||
| Value | Description | Message content |
|
||||
|--------|---------------------------|--------------------------------------------------------------------------|
|
||||
| `0x00` | plain text message | the plain text of the message |
|
||||
| `0x01` | CLI command | the command text of the message |
|
||||
| `0x01` | CLI data | CLI command OR reply text |
|
||||
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
|
||||
| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) |
|
||||
|
||||
## Anonymous request
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Radio Presets
|
||||
|
||||
The radio presets are currently stored on the MeshCore App API server.
|
||||
|
||||
This allows us to update the presets without releasing new application updates.
|
||||
|
||||
The presets can be fetched from [https://api.meshcore.nz/api/v1/config](https://api.meshcore.nz/api/v1/config)
|
||||
|
||||
If you'd like a new radio preset to be added, this will need to be brought up for discussion in the [MeshCore Discord](https://meshcore.gg) server, or via the [issues section](https://github.com/meshcore-dev/MeshCore/issues) in our GitHub repo.
|
||||
|
||||
Before a new preset will be considered for being added, local radio regulations must be researched by the person suggesting the preset, and investigation of the frequencies with an SDR is advised to ensure the least interference to provide a decent user experience.
|
||||
|
||||
Once a preset is added, it's unlikely to be changed. Adding too many presets can lead to user frustration, as new users won't know which preset to select during setup.
|
||||
|
||||
At this time, presets provided by the MeshCore App API are just suggestions, but you are free to configure any radio settings through the companion applications, provided they comply with local laws/regulations.
|
||||
|
||||
> Note: We are planning to implement deep linking such as meshcore:// URLs, and QR codes so local mesh networks can easily distribute their own settings, without relying on the internet, or all the companion applications to provide all the settings.
|
||||
|
||||
## JSON Response
|
||||
|
||||
See the example JSON response below, which shows the expected format provided by the API server.
|
||||
|
||||
Your application should be programmed to expect any of the JSON keys to be missing from the response, as the format could change at any time without any notification.
|
||||
|
||||
Please configure an appropriate User Agent header string when sending your request to the server. We should be able to reach out to you if needed.
|
||||
|
||||
> Note: Some presets contain an optional `network_settings` section which defines the `path_hash_size` to use for this mesh network.
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"suggested_radio_settings": {
|
||||
"info_message": "These presets are suggested by the community.",
|
||||
"entries": [
|
||||
{
|
||||
"title": "Australia (Narrow)",
|
||||
"description": "916.575MHz / SF7 / BW62.5 / CR7",
|
||||
"frequency": "916.575",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "7"
|
||||
},
|
||||
{
|
||||
"title": "New Zealand (Narrow)",
|
||||
"description": "917.375MHz / SF7 / BW62.5 / CR5 / 2B",
|
||||
"frequency": "917.375",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "5",
|
||||
"network_settings": {
|
||||
"path_hash_size": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Portugal 868",
|
||||
"description": "869.618MHz / SF7 / BW62.5 / CR6",
|
||||
"frequency": "869.618",
|
||||
"spreading_factor": "7",
|
||||
"bandwidth": "62.5",
|
||||
"coding_rate": "6"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -27,15 +27,17 @@ set lon {longitude}
|
||||
```
|
||||
Sets your advertisement map longitude. (decimal degrees)
|
||||
|
||||
```
|
||||
set dutycycle {percent}
|
||||
```
|
||||
Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%.
|
||||
|
||||
```
|
||||
set af {air-time-factor}
|
||||
```
|
||||
Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead.
|
||||
Sets the transmit air-time-factor.
|
||||
|
||||
> **Note:** `set dutycycle` is *not* available in Terminal Chat. Every other
|
||||
> firmware — Repeater, Room Server, Sensor and Companion Radio — routes its
|
||||
> `set`/`get` commands through the shared radio prefs handler and does support
|
||||
> it (see [CLI Commands](./cli_commands.md)). Terminal Chat is the sole
|
||||
> exception: it has its own inline `set` handler covering only the six options
|
||||
> listed above.
|
||||
|
||||
|
||||
```
|
||||
@@ -99,3 +101,8 @@ Resets the path to current recipient, for new path discovery.
|
||||
public {text}
|
||||
```
|
||||
Sends the text message to the built-in 'public' group channel
|
||||
|
||||
```
|
||||
help
|
||||
```
|
||||
Lists the available commands.
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "Features.h"
|
||||
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#ifdef ENABLE_WIFI_INTERFACE
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#include <Mesh.h>
|
||||
|
||||
#include <helpers/UTF8Helpers.h>
|
||||
@@ -695,8 +698,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
||||
tryBotReplyDM(from, text, hops);
|
||||
}
|
||||
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) {
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
|
||||
// If the on-device Admin screen sent this command (not the BLE/USB app's CLI
|
||||
@@ -711,6 +713,17 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3
|
||||
|
||||
// Oldest room post the bot still answers (see onSignedMessageRecv()).
|
||||
#define BOT_ROOM_MAX_AGE_SECS 300
|
||||
void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text, char* reply) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
if (from.isRemoteCLIAllowed()) {
|
||||
if (!handleCommand(text, sender_timestamp, reply)) {
|
||||
strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text'
|
||||
}
|
||||
} else {
|
||||
queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const uint8_t *sender_prefix, const char *text) {
|
||||
@@ -1008,8 +1021,21 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
memcpy(&out_frame[i], &data[4], len - 4);
|
||||
i += (len - 4);
|
||||
_serial->writeFrame(out_frame, i);
|
||||
} else if (_listener && len > 4) {
|
||||
_listener->onUnhandledResponse(contact, tag, &data[4], len - 4);
|
||||
} else {
|
||||
bool handled;
|
||||
if (_listener && len > 4) {
|
||||
handled = _listener->onUnhandledResponse(contact, tag, &data[4], len - 4);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
if (!handled) { // let the app know about unknown/unhandled responses (eg. push telemetry packets)
|
||||
int i = 0;
|
||||
out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
|
||||
out_frame[i++] = 0; // reserved
|
||||
memcpy(&out_frame[i], data, len);
|
||||
i += len;
|
||||
_serial->writeFrame(out_frame, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1290,6 +1316,29 @@ void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||
if (_listener) _listener->onACKRecv(ack_crc);
|
||||
}
|
||||
|
||||
// The board's own settings (upstream's dynamic prefs, a key-value store), over
|
||||
// the NodePrefs fields that hold them here: a board with a LoRa front end
|
||||
// reads "fem_rxgain" / "fem_txgain" at begin() and writes them from its CLI.
|
||||
bool MyMesh::BoardPrefs::getByKey(const char* key, char* value, size_t max_len) {
|
||||
uint8_t* f = field(key);
|
||||
if (!f || max_len < 2) return false;
|
||||
value[0] = *f ? '1' : '0';
|
||||
value[1] = 0;
|
||||
return true;
|
||||
}
|
||||
bool MyMesh::BoardPrefs::setByKey(const char* key, const char* value) {
|
||||
uint8_t* f = field(key);
|
||||
if (!f) return false;
|
||||
*f = strcmp(value, "1") == 0 ? 1 : 0;
|
||||
_mesh->savePrefs();
|
||||
return true;
|
||||
}
|
||||
uint8_t* MyMesh::BoardPrefs::field(const char* key) {
|
||||
if (strcmp(key, "fem_rxgain") == 0) return &_mesh->_prefs.radio_fem_rxgain;
|
||||
if (strcmp(key, "fem_txgain") == 0) return &_mesh->_prefs.radio_fem_txgain;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
|
||||
// Sized to match simple_repeater's pool (32), not the old client-only 16: with the
|
||||
// on-device Repeater toggle, queued retransmits (adverts/channel flood from
|
||||
@@ -1298,9 +1347,10 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
// silently drops every incoming packet — DMs and channels included — until a slot
|
||||
// frees up.
|
||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) {
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0), _board_prefs(this) {
|
||||
_iter_started = false;
|
||||
_cli_rescue = false;
|
||||
cli_command[0] = 0;
|
||||
for (int i = 0; i < RELAY_RING; i++) _relay[i].pending = false;
|
||||
_relay_head = 0;
|
||||
_relay_active = 0;
|
||||
@@ -1356,7 +1406,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
||||
_prefs.radio_fem_rxgain = 1;
|
||||
#ifdef RADIO_FEM_TXGAIN
|
||||
_prefs.radio_fem_txgain = RADIO_FEM_TXGAIN; // board-specific default for PA
|
||||
#else
|
||||
_prefs.radio_fem_txgain = 0;
|
||||
#endif
|
||||
#ifdef DISPLAY_ROTATION
|
||||
_prefs.display_rotation = DISPLAY_ROTATION;
|
||||
#endif
|
||||
@@ -1486,8 +1540,7 @@ void MyMesh::begin() {
|
||||
#else
|
||||
radio_driver.setPowerSaving(false); // see Features.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
||||
#endif
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
board.attachDynamicPrefs(&_board_prefs); // FEM gains: the board reads them now, and its CLI sets them
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
}
|
||||
@@ -1631,16 +1684,16 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
uint8_t *pub_key_prefix = &cmd_frame[i];
|
||||
i += 6;
|
||||
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6);
|
||||
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
|
||||
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) {
|
||||
char *text = (char *)&cmd_frame[i];
|
||||
int tlen = len - i;
|
||||
uint32_t est_timeout;
|
||||
text[tlen] = 0; // ensure null
|
||||
int result;
|
||||
uint32_t expected_ack;
|
||||
if (txt_type == TXT_TYPE_CLI_DATA) {
|
||||
if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) {
|
||||
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
|
||||
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
|
||||
result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout);
|
||||
expected_ack = 0; // no Ack expected
|
||||
} else {
|
||||
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
|
||||
@@ -1752,8 +1805,8 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
|
||||
} else if (data_type == DATA_TYPE_RESERVED) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
} else if (payload_len > MAX_CHANNEL_DATA_LENGTH) {
|
||||
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH);
|
||||
} else if (payload_len > MAX_GROUP_DATA_LENGTH) {
|
||||
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH);
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
} else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) {
|
||||
writeOKFrame();
|
||||
@@ -2670,6 +2723,14 @@ void MyMesh::enterCLIRescue() {
|
||||
Serial.println("========= CLI Rescue =========");
|
||||
}
|
||||
|
||||
// Upstream's CLI entry (the rescue CLI, a remote contact's CLI message): our
|
||||
// companion CLI (MyMeshCli.h) answers, as it does CMD_RUN_CLI_COMMAND. `reply`
|
||||
// is upstream's buffer: 160 bytes free.
|
||||
bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
(void)sender_timestamp;
|
||||
return handleCliCommand(command, reply, 160);
|
||||
}
|
||||
|
||||
void MyMesh::checkCLIRescueCmd() {
|
||||
int len = strlen(cli_command);
|
||||
// `cli_command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||
@@ -2695,15 +2756,10 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
||||
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
||||
|
||||
if (memcmp(cli_command, "set ", 4) == 0) {
|
||||
const char* config = &cli_command[4];
|
||||
if (memcmp(config, "pin ", 4) == 0) {
|
||||
_prefs.ble_pin = atoi(&config[4]);
|
||||
savePrefs();
|
||||
Serial.printf(" > pin is now %06d\n", _prefs.ble_pin);
|
||||
} else {
|
||||
Serial.printf(" Error: unknown config: %s\n", config);
|
||||
}
|
||||
reply_buf[0] = 0;
|
||||
if (handleCommand(cli_command, 0, reply_buf)) {
|
||||
// command was handled, print reply output
|
||||
Serial.print(" "); Serial.print(reply_buf); Serial.println();
|
||||
} else if (strcmp(cli_command, "rebuild") == 0) {
|
||||
bool success = _store->formatFileSystem();
|
||||
if (success) {
|
||||
@@ -2907,6 +2963,11 @@ void MyMesh::loop() {
|
||||
checkCLIRescueCmd();
|
||||
} else {
|
||||
checkSerialInterface();
|
||||
#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE)
|
||||
// RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a
|
||||
// display + long-press), so serve config commands on the otherwise unused USB serial
|
||||
checkCLIRescueCmd();
|
||||
#endif
|
||||
}
|
||||
|
||||
// is there are pending dirty contacts write needed?
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include "Features.h" // FEAT_RX_POWERSAVE, used by MyMesh.cpp
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
// Forward declaration for UITask
|
||||
class UITask;
|
||||
@@ -83,6 +84,10 @@ class UITask;
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
|
||||
// Copied from simple_repeater
|
||||
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
|
||||
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
|
||||
|
||||
struct AdvertPath {
|
||||
uint8_t pubkey_prefix[7];
|
||||
uint8_t path_len;
|
||||
@@ -126,7 +131,7 @@ public:
|
||||
virtual void onACKRecv(uint32_t ack_crc) { }
|
||||
virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
|
||||
uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ }
|
||||
virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { }
|
||||
virtual bool onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { return false; /* not handled */ }
|
||||
virtual void onTraceRecv(mesh::Packet *pkt, 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 onRawDataRecv(mesh::Packet *pkt) { }
|
||||
@@ -316,6 +321,8 @@ protected:
|
||||
const char *text) override;
|
||||
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) override;
|
||||
void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text, char* reply) override;
|
||||
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const uint8_t *sender_prefix, const char *text) override;
|
||||
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
||||
@@ -448,7 +455,7 @@ public:
|
||||
// reply is a text frame (TXT_TYPE_CLI_DATA) delivered via onCommandDataRecv();
|
||||
// this just tracks which contact's reply AdminScreen is currently waiting on.
|
||||
bool sendAdminCommand(const ContactInfo& contact, const char* cmd_text, uint32_t& est_timeout) {
|
||||
if (sendCommandData(contact, rtc_clock.getCurrentTime(), 0, cmd_text, est_timeout) == MSG_SEND_FAILED)
|
||||
if (sendCommandData(contact, rtc_clock.getCurrentTime(), 0, TXT_TYPE_CLI_DATA, cmd_text, est_timeout) == MSG_SEND_FAILED)
|
||||
return false;
|
||||
memcpy(&ui_pending_admin_reply, contact.id.pub_key, 4);
|
||||
return true;
|
||||
@@ -634,6 +641,7 @@ private:
|
||||
|
||||
void checkCLIRescueCmd();
|
||||
bool cliRadioCommand(const char* command, char* reply, int n);
|
||||
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
|
||||
void checkSerialInterface();
|
||||
bool isValidClientRepeatFreq(uint32_t f) const;
|
||||
#ifdef ENABLE_SCREENSHOT
|
||||
@@ -690,6 +698,16 @@ private:
|
||||
uint32_t _last_relay_seq; // seq of the most recent tracked send (for the UI to record)
|
||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||
char cli_command[80];
|
||||
char reply_buf[166];
|
||||
class BoardPrefs : public KeyValueStore { // MyMesh.cpp
|
||||
MyMesh* _mesh;
|
||||
uint8_t* field(const char* key);
|
||||
public:
|
||||
BoardPrefs(MyMesh* mesh) : _mesh(mesh) { }
|
||||
bool setByKey(const char* key, const char* value) override;
|
||||
bool getByKey(const char* key, char* value, size_t max_len) override;
|
||||
};
|
||||
BoardPrefs _board_prefs;
|
||||
uint8_t app_target_ver;
|
||||
uint8_t *sign_data;
|
||||
uint32_t sign_data_len;
|
||||
|
||||
@@ -21,6 +21,7 @@ bool MyMesh::handleCliCommand(const char* command, char* reply, int n) {
|
||||
command += 3;
|
||||
}
|
||||
if (cliRadioCommand(command, reply, n)) return true;
|
||||
if (n >= 64 && board.handleCommand(command, 0, reply)) return true; // the board's own (radio.fem.*); short replies
|
||||
|
||||
if (strcmp(command, "reboot") == 0 || strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
||||
bool restart = command[0] == 'r';
|
||||
|
||||
@@ -372,6 +372,7 @@ void loop() {
|
||||
ui_task.loop();
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
board.loop();
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
|
||||
@@ -113,7 +113,7 @@ void setup() {
|
||||
uint32_t start = millis();
|
||||
while (!Serial && millis() - start < 3000) delay(10);
|
||||
delay(100);
|
||||
#if defined(ESP32) && ARDUINO_USB_MODE
|
||||
#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
|
||||
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
|
||||
#endif
|
||||
@@ -149,6 +149,8 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
board.loop();
|
||||
|
||||
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
|
||||
radio_driver.triggerNoiseFloorCalibrate(0);
|
||||
next_noise_floor_calib_ms = millis();
|
||||
|
||||
@@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
int results_offset = 0;
|
||||
uint8_t results_buffer[130];
|
||||
for(int index = 0; index < count && index + offset < neighbours_count; index++){
|
||||
|
||||
|
||||
// stop if we can't fit another entry in results
|
||||
int entry_size = pubkey_prefix_length + 4 + 1;
|
||||
if(results_offset + entry_size > sizeof(results_buffer)){
|
||||
@@ -704,7 +704,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
@@ -790,6 +790,7 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
|
||||
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
|
||||
&& !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime())
|
||||
&& !isHiddenNode() // this node wishes to NOT be discoverable
|
||||
) {
|
||||
int i = 1;
|
||||
uint8_t filter = packet->payload[i++];
|
||||
@@ -927,6 +928,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
_prefs.flood_max_advert = 8;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
_prefs.loop_detect = LOOP_DETECT_MINIMAL;
|
||||
|
||||
// bridge defaults
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
@@ -1001,8 +1003,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -1193,7 +1195,7 @@ void MyMesh::formatRadioStatsReply(char *reply) {
|
||||
}
|
||||
|
||||
void MyMesh::formatPacketStatsReply(char *reply) {
|
||||
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
||||
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
||||
getNumRecvFlood(), getNumRecvDirect());
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,6 @@ struct RepeaterStats {
|
||||
uint32_t n_recv_errors;
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
struct NeighbourInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t advert_timestamp;
|
||||
@@ -71,11 +67,11 @@ struct NeighbourInfo {
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.17.1"
|
||||
#define FIRMWARE_VERSION "v1.18.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
@@ -191,6 +187,9 @@ public:
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
bool isHiddenNode() const {
|
||||
return _prefs.advert_interval == 0 && _prefs.flood_advert_interval == 0;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
|
||||
@@ -244,6 +244,8 @@ void loop() {
|
||||
command[0] = 0; // reset command buffer
|
||||
}
|
||||
|
||||
board.loop(); // let the board feed its watchdog, run periodic housekeeping
|
||||
|
||||
#ifdef ETHERNET_ENABLED
|
||||
ethernet_loop_maintain();
|
||||
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
|
||||
|
||||
@@ -443,7 +443,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
@@ -463,7 +463,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
|
||||
uint8_t temp[166];
|
||||
bool send_ack;
|
||||
if (flags == TXT_TYPE_CLI_DATA) {
|
||||
if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
||||
if (client->isAdmin()) {
|
||||
if (is_retry) {
|
||||
temp[5] = 0; // no reply
|
||||
@@ -725,8 +725,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -988,6 +988,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
addSystemPost(msg);
|
||||
snprintf(reply, MAX_POST_TEXT_LEN, "OK");
|
||||
}
|
||||
} else if (memcmp(command, "set repeat on", 13) == 0) {
|
||||
strcpy(reply, "ERR - repeat now deprecated");
|
||||
} else{
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.17.1"
|
||||
#define FIRMWARE_VERSION "v1.18.0"
|
||||
#endif
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
|
||||
@@ -199,6 +199,7 @@ void loop() {
|
||||
ui_task.loop();
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
board.loop();
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
|
||||
@@ -245,6 +245,10 @@ protected:
|
||||
|
||||
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
|
||||
}
|
||||
|
||||
void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override {
|
||||
}
|
||||
|
||||
void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "SensorMesh.h"
|
||||
#include <helpers/RoutingPolicy.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
@@ -54,6 +56,10 @@
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
||||
#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present)
|
||||
|
||||
#define REQ_TYPE_SUBSCRIBE 0x08
|
||||
#define REQ_TYPE_UNSUBSCRIBE 0x09
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
@@ -73,104 +79,70 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t getDataSize(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
return 9;
|
||||
case LPP_POLYLINE:
|
||||
return 8; // TODO: this is MINIMIUM
|
||||
case LPP_GYROMETER:
|
||||
case LPP_ACCELEROMETER:
|
||||
return 6;
|
||||
case LPP_GENERIC_SENSOR:
|
||||
case LPP_FREQUENCY:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
case LPP_UNIXTIME:
|
||||
return 4;
|
||||
case LPP_COLOUR:
|
||||
return 3;
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
case LPP_LUMINOSITY:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
case LPP_DIRECTION:
|
||||
case LPP_POWER:
|
||||
return 2;
|
||||
/* --------------------- Cayenne LPP helpers ----------------------------*/
|
||||
|
||||
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
|
||||
uint8_t i = 0;
|
||||
|
||||
while (i + 2 < size) {
|
||||
// Get channel #
|
||||
uint8_t ch = buf[i++];
|
||||
// Get data type
|
||||
uint8_t t = buf[i++];
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
|
||||
if (ch == channel && t == type) {
|
||||
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint32_t getMultiplier(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_CURRENT:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
return 1000;
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
return 100;
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return 10;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool isSigned(uint8_t type) {
|
||||
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
|
||||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
||||
}
|
||||
|
||||
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
uint32_t value = 0;
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
value = (value << 8) + buffer[i];
|
||||
i += sz; // skip
|
||||
}
|
||||
return def_value; // not found
|
||||
}
|
||||
|
||||
int sign = 1;
|
||||
if (is_signed) {
|
||||
uint32_t bit = 1ul << ((size * 8) - 1);
|
||||
if ((value & bit) == bit) {
|
||||
value = (bit << 1) - value;
|
||||
sign = -1;
|
||||
/* ------------------ end Cayenne LPP helpers ----------------------*/
|
||||
|
||||
bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
||||
auto buf = telemetry.getBuffer();
|
||||
uint8_t size = telemetry.getSize();
|
||||
uint8_t i = 0;
|
||||
bool changed = false;
|
||||
|
||||
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
|
||||
float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
|
||||
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||
if (abs(v - pv) > min_delta) changed = true; // Yes, has changed
|
||||
|
||||
i += 2 + sz; // skip
|
||||
}
|
||||
if (changed) {
|
||||
// take snapshot of all _monitored_ telem values, for next cycle
|
||||
i = 0;
|
||||
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
|
||||
c->extra.sensor.prev_telem[i] = ch;
|
||||
c->extra.sensor.prev_telem[i + 1] = t;
|
||||
|
||||
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||
LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
|
||||
i += 2 + sz; // skip
|
||||
}
|
||||
}
|
||||
return sign * ((float) value / multiplier);
|
||||
return changed;
|
||||
}
|
||||
|
||||
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
// check sign
|
||||
bool sign = value < 0;
|
||||
if (sign) value = -value;
|
||||
uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
||||
uint8_t perms = from->isAdmin() ? 0xFF : from->permissions;
|
||||
|
||||
// get value to store
|
||||
uint32_t v = value * multiplier;
|
||||
|
||||
// format an uint32_t as if it was an int32_t
|
||||
if (is_signed & sign) {
|
||||
uint32_t mask = (1 << (size * 8)) - 1;
|
||||
v = v & mask;
|
||||
if (sign) v = mask - v + 1;
|
||||
}
|
||||
|
||||
// add bytes (MSB first)
|
||||
for (uint8_t i=1; i<=size; i++) {
|
||||
dest[size - i] = (v & 0xFF);
|
||||
v >>= 8;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
|
||||
@@ -181,6 +153,10 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
||||
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
||||
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
|
||||
}
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
@@ -211,12 +187,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
||||
auto d = &data[i];
|
||||
reply_data[ofs++] = d->_channel;
|
||||
reply_data[ofs++] = d->_lpp_type;
|
||||
uint8_t sz = getDataSize(d->_lpp_type);
|
||||
uint32_t mult = getMultiplier(d->_lpp_type);
|
||||
bool is_signed = isSigned(d->_lpp_type);
|
||||
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
||||
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
||||
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
||||
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
||||
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
|
||||
bool is_signed = LPPData::isSigned(d->_lpp_type);
|
||||
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
||||
ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
||||
ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
||||
}
|
||||
return ofs;
|
||||
}
|
||||
@@ -234,6 +210,45 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
||||
return ofs;
|
||||
}
|
||||
}
|
||||
if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||
memcpy(&from->extra.sensor.push_tag, &payload[0], 4);
|
||||
uint16_t timeout_secs;
|
||||
memcpy(&timeout_secs, &payload[4], 2);
|
||||
uint8_t reserved = payload[6];
|
||||
uint8_t min_deltas_len = payload[7];
|
||||
RegionEntry* r;
|
||||
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope
|
||||
r = recv_pkt_region;
|
||||
} else { // use default scope
|
||||
r = region_map.getDefaultRegion();
|
||||
}
|
||||
uint8_t reply_len;
|
||||
if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) {
|
||||
from->extra.sensor.scope_region_id = r->id;
|
||||
from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs;
|
||||
from->extra.sensor.min_deltas_len = min_deltas_len;
|
||||
memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len);
|
||||
// reply with actual expiry seconds (we could modify/impose restriction)
|
||||
memcpy(&reply_data[4], &timeout_secs, 2);
|
||||
memset(&reply_data[6], 0, 2); // reserved
|
||||
strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used
|
||||
reply_len = 8 + strlen((char *)&reply_data[8]);
|
||||
} else {
|
||||
memset(&reply_data[4], 0, 4); // expiry secs (0 for error)
|
||||
reply_len = 8;
|
||||
}
|
||||
return reply_len;
|
||||
}
|
||||
if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||
from->extra.sensor.scope_region_id = 0;
|
||||
from->extra.sensor.push_tag = 0;
|
||||
from->extra.sensor.expiry_timestamp = 0;
|
||||
from->extra.sensor.min_deltas_len = 0;
|
||||
// REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE?
|
||||
memset(&reply_data[4], 0, 8); // success
|
||||
getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness
|
||||
return 12 + 2;
|
||||
}
|
||||
return 0; // unknown command
|
||||
}
|
||||
|
||||
@@ -262,7 +277,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
|
||||
sendDirect(pkt, c->out_path, c->out_path_len);
|
||||
} else {
|
||||
unsigned long delay_millis = 0;
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
}
|
||||
}
|
||||
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
|
||||
@@ -330,6 +345,25 @@ int SensorMesh::getAGCResetInterval() const {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
|
||||
void SensorMesh::startRegionsLoad() {
|
||||
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
|
||||
memset(load_stack, 0, sizeof(load_stack));
|
||||
load_stack[0] = &temp_map.getWildcard();
|
||||
region_load_active = true;
|
||||
}
|
||||
|
||||
bool SensorMesh::saveRegions() {
|
||||
return region_map.save(_fs);
|
||||
}
|
||||
|
||||
void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) {
|
||||
if (r) {
|
||||
region_map.getTransportKeysFor(*r, &default_scope, 1);
|
||||
} else {
|
||||
memset(default_scope.key, 0, sizeof(default_scope.key));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
|
||||
ClientInfo* client;
|
||||
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
||||
@@ -343,7 +377,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
|
||||
} else {
|
||||
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -379,7 +413,41 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
|
||||
return 13; // reply length
|
||||
}
|
||||
|
||||
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) {
|
||||
if (region_load_active) {
|
||||
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
|
||||
region_map = temp_map; // copy over the temp instance as new current map
|
||||
region_load_active = false;
|
||||
|
||||
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
|
||||
} else {
|
||||
char *np = command;
|
||||
while (*np == ' ') np++; // skip indent
|
||||
int indent = np - command;
|
||||
|
||||
char *ep = np;
|
||||
while (RegionMap::is_name_char(*ep)) ep++;
|
||||
if (*ep) { *ep++ = 0; } // set null terminator for end of name
|
||||
|
||||
while (*ep && *ep != 'F') ep++; // look for (optional) flags
|
||||
|
||||
if (indent > 0 && indent < 8 && strlen(np) > 0) {
|
||||
auto parent = load_stack[indent - 1];
|
||||
if (parent) {
|
||||
auto old = region_map.findByName(np);
|
||||
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists)
|
||||
if (nw) {
|
||||
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr
|
||||
|
||||
load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's
|
||||
}
|
||||
}
|
||||
}
|
||||
reply[0] = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (*command == ' ') command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
@@ -400,10 +468,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
||||
if (sp == NULL) {
|
||||
strcpy(reply, "Err - bad params");
|
||||
} else {
|
||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
|
||||
*sp++ = 0; // replace space with null terminator
|
||||
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||
uint8_t perms = atoi(sp);
|
||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
||||
@@ -451,6 +520,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
||||
}
|
||||
}
|
||||
|
||||
mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) {
|
||||
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
||||
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
|
||||
if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
|
||||
recv_pkt_region = NULL;
|
||||
} else {
|
||||
recv_pkt_region = ®ion_map.getWildcard();
|
||||
}
|
||||
} else {
|
||||
recv_pkt_region = NULL;
|
||||
}
|
||||
return Mesh::onRecvPacket(pkt);
|
||||
}
|
||||
|
||||
void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
||||
uint32_t timestamp;
|
||||
@@ -472,10 +556,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
||||
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,7 +587,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
|
||||
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
|
||||
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size);
|
||||
if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size);
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
@@ -531,7 +615,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
||||
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
|
||||
uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5);
|
||||
if (reply_len == 0) return; // invalid command
|
||||
|
||||
from->last_timestamp = timestamp;
|
||||
@@ -541,14 +625,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,12 +655,12 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize());
|
||||
if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize());
|
||||
} else {
|
||||
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
||||
}
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
} else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
||||
from->last_timestamp = sender_timestamp;
|
||||
from->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
@@ -586,7 +670,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
uint8_t temp[166];
|
||||
char *command = (char *) &data[5];
|
||||
char *reply = (char *) &temp[5];
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
handleCommand(from, sender_timestamp, command, reply);
|
||||
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
@@ -601,7 +685,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (from->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
|
||||
sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
|
||||
} else {
|
||||
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
}
|
||||
@@ -699,7 +783,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||
|
||||
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
region_map(key_store),
|
||||
region_map(key_store), temp_map(key_store),
|
||||
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
||||
telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
{
|
||||
@@ -708,6 +792,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
last_read_time = 0;
|
||||
num_alert_tasks = 0;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
recv_pkt_region = NULL;
|
||||
region_load_active = false;
|
||||
|
||||
// defaults
|
||||
_prefs.airtime_factor = 1.0;
|
||||
@@ -771,8 +857,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -820,11 +906,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t
|
||||
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
|
||||
}
|
||||
|
||||
void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||
if (scope.isNull()) {
|
||||
sendFlood(pkt, delay_millis, path_hash_size);
|
||||
} else {
|
||||
uint16_t codes[2];
|
||||
codes[0] = scope.calcTransportCode(pkt);
|
||||
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
|
||||
sendFlood(pkt, codes, delay_millis, path_hash_size);
|
||||
}
|
||||
}
|
||||
|
||||
void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
|
||||
TransportKey req_scope;
|
||||
bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
|
||||
bool req_scope_known = recv_pkt_region != NULL && !is_wildcard
|
||||
&& region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0;
|
||||
|
||||
switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) {
|
||||
case mesh::REPLY_SCOPE_REQUEST:
|
||||
sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request
|
||||
break;
|
||||
case mesh::REPLY_SCOPE_DEFAULT:
|
||||
// requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region.
|
||||
// un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0
|
||||
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
|
||||
break;
|
||||
case mesh::REPLY_SCOPE_NONE:
|
||||
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
if (flood) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
} else {
|
||||
sendZeroHop(pkt, delay_millis);
|
||||
}
|
||||
@@ -867,23 +985,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) {
|
||||
}
|
||||
|
||||
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
||||
auto buf = telemetry.getBuffer();
|
||||
uint8_t size = telemetry.getSize();
|
||||
uint8_t i = 0;
|
||||
|
||||
while (i + 2 < size) {
|
||||
// Get channel #
|
||||
uint8_t ch = buf[i++];
|
||||
// Get data type
|
||||
uint8_t t = buf[i++];
|
||||
uint8_t sz = getDataSize(t);
|
||||
|
||||
if (ch == channel && t == type) {
|
||||
return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t));
|
||||
}
|
||||
i += sz; // skip
|
||||
}
|
||||
return 0.0f; // not found
|
||||
return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f);
|
||||
}
|
||||
|
||||
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
||||
@@ -903,7 +1005,7 @@ void SensorMesh::loop() {
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
unsigned long delay_millis = 0;
|
||||
if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
|
||||
updateFloodAdvertTimer(); // schedule next flood advert
|
||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||
@@ -932,6 +1034,39 @@ void SensorMesh::loop() {
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
|
||||
// This MCU temperature will be overridden by external sensors (if any)
|
||||
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
|
||||
}
|
||||
|
||||
// compare with previous telemetry, check if any deltas are greater than subscriber minimums
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas
|
||||
RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id);
|
||||
if (r == NULL) continue; // unknown region scope
|
||||
if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired
|
||||
if (telemHasChanged(c)) {
|
||||
TransportKey scope;
|
||||
if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) {
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(reply_data, &c->extra.sensor.push_tag, 4);
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(&reply_data[4], ×tamp, 4);
|
||||
memcpy(&reply_data[8], telemetry.getBuffer(), tlen);
|
||||
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen);
|
||||
if (reply) {
|
||||
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, c->out_path, c->out_path_len, 0);
|
||||
} else {
|
||||
sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSensorDataRead();
|
||||
|
||||
@@ -974,7 +1109,7 @@ void SensorMesh::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// is there are pending dirty contacts write needed?
|
||||
// pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs);
|
||||
dirty_contacts_expiry = 0;
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.17.1"
|
||||
#define FIRMWARE_VERSION "v1.18.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "sensor"
|
||||
|
||||
#define MAX_SEARCH_RESULTS 8
|
||||
#define MAX_SEARCH_RESULTS 24
|
||||
#define MAX_CONCURRENT_ALERTS 4
|
||||
|
||||
class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
void begin(FILESYSTEM* fs);
|
||||
void loop();
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply);
|
||||
|
||||
// CommonCLI callbacks
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
@@ -78,9 +78,15 @@ public:
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override { }
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
void startRegionsLoad() override;
|
||||
bool saveRegions() override;
|
||||
void onDefaultRegionChanged(const RegionEntry* r) override;
|
||||
|
||||
float getTelemValue(uint8_t channel, uint8_t type);
|
||||
|
||||
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
|
||||
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
||||
|
||||
protected:
|
||||
// current telemetry data queries
|
||||
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
|
||||
@@ -93,12 +99,12 @@ protected:
|
||||
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
|
||||
|
||||
// alerts
|
||||
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
||||
enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
||||
|
||||
struct Trigger {
|
||||
uint32_t timestamp;
|
||||
AlertPriority pri;
|
||||
uint32_t expected_acks[4];
|
||||
AlertPriority pri;
|
||||
int8_t curr_contact_idx;
|
||||
uint8_t attempt;
|
||||
unsigned long send_expiry;
|
||||
@@ -122,6 +128,7 @@ protected:
|
||||
int getInterferenceThreshold() const override;
|
||||
bool getCADEnabled() const override;
|
||||
int getAGCResetInterval() const 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;
|
||||
int searchPeersByHash(const uint8_t* hash) override;
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||
@@ -129,6 +136,7 @@ protected:
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onControlDataRecv(mesh::Packet* packet) override;
|
||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
||||
|
||||
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
|
||||
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
|
||||
private:
|
||||
@@ -141,7 +149,9 @@ private:
|
||||
unsigned long dirty_contacts_expiry;
|
||||
CayenneLPP telemetry;
|
||||
TransportKeyStore key_store;
|
||||
RegionMap region_map;
|
||||
RegionMap region_map, temp_map;
|
||||
RegionEntry* recv_pkt_region;
|
||||
RegionEntry* load_stack[8];
|
||||
TransportKey default_scope;
|
||||
uint32_t last_read_time;
|
||||
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
||||
@@ -152,9 +162,11 @@ private:
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
bool region_load_active;
|
||||
|
||||
bool telemHasChanged(ClientInfo* c);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
|
||||
uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
|
||||
void sendAlert(const ClientInfo* c, Trigger* t);
|
||||
|
||||
@@ -145,7 +145,7 @@ void loop() {
|
||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||
command[len - 1] = 0; // replace newline with C string null terminator
|
||||
char reply[160];
|
||||
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
|
||||
the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial!
|
||||
if (reply[0]) {
|
||||
Serial.print(" -> "); Serial.println(reply);
|
||||
}
|
||||
@@ -153,6 +153,8 @@ void loop() {
|
||||
command[0] = 0; // reset command buffer
|
||||
}
|
||||
|
||||
board.loop(); // let the board feed its watchdog, run periodic housekeeping
|
||||
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
+6
-2
@@ -86,7 +86,8 @@ platform_packages =
|
||||
; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1177
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1295
|
||||
framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301
|
||||
framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301665b40959682252911e57b11df3ee651a
|
||||
platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
extra_scripts = create-uf2.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
@@ -118,7 +119,9 @@ build_flags = ${arduino_base.build_flags}
|
||||
|
||||
[stm32_base]
|
||||
extends = arduino_base
|
||||
platform = ststm32
|
||||
; Pinned pending upstream fix for upstream conversions.
|
||||
; https://github.com/jgromes/RadioLib/issues/1873
|
||||
platform = ststm32@19.7.1
|
||||
extra_scripts = post:arch/stm32/build_hex.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D STM32_PLATFORM
|
||||
@@ -179,6 +182,7 @@ build_src_filter =
|
||||
+<../src/Utils.cpp>
|
||||
+<../src/Packet.cpp>
|
||||
+<../src/helpers/ConfigSerializer.cpp>
|
||||
+<../src/helpers/DynamicConfigSerializer.cpp>
|
||||
lib_deps =
|
||||
google/googletest @ 1.17.0
|
||||
|
||||
|
||||
+6
-6
@@ -89,13 +89,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
}
|
||||
|
||||
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
|
||||
return forwardMultipartDirect(pkt);
|
||||
return forwardMultipartDirect(pkt, d);
|
||||
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
if (!_tables->wasSeen(pkt)) { // don't retransmit!
|
||||
_tables->markSeen(pkt);
|
||||
removeSelfFromPath(pkt);
|
||||
routeDirectRecvAcks(pkt, 0);
|
||||
routeDirectRecvAcks(pkt, d);
|
||||
}
|
||||
return ACTION_RELEASE;
|
||||
}
|
||||
@@ -104,7 +105,6 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
_tables->markSeen(pkt);
|
||||
removeSelfFromPath(pkt);
|
||||
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
n_forwarded++;
|
||||
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
|
||||
}
|
||||
@@ -360,7 +360,7 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
|
||||
return ACTION_RELEASE;
|
||||
}
|
||||
|
||||
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
|
||||
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt, uint32_t delay_millis) {
|
||||
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
|
||||
uint8_t type = pkt->payload[0] & 0x0F;
|
||||
|
||||
@@ -374,7 +374,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
|
||||
if (!_tables->wasSeen(&tmp)) { // don't retransmit!
|
||||
_tables->markSeen(&tmp);
|
||||
removeSelfFromPath(&tmp);
|
||||
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
|
||||
routeDirectRecvAcks(&tmp, delay_millis + ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
|
||||
}
|
||||
}
|
||||
return ACTION_RELEASE;
|
||||
@@ -384,13 +384,13 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
if (!packet->isMarkedDoNotRetransmit()) {
|
||||
uint8_t extra = getExtraAckTransmitCount();
|
||||
while (extra > 0) {
|
||||
delay_millis += getDirectRetransmitDelay(packet) + 300;
|
||||
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
|
||||
if (a1) {
|
||||
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
|
||||
a1->header &= ~PH_ROUTE_MASK;
|
||||
a1->header |= ROUTE_TYPE_DIRECT;
|
||||
sendPacket(a1, 0, delay_millis);
|
||||
delay_millis += 300;
|
||||
}
|
||||
extra--;
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class Mesh : public Dispatcher {
|
||||
void removeSelfFromPath(Packet* packet);
|
||||
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
DispatcherAction forwardMultipartDirect(Packet* pkt);
|
||||
DispatcherAction forwardMultipartDirect(Packet* pkt, uint32_t delay_millis);
|
||||
|
||||
protected:
|
||||
DispatcherAction onRecvPacket(Packet* pkt) override;
|
||||
|
||||
+18
-7
@@ -64,21 +64,32 @@ public:
|
||||
virtual uint8_t getStartupReason() const = 0;
|
||||
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 setLoRaFemLnaEnabled(bool enable) { return false; }
|
||||
virtual bool canControlLoRaFemLna() const { return false; }
|
||||
virtual bool isLoRaFemLnaEnabled() const { return false; }
|
||||
// Software-selectable external FEM transmit gain. This is not a PA power switch.
|
||||
virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; }
|
||||
virtual bool canControlLoRaFemPaGain() const { return false; }
|
||||
virtual bool isLoRaFemPaGainEnabled() const { return false; }
|
||||
|
||||
// Power management interface (boards with power management override these)
|
||||
virtual bool isPwrMgtInitialised() const { return false; }
|
||||
virtual bool isExternalPowered() { return false; }
|
||||
// True while a board can identify an active battery-charging source.
|
||||
virtual bool isChargerActive() { return false; }
|
||||
|
||||
// Optional, source-specific power detection. Boards that can distinguish a USB
|
||||
// supply from a solar charger override these; defaults keep every other board
|
||||
// unaffected (the generic CLI prints "n/a" when a capability is absent).
|
||||
virtual bool hasUsbPowerDetect() const { return false; }
|
||||
virtual bool isUsbPowered() { return false; }
|
||||
virtual bool hasSolarChargerDetect() const { return false; }
|
||||
virtual bool isSolarChargerActive() { return false; }
|
||||
virtual uint16_t getBootVoltage() { return 0; }
|
||||
virtual bool getWakeLpcompSupported() const { return false; }
|
||||
virtual uint32_t getResetReason() const { return 0; }
|
||||
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
||||
virtual uint8_t getShutdownReason() const { return 0; }
|
||||
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
|
||||
|
||||
virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; }
|
||||
|
||||
// Called from the example main loops. Lets a board feed its watchdog and
|
||||
// run periodic housekeeping. Default no-op.
|
||||
virtual void loop() { /* no op */ }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+9
-2
@@ -147,8 +147,6 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin
|
||||
int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
|
||||
if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes
|
||||
|
||||
// decrypt() works in whole blocks: an unaligned length would write past
|
||||
// the caller's buffer (upstream 9c9a2ba2 / b74ba545).
|
||||
int enc_len = src_len - CIPHER_MAC_SIZE;
|
||||
if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext
|
||||
|
||||
@@ -208,6 +206,15 @@ bool Utils::isHexChar(char c) {
|
||||
return c == '0' || hexVal(c) > 0;
|
||||
}
|
||||
|
||||
bool Utils::isZeroes(const uint8_t* buf, size_t len) {
|
||||
while (len > 0) {
|
||||
if (*buf != 0) return false;
|
||||
buf++;
|
||||
len--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
|
||||
int len = strlen(src_hex);
|
||||
if (len != dest_size*2) return false; // incorrect length
|
||||
|
||||
@@ -82,6 +82,8 @@ public:
|
||||
static int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
|
||||
|
||||
static bool isHexChar(char c);
|
||||
|
||||
static bool isZeroes(const uint8_t* buf, size_t len);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// https://github.com/meshcore-dev/MeshCore/issues/2469
|
||||
// armstubs.cpp exists to silence warnings introduced after upgrading platformio/toolchain-gccarmnoneeabi
|
||||
extern "C" __attribute__((weak)) int _close(int fd) { return -1; }
|
||||
extern "C" __attribute__((weak)) int _lseek(int fd, int offset, int whence) { return 0; }
|
||||
extern "C" __attribute__((weak)) int _read(int fd, char *buf, int len) { return 0; }
|
||||
extern "C" __attribute__((weak)) int _fstat(int fd, struct stat *st) { return 0; }
|
||||
extern "C" __attribute__((weak)) int _isatty(int fd) { return 1; }
|
||||
extern "C" __attribute__((weak)) int _getpid(void) { return 1; }
|
||||
extern "C" __attribute__((weak)) int _kill(int pid, int sig) { return -1; }
|
||||
@@ -28,6 +28,14 @@
|
||||
return i;
|
||||
}
|
||||
|
||||
bool AdvertDataParser::isValidName(const char *n) {
|
||||
while (*n) {
|
||||
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
|
||||
n++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
|
||||
_name[0] = 0;
|
||||
_lat = _lon = 0;
|
||||
|
||||
@@ -50,6 +50,8 @@ class AdvertDataParser {
|
||||
public:
|
||||
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
|
||||
|
||||
static bool isValidName(const char* name);
|
||||
|
||||
bool isValid() const { return _valid; }
|
||||
uint8_t getType() const { return _flags & 0x0F; }
|
||||
uint16_t getFeat1() const { return _extra1; }
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#define TXT_ACK_DELAY 200
|
||||
#endif
|
||||
|
||||
#define CLI_REPLY_DELAY_MILLIS 600
|
||||
|
||||
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
}
|
||||
@@ -238,8 +240,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
ContactInfo& from = contacts[i];
|
||||
|
||||
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
@@ -247,7 +249,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
|
||||
if (flags == TXT_TYPE_PLAIN) {
|
||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
||||
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know
|
||||
|
||||
int text_len = strlen((char *)&data[5]);
|
||||
uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
@@ -265,20 +267,43 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
sendAckTo(from, ack_hash, 6);
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
char *text = (char *)&data[5];
|
||||
onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know
|
||||
// NOTE: no ack expected for CLI_DATA replies
|
||||
} else if (flags == TXT_TYPE_CLI_COMMAND) {
|
||||
uint8_t temp[166];
|
||||
char *command = (char *)&data[5];
|
||||
char *reply = (char *)&temp[5];
|
||||
*reply = 0;
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
|
||||
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
|
||||
if (path) sendFloodScoped(from, path);
|
||||
onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know
|
||||
// NOTE: no ack expected for CLI_COMMAND replies
|
||||
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
if (timestamp == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
timestamp++;
|
||||
}
|
||||
memcpy(temp, ×tamp, 4);
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2);
|
||||
|
||||
auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
|
||||
if (reply_pkt) {
|
||||
if (from.out_path_len == OUT_PATH_UNKNOWN) {
|
||||
sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS);
|
||||
} else {
|
||||
sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
|
||||
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
|
||||
from.sync_since = timestamp;
|
||||
if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
|
||||
from.sync_since = sender_timestamp;
|
||||
}
|
||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
||||
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
|
||||
onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE);
|
||||
@@ -379,6 +404,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8
|
||||
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) {
|
||||
// Skip empty/unconfigured slots. An empty slot has an all-zero secret and
|
||||
// therefore matches null-key group traffic (a node transmitting with an
|
||||
// unset PSK): the zero-key MAC validates against the empty slot and the
|
||||
// foreign message is delivered as if it belonged to that channel. Any node
|
||||
// with a free channel slot would otherwise act as a null-key sink.
|
||||
if (channels[i].name[0] == 0) continue;
|
||||
if (channels[i].channel.hash[0] == hash[0]) {
|
||||
dest[n++] = channels[i].channel;
|
||||
}
|
||||
@@ -469,13 +500,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
|
||||
return rc;
|
||||
}
|
||||
|
||||
int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) {
|
||||
int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) {
|
||||
int text_len = strlen(text);
|
||||
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
|
||||
|
||||
uint8_t temp[5+MAX_TEXT_LEN+1];
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
|
||||
temp[4] = (attempt & 3) | (txt_type << 2);
|
||||
memcpy(&temp[5], text, text_len + 1);
|
||||
|
||||
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#include "ContactInfo.h"
|
||||
|
||||
#define MAX_SEARCH_RESULTS 8
|
||||
#define MAX_SEARCH_RESULTS 24
|
||||
|
||||
#define MSG_SEND_FAILED 0
|
||||
#define MSG_SEND_SENT_FLOOD 1
|
||||
@@ -124,6 +124,7 @@ protected:
|
||||
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
|
||||
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
|
||||
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
|
||||
virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0;
|
||||
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
|
||||
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
|
||||
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
|
||||
@@ -166,7 +167,7 @@ public:
|
||||
mesh::Packet* createSelfAdvert(const char* name);
|
||||
mesh::Packet* createSelfAdvert(const char* name, double lat, double lon);
|
||||
int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout);
|
||||
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
|
||||
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout);
|
||||
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
|
||||
bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len);
|
||||
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
|
||||
|
||||
@@ -28,13 +28,21 @@ struct ClientInfo {
|
||||
unsigned long ack_timeout;
|
||||
uint8_t push_failures;
|
||||
} room;
|
||||
struct {
|
||||
uint32_t expiry_timestamp; // epoch seconds
|
||||
uint32_t push_tag;
|
||||
uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber
|
||||
uint8_t min_deltas_len;
|
||||
uint8_t min_deltas[14]; // LPP encoded
|
||||
uint8_t prev_telem[14]; // LPP encoded
|
||||
} sensor;
|
||||
} extra;
|
||||
|
||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 20
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
class ClientACL {
|
||||
|
||||
+41
-194
@@ -164,6 +164,7 @@ void CommonCLI::savePrefs() {
|
||||
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
|
||||
}
|
||||
_callbacks->savePrefs();
|
||||
_prefs->clearDirty();
|
||||
}
|
||||
|
||||
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
@@ -180,6 +181,16 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
}
|
||||
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
if (_prefs->getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is a radio CLI command?
|
||||
if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); }
|
||||
return;
|
||||
}
|
||||
// hook for variant-specific CLI processing
|
||||
if (_board->handleCommand(command, sender_timestamp, reply)) {
|
||||
if (_prefs->isDirty()) { savePrefs(); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
|
||||
_board->powerOff(); // doesn't return
|
||||
} else if (memcmp(command, "reboot", 6) == 0) {
|
||||
@@ -273,6 +284,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
|
||||
} else if (memcmp(command, "board", 5) == 0) {
|
||||
sprintf(reply, "%s", _board->getManufacturerName());
|
||||
} else if (strcmp(command, "power") == 0) {
|
||||
const char* usb_state = _board->hasUsbPowerDetect()
|
||||
? (_board->isUsbPowered() ? "yes" : "no")
|
||||
: "n/a";
|
||||
const char* solar_state = _board->hasSolarChargerDetect()
|
||||
? (_board->isSolarChargerActive() ? "yes" : "no")
|
||||
: "n/a";
|
||||
snprintf(reply, 160, "batt:%u mV usb:%s solar_chg:%s ext:%s charger:%s",
|
||||
(unsigned)_board->getBattMilliVolts(),
|
||||
usb_state,
|
||||
solar_state,
|
||||
_board->isExternalPowered() ? "yes" : "no",
|
||||
_board->isChargerActive() ? "yes" : "no");
|
||||
} else if (memcmp(command, "sensor get ", 11) == 0) {
|
||||
const char* key = command + 11;
|
||||
const char* val = _sensors->getSettingByKey(key);
|
||||
@@ -344,6 +368,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else {
|
||||
strcpy(reply, "gps provider not found");
|
||||
}
|
||||
} else if (strcmp(command, "gps diag") == 0) {
|
||||
LocationProvider * l = _sensors->getLocationProvider();
|
||||
if (l != NULL) {
|
||||
const char* setting = _sensors->getSettingByKey("gps");
|
||||
bool requested = setting != NULL && strcmp(setting, "1") == 0;
|
||||
int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U);
|
||||
if (prefix_len < 0 || prefix_len >= 160) {
|
||||
reply[159] = 0;
|
||||
} else {
|
||||
l->formatDiagnostics(reply + prefix_len, 160 - prefix_len);
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "gps provider not found");
|
||||
}
|
||||
} else if (memcmp(command, "gps setloc", 10) == 0) {
|
||||
_prefs->node_lat = _sensors->node_lat;
|
||||
_prefs->node_lon = _sensors->node_lon;
|
||||
@@ -447,39 +485,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
|
||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
if (memcmp(config, "dutycycle ", 10) == 0) {
|
||||
float dc = atof(&config[10]);
|
||||
if (dc < 1 || dc > 100) {
|
||||
strcpy(reply, "ERROR: dutycycle must be 1-100");
|
||||
} else {
|
||||
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
|
||||
savePrefs();
|
||||
float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
|
||||
int a_int = (int)actual;
|
||||
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
|
||||
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
|
||||
}
|
||||
} else if (memcmp(config, "af ", 3) == 0) {
|
||||
_prefs->airtime_factor = atof(&config[3]);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "int.thresh ", 11) == 0) {
|
||||
_prefs->interference_threshold = atoi(&config[11]);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "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) {
|
||||
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
|
||||
savePrefs();
|
||||
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
||||
} else if (memcmp(config, "multi.acks ", 11) == 0) {
|
||||
_prefs->multi_acks = atoi(&config[11]);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
|
||||
|
||||
if (memcmp(config, "allow.read.only ", 16) == 0) {
|
||||
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
@@ -532,77 +539,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
|
||||
savePrefs();
|
||||
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
|
||||
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
|
||||
bool enabled = memcmp(&config[13], "on", 2) == 0;
|
||||
_prefs->rx_boosted_gain = enabled;
|
||||
savePrefs();
|
||||
if (_callbacks->setRxBoostedGain(enabled)) {
|
||||
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.fem.txgain ", 17) == 0) {
|
||||
if (!_board->canControlLoRaFemPaGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&config[17], "on", 2) == 0) {
|
||||
if (_board->setLoRaFemPaGainEnabled(true)) {
|
||||
_prefs->radio_fem_txgain = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK - LoRa FEM TX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
|
||||
}
|
||||
} else if (memcmp(&config[17], "off", 3) == 0) {
|
||||
if (_board->setLoRaFemPaGainEnabled(false)) {
|
||||
_prefs->radio_fem_txgain = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK - LoRa FEM TX gain off");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Error: state must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "radio ", 6) == 0) {
|
||||
strcpy(tmp, &config[6]);
|
||||
const char *parts[4];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
|
||||
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
|
||||
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
|
||||
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
||||
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
|
||||
_prefs->sf = sf;
|
||||
_prefs->cr = cr;
|
||||
_prefs->freq = freq;
|
||||
_prefs->bw = bw;
|
||||
_callbacks->savePrefs();
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
}
|
||||
} else if (memcmp(config, "lat ", 4) == 0) {
|
||||
_prefs->node_lat = atof(&config[4]);
|
||||
savePrefs();
|
||||
@@ -611,24 +547,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
_prefs->node_lon = atof(&config[4]);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "rxdelay ", 8) == 0) {
|
||||
float db = atof(&config[8]);
|
||||
if (db >= 0 && db <= 20.0f) {
|
||||
_prefs->rx_delay_base = db;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-20");
|
||||
}
|
||||
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
||||
float f = atof(&config[8]);
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
_prefs->tx_delay_factor = f;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
|
||||
uint8_t m = atoi(&config[19]);
|
||||
if (m <= 64) {
|
||||
@@ -656,15 +574,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, max 64");
|
||||
}
|
||||
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
|
||||
float f = atof(&config[15]);
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
_prefs->direct_tx_delay_factor = f;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
||||
config += 11;
|
||||
char *dp = _prefs->owner_info;
|
||||
@@ -675,16 +584,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
*dp = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
|
||||
config += 15;
|
||||
uint8_t mode = atoi(config);
|
||||
if (mode < 3) {
|
||||
_prefs->path_hash_mode = mode;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0,1, or 2");
|
||||
}
|
||||
} else if (memcmp(config, "loop.detect ", 12) == 0) {
|
||||
config += 12;
|
||||
uint8_t mode;
|
||||
@@ -705,12 +604,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "tx ", 3) == 0) {
|
||||
// Store what the radio actually applied, not the raw request -- on a
|
||||
// board with an external-PA gain curve those can differ (see setTxPower()).
|
||||
_prefs->tx_power_dbm = _callbacks->setTxPower(atoi(&config[3]));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
|
||||
_prefs->freq = atof(&config[5]);
|
||||
savePrefs();
|
||||
@@ -807,22 +700,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
|
||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
if (memcmp(config, "dutycycle", 9) == 0) {
|
||||
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
|
||||
int dc_int = (int)dc;
|
||||
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
|
||||
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
|
||||
} else if (memcmp(config, "af", 2) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
|
||||
} else if (memcmp(config, "int.thresh", 10) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
|
||||
} else if (memcmp(config, "cad", 3) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
||||
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
|
||||
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
||||
if (memcmp(config, "allow.read.only", 15) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
||||
@@ -843,37 +721,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
||||
} else if (memcmp(config, "lon", 3) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
||||
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
|
||||
} 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.fem.txgain", 16) == 0) {
|
||||
if (!_board->canControlLoRaFemPaGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
|
||||
}
|
||||
} else if (memcmp(config, "radio", 5) == 0) {
|
||||
char freq[16], bw[16];
|
||||
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
||||
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
|
||||
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
|
||||
} else if (memcmp(config, "rxdelay", 7) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
||||
} else if (memcmp(config, "txdelay", 7) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
||||
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
||||
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
|
||||
} else if (memcmp(config, "flood.max", 9) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
|
||||
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
|
||||
} else if (memcmp(config, "owner.info", 10) == 0) {
|
||||
auto start = reply;
|
||||
*reply++ = '>';
|
||||
@@ -884,8 +737,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sp++;
|
||||
}
|
||||
*reply = 0; // set null terminator
|
||||
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
|
||||
} else if (memcmp(config, "loop.detect", 11) == 0) {
|
||||
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
|
||||
strcpy(reply, "> off");
|
||||
@@ -896,10 +747,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "> strict");
|
||||
}
|
||||
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
|
||||
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
|
||||
} else if (memcmp(config, "freq", 4) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
|
||||
} else if (memcmp(config, "public.key", 10) == 0) {
|
||||
strcpy(reply, "> ");
|
||||
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
||||
|
||||
+48
-2
@@ -6,6 +6,8 @@
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
#include <helpers/CommonRadioPrefs.h>
|
||||
#include <helpers/DynamicConfigSerializer.h>
|
||||
|
||||
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
|
||||
#define WITH_BRIDGE
|
||||
@@ -72,7 +74,7 @@ public:
|
||||
uint8_t extra_sf[4];
|
||||
|
||||
private:
|
||||
class RadioPrefs : public ConfigSerializer {
|
||||
class RadioPrefs : public CommonRadioPrefs {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
@@ -96,6 +98,41 @@ private:
|
||||
}
|
||||
public:
|
||||
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
// CommonRadioPrefs interface
|
||||
float getFreq() const override { return _parent->freq; }
|
||||
void setFreq(float f) override { _parent->freq = f; markDirty(); }
|
||||
float getBandwidth() const override { return _parent->bw; }
|
||||
void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); }
|
||||
uint8_t getSpreadFactor() const override { return _parent->sf; }
|
||||
void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); }
|
||||
uint8_t getCodingRate() const override { return _parent->cr; }
|
||||
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
|
||||
float getAirtimeFactor() const override { return _parent->airtime_factor; }
|
||||
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
|
||||
bool isCadEnabled() const override { return _parent->cad_enabled; }
|
||||
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
|
||||
uint8_t getIntThresh() const override { return _parent->interference_threshold; }
|
||||
void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); }
|
||||
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
|
||||
void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); }
|
||||
uint8_t getTxPower() const override { return _parent->tx_power_dbm; }
|
||||
void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); }
|
||||
float getRxDelay() const override { return _parent->rx_delay_base; }
|
||||
void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); }
|
||||
uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
|
||||
void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); }
|
||||
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
|
||||
void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); }
|
||||
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
|
||||
void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); }
|
||||
float getFloodTxDelay() const override { return _parent->tx_delay_factor; }
|
||||
void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); }
|
||||
float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; }
|
||||
void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); }
|
||||
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
|
||||
void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); }
|
||||
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
|
||||
void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); }
|
||||
};
|
||||
RadioPrefs radio;
|
||||
|
||||
@@ -166,6 +203,8 @@ private:
|
||||
};
|
||||
RoomPrefs room;
|
||||
|
||||
DynamicConfigSerializer custom;
|
||||
|
||||
protected:
|
||||
void structure() override {
|
||||
def("name", node_name, sizeof(node_name));
|
||||
@@ -182,16 +221,23 @@ protected:
|
||||
def("repeat", repeat);
|
||||
def("room", room);
|
||||
def("power", power);
|
||||
def("custom", custom);
|
||||
}
|
||||
|
||||
public:
|
||||
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) {
|
||||
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this), custom(&radio) {
|
||||
node_name[0] = 0;
|
||||
password[0] = 0;
|
||||
guest_password[0] = 0;
|
||||
bridge_secret[0] = 0;
|
||||
owner_info[0] = 0;
|
||||
}
|
||||
|
||||
CommonRadioPrefs* getRadioPrefs() { return &radio; }
|
||||
KeyValueStore* getCustom() { return &custom; }
|
||||
|
||||
bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); }
|
||||
void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); }
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#include "CommonRadioPrefs.h"
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "Utils.h"
|
||||
#include "target.h"
|
||||
|
||||
bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) {
|
||||
if (strcmp(key, "fem_rxgain") == 0) {
|
||||
snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain());
|
||||
return true;
|
||||
}
|
||||
if (strcmp(key, "fem_txgain") == 0) {
|
||||
snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool CommonRadioPrefs::setByKey(const char* key, const char* value) {
|
||||
if (strcmp(key, "fem_rxgain") == 0) {
|
||||
setFEMRxGain(atoi(value));
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
if (strcmp(key, "fem_txgain") == 0) {
|
||||
setFEMTxGain(atoi(value));
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio") == 0) {
|
||||
char freq[16], bw[16];
|
||||
strcpy(freq, StrHelper::ftoa(getFreq()));
|
||||
strcpy(bw, StrHelper::ftoa3(getBandwidth()));
|
||||
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio ", 10) == 0) {
|
||||
char tmp[132];
|
||||
strcpy(tmp, &command[10]);
|
||||
const char *parts[4];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
|
||||
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
|
||||
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
|
||||
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
||||
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
|
||||
setSpreadFactor(sf);
|
||||
setCodingRate(cr);
|
||||
setFreq(freq);
|
||||
setBandwidth(bw);
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get freq") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(getFreq()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get af") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor()));
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set af ", 7) == 0) {
|
||||
char* end;
|
||||
float af = strtof(&command[7], &end);
|
||||
if (end == &command[7] || af < 0 || af > 9) {
|
||||
strcpy(reply, "ERROR: af must be 0-9");
|
||||
} else {
|
||||
setAirtimeFactor(af);
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get dutycycle") == 0) {
|
||||
float dc = 100.0f / (getAirtimeFactor() + 1.0f);
|
||||
int dc_int = (int)dc;
|
||||
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
|
||||
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set dutycycle ", 14) == 0) {
|
||||
float dc = atof(&command[14]);
|
||||
if (dc < 1 || dc > 100) {
|
||||
strcpy(reply, "ERROR: dutycycle must be 1-100");
|
||||
} else {
|
||||
setAirtimeFactor((100.0f / dc) - 1.0f);
|
||||
float actual = 100.0f / (getAirtimeFactor() + 1.0f);
|
||||
int a_int = (int)actual;
|
||||
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
|
||||
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get int.thresh") == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) getIntThresh());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set int.thresh ", 15) == 0) {
|
||||
setIntThresh(atoi(&command[15]));
|
||||
strcpy(reply, "OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get cad") == 0) {
|
||||
sprintf(reply, "> %s", isCadEnabled() ? "on" : "off");
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set cad ", 8) == 0) {
|
||||
setCadEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get radio.rxgain") == 0) {
|
||||
sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off");
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.rxgain ", 17) == 0) {
|
||||
bool enabled = memcmp(&command[17], "on", 2) == 0;
|
||||
setRxGain(enabled);
|
||||
if (radio_driver.setRxBoostedGainMode(enabled)) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memcmp(command, "get tx", 6) == 0 && (command[6] == 0 || command[6] == ' ')) {
|
||||
sprintf(reply, "> %d", (int32_t) getTxPower());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set tx ", 7) == 0) {
|
||||
setTxPower(atoi(&command[7]));
|
||||
radio_driver.setTxPower(getTxPower());
|
||||
strcpy(reply, "OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get rxdelay") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay()));
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set rxdelay ", 12) == 0) {
|
||||
float db = atof(&command[12]);
|
||||
if (db >= 0 && db <= 20.0f) {
|
||||
setRxDelay(db);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-20");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get agc.reset.interval") == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) getAgcResetInt());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set agc.reset.interval ", 23) == 0) {
|
||||
int secs = atoi(&command[23]);
|
||||
secs = constrain(secs, 0, 255 * 4);
|
||||
setAgcResetInt((uint16_t) secs);
|
||||
sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get path.hash.mode") == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)getHashMode());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
|
||||
const char* config = command + 19;
|
||||
uint8_t mode = atoi(config);
|
||||
if (mode < 3) {
|
||||
setHashMode(mode);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0,1, or 2");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get multi.acks") == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) getMultiAcks());
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set multi.acks ", 15) == 0) {
|
||||
setMultiAcks(atoi(&command[15]));
|
||||
strcpy(reply, "OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get txdelay") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay()));
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set txdelay ", 12) == 0) {
|
||||
float f = atof(&command[12]);
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
setFloodTxDelay(f);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get direct.txdelay") == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay()));
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set direct.txdelay ", 19) == 0) {
|
||||
float f = atof(&command[19]);
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
setDirectTxDelay(f);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
#include "ConfigSerializer.h"
|
||||
#include "KeyValueStore.h"
|
||||
|
||||
class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore {
|
||||
bool _is_dirty = false;
|
||||
protected:
|
||||
CommonRadioPrefs() { }
|
||||
public:
|
||||
void markDirty() { _is_dirty = true; }
|
||||
void clearDirty() { _is_dirty = false; }
|
||||
bool isDirty() const { return _is_dirty; }
|
||||
|
||||
virtual float getFreq() const = 0;
|
||||
virtual void setFreq(float f) = 0;
|
||||
|
||||
virtual float getBandwidth() const = 0;
|
||||
virtual void setBandwidth(float bw) = 0;
|
||||
|
||||
virtual uint8_t getSpreadFactor() const = 0;
|
||||
virtual void setSpreadFactor(uint8_t sf) = 0;
|
||||
|
||||
virtual uint8_t getCodingRate() const = 0;
|
||||
virtual void setCodingRate(uint8_t cr) = 0;
|
||||
|
||||
virtual float getAirtimeFactor() const = 0;
|
||||
virtual void setAirtimeFactor(float af) = 0;
|
||||
|
||||
virtual bool isCadEnabled() const = 0;
|
||||
virtual void setCadEnabled(bool en) = 0;
|
||||
|
||||
virtual uint8_t getIntThresh() const = 0;
|
||||
virtual void setIntThresh(uint8_t t) = 0;
|
||||
|
||||
virtual uint8_t getRxGain() const = 0;
|
||||
virtual void setRxGain(uint8_t g) = 0;
|
||||
|
||||
virtual uint8_t getTxPower() const = 0;
|
||||
virtual void setTxPower(uint8_t dbm) = 0;
|
||||
|
||||
virtual float getRxDelay() const = 0;
|
||||
virtual void setRxDelay(float d) = 0;
|
||||
|
||||
virtual uint16_t getAgcResetInt() const = 0;
|
||||
virtual void setAgcResetInt(uint16_t secs) = 0;
|
||||
|
||||
virtual uint8_t getHashMode() const = 0;
|
||||
virtual void setHashMode(uint8_t m) = 0;
|
||||
|
||||
virtual uint8_t getMultiAcks() const = 0;
|
||||
virtual void setMultiAcks(uint8_t m) = 0;
|
||||
|
||||
virtual float getFloodTxDelay() const = 0;
|
||||
virtual void setFloodTxDelay(float d) = 0;
|
||||
|
||||
virtual float getDirectTxDelay() const = 0;
|
||||
virtual void setDirectTxDelay(float d) = 0;
|
||||
|
||||
virtual uint8_t getFEMRxGain() const = 0;
|
||||
virtual void setFEMRxGain(uint8_t g) = 0;
|
||||
|
||||
virtual uint8_t getFEMTxGain() const = 0;
|
||||
virtual void setFEMTxGain(uint8_t g) = 0;
|
||||
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply);
|
||||
|
||||
bool setByKey(const char* key, const char* value) override; // for dynamic key/value access
|
||||
bool getByKey(const char* key, char* value, size_t max_len) override;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ConfigSerializer.h"
|
||||
#include <stdlib.h> // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not)
|
||||
|
||||
bool ConfigSerializer::saveSerial(Stream& s) {
|
||||
Context context(&s, OP::WRITE);
|
||||
@@ -23,7 +24,9 @@ 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 == '_';
|
||||
// digits are allowed: keys like gr_1hop contain one, otherwise loadSerial
|
||||
// fails at that key and prefs stop reloading after a reboot
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
static bool is_value_char(char c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.';
|
||||
@@ -62,6 +65,7 @@ int ConfigSerializer::Context::readNext() {
|
||||
case EXPECT_COMMA_OR_KEY:
|
||||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
||||
case EXPECT_KEY:
|
||||
if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}'
|
||||
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; }
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
class ConfigSerializer {
|
||||
bool _first;
|
||||
int8_t _depth;
|
||||
bool _dirty = false;
|
||||
|
||||
protected:
|
||||
enum OP { READ, WRITE };
|
||||
|
||||
class Context {
|
||||
@@ -38,13 +40,14 @@ class ConfigSerializer {
|
||||
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); }
|
||||
const char* getKey(uint8_t depth) { return _keys[depth]; }
|
||||
};
|
||||
|
||||
Context* _context = NULL;
|
||||
int8_t getDepth() const { return _depth; }
|
||||
|
||||
void writeComma();
|
||||
|
||||
protected:
|
||||
ConfigSerializer() { }
|
||||
|
||||
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
|
||||
@@ -62,7 +65,12 @@ protected:
|
||||
|
||||
virtual void structure() = 0;
|
||||
|
||||
void markDirty() { _dirty = true; }
|
||||
|
||||
public:
|
||||
bool loadSerial(Stream& s);
|
||||
bool saveSerial(Stream& s);
|
||||
|
||||
virtual bool isDirty() const { return _dirty; }
|
||||
virtual void clearDirty() { _dirty = false; }
|
||||
};
|
||||
|
||||
@@ -32,6 +32,12 @@ struct ContactInfo {
|
||||
return shared_secret;
|
||||
}
|
||||
|
||||
bool isFav() const { return flags & 0x01; }
|
||||
bool isTelemBaseAllowed() const { return flags & 0x02; }
|
||||
bool isTelemLocAllowed() const { return flags & 0x04; }
|
||||
bool isTelemEnvAllowed() const { return flags & 0x08; }
|
||||
bool isRemoteCLIAllowed() const { return flags & 0x10; }
|
||||
|
||||
private:
|
||||
mutable uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "DynamicConfigSerializer.h"
|
||||
#include <Utils.h>
|
||||
|
||||
#define PROP_SEP_CHAR '|'
|
||||
#define PROP_SEP_STR "|"
|
||||
#define KEY_SEP_CHAR ':'
|
||||
#define KEY_SEP_STR ":"
|
||||
|
||||
bool DynamicConfigSerializer::setByKeyPrv(const char* key, const char* value) {
|
||||
if (_fallback && _fallback->setByKey(key, value)) return true;
|
||||
|
||||
// TODO: guard for bad chars (':' or '|')
|
||||
char tmp[MAX_DYNAMIC_CONFG];
|
||||
strcpy(tmp, _config); // make a (modifiable) copy
|
||||
|
||||
const char* parts[8];
|
||||
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
|
||||
|
||||
// add/replace in _config[]
|
||||
char new_config[MAX_DYNAMIC_CONFG];
|
||||
new_config[0] = 0;
|
||||
|
||||
int keylen = strlen(key);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char* item = parts[i];
|
||||
if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) {
|
||||
// key exists, so omit old value from this pass (will append new value at end)
|
||||
} else {
|
||||
if (new_config[0]) {
|
||||
strcat(new_config, PROP_SEP_STR);
|
||||
}
|
||||
strcat(new_config, item);
|
||||
}
|
||||
}
|
||||
// now append new key/value (if it fits)
|
||||
if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) {
|
||||
if (new_config[0]) {
|
||||
strcat(new_config, PROP_SEP_STR);
|
||||
}
|
||||
strcat(new_config, key);
|
||||
strcat(new_config, KEY_SEP_STR);
|
||||
strcat(new_config, value);
|
||||
strcpy(_config, new_config); // commit new serialized string
|
||||
return true;
|
||||
}
|
||||
return false; // didn't fit in _config[]
|
||||
}
|
||||
|
||||
bool DynamicConfigSerializer::setByKey(const char* key, const char* value) {
|
||||
if (setByKeyPrv(key, value)) {
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) {
|
||||
if (_fallback && _fallback->getByKey(key, value, max_len)) return true;
|
||||
|
||||
char tmp[MAX_DYNAMIC_CONFG];
|
||||
strcpy(tmp, _config); // make a (modifiable) copy
|
||||
|
||||
const char* parts[8];
|
||||
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
|
||||
|
||||
int keylen = strlen(key);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char* item = parts[i];
|
||||
if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) {
|
||||
strncpy(value, &item[keylen+1], max_len);
|
||||
value[max_len] = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DynamicConfigSerializer::structure() {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
char tmp[MAX_DYNAMIC_CONFG];
|
||||
strcpy(tmp, _config); // make a (modifiable) copy
|
||||
|
||||
const char* parts[8];
|
||||
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
char* item = (char *) parts[i];
|
||||
char* eq = strchr(item, KEY_SEP_CHAR);
|
||||
if (eq) {
|
||||
*eq = 0; // replace separator with null terminator
|
||||
def(item, eq + 1, MAX_DYNAMIC_CONFG/2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setByKeyPrv(_context->getKey(getDepth()), _context->getToken());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include "ConfigSerializer.h"
|
||||
#include "KeyValueStore.h"
|
||||
|
||||
#ifndef MAX_DYNAMIC_CONFG
|
||||
#define MAX_DYNAMIC_CONFG 128
|
||||
#endif
|
||||
|
||||
class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore {
|
||||
char _config[MAX_DYNAMIC_CONFG];
|
||||
KeyValueStore* _fallback;
|
||||
|
||||
bool setByKeyPrv(const char* key, const char* value);
|
||||
|
||||
protected:
|
||||
void structure() override;
|
||||
|
||||
public:
|
||||
DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { _config[0] = 0; }
|
||||
|
||||
bool setByKey(const char* key, const char* value) override;
|
||||
bool getByKey(const char* key, char* value, size_t max_len) override;
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "soc/rtc.h"
|
||||
#include "esp_system.h"
|
||||
#include <driver/rtc_io.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
class ESP32Board : public mesh::MainBoard {
|
||||
protected:
|
||||
@@ -51,6 +52,8 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
// Temperature from ESP32 MCU
|
||||
float getMCUTemperature() override {
|
||||
uint32_t raw = 0;
|
||||
@@ -216,7 +219,7 @@ public:
|
||||
if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) {
|
||||
tv.tv_sec = _rtc_backup_time;
|
||||
} else {
|
||||
tv.tv_sec = 1772323200; // 1 Mar 2026
|
||||
tv.tv_sec = RTC_TIME_MIN;
|
||||
}
|
||||
tv.tv_usec = 0;
|
||||
settimeofday(&tv, NULL);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
class KeyValueStore {
|
||||
protected:
|
||||
KeyValueStore() { }
|
||||
public:
|
||||
virtual bool setByKey(const char* key, const char* value) { return false; }
|
||||
virtual bool getByKey(const char* key, char* value, size_t max_len) { return false; }
|
||||
};
|
||||
+27
-20
@@ -47,7 +47,8 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
|
||||
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
|
||||
}
|
||||
|
||||
void NRF52Board::initPowerMgr() {
|
||||
void NRF52Board::pwrmgtInit() {
|
||||
if (pwrmgt_initialised) return;
|
||||
// Copy early-captured register values
|
||||
reset_reason = g_nrf52_reset_reason;
|
||||
shutdown_reason = g_nrf52_shutdown_reason;
|
||||
@@ -74,6 +75,7 @@ void NRF52Board::initPowerMgr() {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
|
||||
getResetReasonString(reset_reason), (unsigned long)reset_reason);
|
||||
}
|
||||
pwrmgt_initialised = true;
|
||||
}
|
||||
|
||||
const char* NRF52Board::getResetReasonString(uint32_t reason) {
|
||||
@@ -98,6 +100,7 @@ const char* NRF52Board::getResetReasonString(uint32_t reason) {
|
||||
|
||||
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
|
||||
switch (reason) {
|
||||
case SHUTDOWN_REASON_NONE: return "None";
|
||||
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
|
||||
case SHUTDOWN_REASON_USER: return "User Request";
|
||||
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
|
||||
@@ -106,7 +109,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
|
||||
}
|
||||
|
||||
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
|
||||
initPowerMgr();
|
||||
pwrmgtInit();
|
||||
|
||||
// Read boot voltage
|
||||
boot_voltage_mv = getBattMilliVolts();
|
||||
@@ -173,24 +176,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
|
||||
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t lpcomp_refsel) {
|
||||
pwrmgtWakeArmVbus();
|
||||
if (!isPwrMgtInitialised()) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: Power Management not initialised");
|
||||
return;
|
||||
}
|
||||
if (!getWakeLpcompSupported()) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: LPCOMP unsupported on variant");
|
||||
return;
|
||||
}
|
||||
// LPCOMP is not managed by SoftDevice - direct register access required
|
||||
// Halt and disable before reconfiguration
|
||||
NRF_LPCOMP->TASKS_STOP = 1;
|
||||
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
|
||||
|
||||
// Select analog input (AIN0-7 maps to PSEL 0-7)
|
||||
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
|
||||
NRF_LPCOMP->PSEL = ((uint32_t)PWRMGT_LPCOMP_AIN << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
|
||||
|
||||
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
|
||||
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
|
||||
NRF_LPCOMP->REFSEL = ((uint32_t)lpcomp_refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
|
||||
|
||||
// Detect UP events (voltage rises above threshold for battery recovery)
|
||||
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
|
||||
|
||||
// Enable 50mV hysteresis for noise immunity
|
||||
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
|
||||
|
||||
// Clear stale events/interrupts before enabling wake
|
||||
NRF_LPCOMP->EVENTS_READY = 0;
|
||||
NRF_LPCOMP->EVENTS_DOWN = 0;
|
||||
@@ -204,23 +213,22 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
|
||||
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
|
||||
NRF_LPCOMP->TASKS_START = 1;
|
||||
|
||||
// Wait for comparator to settle before entering SYSTEMOFF
|
||||
// Wait for comparator to settle
|
||||
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
|
||||
delayMicroseconds(50);
|
||||
}
|
||||
|
||||
if (refsel == 7) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
|
||||
} else if (refsel <= 6) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
|
||||
ain_channel, refsel + 1);
|
||||
if (lpcomp_refsel == 7) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN);
|
||||
} else if (lpcomp_refsel <= 6) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1);
|
||||
} else {
|
||||
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
|
||||
ain_channel, ref_num);
|
||||
uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1);
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure VBUS (USB power) wake alongside LPCOMP
|
||||
void NRF52Board::pwrmgtWakeArmVbus() {
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
if (sd_enabled) {
|
||||
@@ -229,8 +237,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
|
||||
NRF_POWER->EVENTS_USBDETECTED = 0;
|
||||
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -25,15 +26,15 @@ struct PowerMgtConfig {
|
||||
#endif
|
||||
|
||||
class NRF52Board : public mesh::MainBoard {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initPowerMgr();
|
||||
#endif
|
||||
private:
|
||||
bool pwrmgt_initialised = false;
|
||||
|
||||
protected:
|
||||
uint8_t startup_reason;
|
||||
char *ota_name;
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void pwrmgtInit();
|
||||
uint32_t reset_reason; // RESETREAS register value
|
||||
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
|
||||
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
|
||||
@@ -41,6 +42,7 @@ protected:
|
||||
bool checkBootVoltage(const PowerMgtConfig* config);
|
||||
void enterSystemOff(uint8_t reason);
|
||||
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
|
||||
void pwrmgtWakeArmVbus();
|
||||
virtual void initiateShutdown(uint8_t reason);
|
||||
#endif
|
||||
|
||||
@@ -57,12 +59,18 @@ public:
|
||||
virtual void sleep(uint32_t secs) override;
|
||||
bool isExternalPowered() override;
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
uint16_t getBootVoltage() override { return boot_voltage_mv; }
|
||||
virtual uint32_t getResetReason() const override { return reset_reason; }
|
||||
uint8_t getShutdownReason() const override { return shutdown_reason; }
|
||||
const char* getResetReasonString(uint32_t reason) override;
|
||||
const char* getShutdownReasonString(uint8_t reason) override;
|
||||
bool isPwrMgtInitialised() const override { return pwrmgt_initialised; }
|
||||
#ifdef PWRMGT_LPCOMP_AIN
|
||||
bool getWakeLpcompSupported() const override { return true; }
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[],
|
||||
}
|
||||
|
||||
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
|
||||
if (packet->transport_codes[0] == 0) return &wildcard;
|
||||
for (int i = 0; i < num_regions; i++) {
|
||||
auto region = ®ions[i];
|
||||
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
// the GPS status icon.
|
||||
virtual bool isGpsDutySleeping() const { return false; }
|
||||
virtual int getAvailableLPPTypes(uint8_t* types, int max_count) const { return 0; }
|
||||
virtual bool isGPSDetected() const { return false; }
|
||||
|
||||
// Helper functions to manage setting by keys (useful in many places ...)
|
||||
const char* getSettingByKey(const char* key) {
|
||||
|
||||
@@ -27,8 +27,13 @@ bool StrHelper::isBlank(const char* str) {
|
||||
}
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdio.h>
|
||||
#if __has_include(<api/itoa.h>)
|
||||
// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084
|
||||
#include <api/itoa.h>
|
||||
#endif
|
||||
|
||||
union int32_Float_t
|
||||
union int32_Float_t
|
||||
{
|
||||
int32_t Long;
|
||||
float Float;
|
||||
@@ -100,7 +105,7 @@ static void _ftoa(float f, char *p, int *status)
|
||||
*p++ = '-';
|
||||
if (int_part == 0)
|
||||
*p++ = '0';
|
||||
else
|
||||
else
|
||||
{
|
||||
ltoa(int_part, p, 10);
|
||||
while (*p)
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define TXT_TYPE_PLAIN 0 // a plain text message
|
||||
#define TXT_TYPE_CLI_DATA 1 // a CLI command
|
||||
#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply
|
||||
#define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender
|
||||
#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly)
|
||||
|
||||
#define DATA_TYPE_RESERVED 0x0000 // reserved for future use
|
||||
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
|
||||
|
||||
|
||||
@@ -132,8 +132,12 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
|
||||
char charpin[20];
|
||||
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
|
||||
|
||||
// If we want to control BLE LED ourselves, uncomment this:
|
||||
// Bluefruit.autoConnLed(false);
|
||||
#ifdef DISABLE_BLE_LED // use variant platformio.ini build_flags macro: -D DISABLE_BLE_LED
|
||||
Bluefruit.autoConnLed(false); // false removes the automatic control of the LED by the Bluefruit library
|
||||
#else
|
||||
Bluefruit.autoConnLed(true); // true leaves the default automatic control of the LED by the Bluefruit library
|
||||
#endif
|
||||
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.begin();
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 {
|
||||
public:
|
||||
CustomLR1110(Module *mod) : LR1110(mod) { }
|
||||
|
||||
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
|
||||
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
|
||||
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
|
||||
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
|
||||
tcxoVoltage);
|
||||
// RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator.
|
||||
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
|
||||
return state;
|
||||
}
|
||||
|
||||
size_t getPacketLength(bool update) override {
|
||||
size_t len = LR1110::getPacketLength(update);
|
||||
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <RadioLib.h>
|
||||
#include "MeshCore.h"
|
||||
|
||||
class CustomLR1121 : public LR1121 {
|
||||
uint32_t _preambleMillis = 66;
|
||||
uint32_t _maxPayloadMillis = 3934;
|
||||
uint32_t _activityAt = 0;
|
||||
bool _headerSeen = false;
|
||||
bool _rx_boosted = false;
|
||||
int8_t _txPower = 0;
|
||||
bool _txPowerSet = false;
|
||||
|
||||
public:
|
||||
CustomLR1121(Module *mod) : LR1121(mod) { }
|
||||
|
||||
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
|
||||
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
|
||||
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
|
||||
int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
|
||||
tcxoVoltage);
|
||||
// RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator.
|
||||
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
|
||||
return state;
|
||||
}
|
||||
|
||||
size_t getPacketLength(bool update) override {
|
||||
size_t len = LR1121::getPacketLength(update);
|
||||
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
||||
// we've just received a corrupted packet
|
||||
// this may have triggered a bug causing subsequent packets to be shifted
|
||||
// call standby() to return radio to known-good state
|
||||
// recvRaw will call startReceive() to restart rx
|
||||
MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()");
|
||||
standby();
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
using LR1121::setOutputPower;
|
||||
using LR1121::setFrequency;
|
||||
|
||||
int16_t setOutputPower(int8_t power) override {
|
||||
#ifdef LR1121_FORCE_HP_PA
|
||||
const bool force_hp = true; // boards with only RFO_HP_LF wired: never select the LP PA
|
||||
#else
|
||||
const bool force_hp = false;
|
||||
#endif
|
||||
// clip to the active PA's range (HF PA tops out at 13 dBm) rather than fail and keep the other band's PA
|
||||
int8_t clipped = power;
|
||||
checkOutputPower(power, &clipped, force_hp);
|
||||
int16_t state = LR1121::setOutputPower(clipped, force_hp);
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
_txPower = power;
|
||||
_txPowerSet = true;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
int16_t setFrequency(float freq) override {
|
||||
int16_t state = LR1121::setFrequency(freq);
|
||||
// PA selection depends on the band, and set radio / tempradio don't call setTxPower
|
||||
if (state == RADIOLIB_ERR_NONE && _txPowerSet) state = setOutputPower(_txPower);
|
||||
return state;
|
||||
}
|
||||
|
||||
float getFreqMHz() const { return freqMHz; }
|
||||
|
||||
int16_t setRxBoostedGainMode(bool en) {
|
||||
_rx_boosted = en;
|
||||
return LR1121::setRxBoostedGainMode(en);
|
||||
}
|
||||
|
||||
bool getRxBoostedGainMode() const { return _rx_boosted; }
|
||||
|
||||
int16_t startReceive() override {
|
||||
// include the PREAMBLE_DETECTED irq bit in reported flags.
|
||||
return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
}
|
||||
|
||||
bool isReceiving() {
|
||||
uint32_t irq = getIrqStatus();
|
||||
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
|
||||
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; }
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "CustomLR1121.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "LR11x0Reset.h"
|
||||
|
||||
class CustomLR1121Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomLR1121 *)_radio)->setFrequency(freq);
|
||||
((CustomLR1121 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomLR1121 *)_radio)->setBandwidth(bw);
|
||||
((CustomLR1121 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
|
||||
((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis);
|
||||
((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
|
||||
}
|
||||
|
||||
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); }
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomLR1121 *)_radio)->isReceiving();
|
||||
}
|
||||
float getCurrentRSSI() override {
|
||||
float rssi = -110;
|
||||
((CustomLR1121 *)_radio)->getRssiInst(&rssi);
|
||||
return rssi;
|
||||
}
|
||||
|
||||
void onSendFinished() override {
|
||||
RadioLibWrapper::onSendFinished();
|
||||
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
|
||||
}
|
||||
|
||||
uint32_t getEstAirtimeFor(int len_bytes) override {
|
||||
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
|
||||
return airtime < 200 ? 200 : airtime; // at least 200 millis
|
||||
}
|
||||
|
||||
float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); }
|
||||
float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); }
|
||||
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); }
|
||||
|
||||
bool setRxBoostedGainMode(bool en) override {
|
||||
return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
bool getRxBoostedGainMode() const override {
|
||||
return ((CustomLR1121 *)_radio)->getRxBoostedGainMode();
|
||||
}
|
||||
};
|
||||
@@ -72,7 +72,7 @@ class CustomLR2021 : public LR2021 {
|
||||
|
||||
int16_t startReceive() override {
|
||||
// include the PREAMBLE_DETECTED irq bit in reported flags
|
||||
return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
}
|
||||
|
||||
bool isReceiving() {
|
||||
|
||||
@@ -52,6 +52,9 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
#if defined(USE_LR2021)
|
||||
idle();
|
||||
#endif
|
||||
_tx_dbm = dbm; // logical/requested power -- what getTxPower(), the app and the CLI report
|
||||
#if defined(NUM_PA_POINTS) && defined(TX_GAIN_LORA)
|
||||
// Per-board external-PA gain curve (radio_dbm -> measured gain in dB, from
|
||||
@@ -441,4 +444,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t
|
||||
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
|
||||
|
||||
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// only built when the env enables the core BLE stack (build_as_lib.py globs this dir)
|
||||
#ifdef PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH
|
||||
#include "SerialBLEInterface.h"
|
||||
#include <BluetoothLock.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Nordic UART 6E4000xx-B5A3-F393-E0A9-E50E24DCCA9E, as raw bytes: the core lib's string
|
||||
// parser uses sscanf("%llx"), which newlib-nano on the RP2040 doesn't support (yields all zeros)
|
||||
#define NUS_UUID(n) { 0x6E, 0x40, 0x00, n, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E }
|
||||
static const uint8_t SERVICE_UUID[16] = NUS_UUID(0x01);
|
||||
static const uint8_t CHARACTERISTIC_UUID_RX[16] = NUS_UUID(0x02);
|
||||
static const uint8_t CHARACTERISTIC_UUID_TX[16] = NUS_UUID(0x03);
|
||||
|
||||
// The BLE core lib's own setValue()/notify path reallocs the value buffer on every call,
|
||||
// so a second frame queued before the radio drained the first would corrupt it. We keep our
|
||||
// own frame queue and drive att_server_notify() from the can-send-now callback instead.
|
||||
|
||||
SerialBLEInterface::SerialBLEInterface()
|
||||
: BLEService(BLEUUID(SERVICE_UUID)),
|
||||
_rx(BLEUUID(CHARACTERISTIC_UUID_RX), BLEWrite, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED),
|
||||
_tx(BLEUUID(CHARACTERISTIC_UUID_TX), BLERead | BLENotify, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED)
|
||||
{
|
||||
_isEnabled = false;
|
||||
_tx_pending = false;
|
||||
send_queue_len = 0;
|
||||
recv_queue_len = 0;
|
||||
memset(&_can_send, 0, sizeof(_can_send));
|
||||
_rx.setCallbacks(this);
|
||||
addCharacteristic(&_rx);
|
||||
addCharacteristic(&_tx);
|
||||
}
|
||||
|
||||
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
|
||||
// JustWorks here only makes the server request pairing on connect; caps are overridden below
|
||||
BLE.setSecurity(BLESecurityJustWorks);
|
||||
// adv data carries the 128-bit service UUID, leaving room for only 8 name chars;
|
||||
// the full name goes in the scan response
|
||||
BLE.begin(prefix);
|
||||
|
||||
if (strcmp(name, "@@MAC") == 0) {
|
||||
bd_addr_t a;
|
||||
gap_local_bd_addr(a);
|
||||
sprintf(name, "%02X%02X%02X%02X%02X%02X", a[0], a[1], a[2], a[3], a[4], a[5]); // modify (IN-OUT param)
|
||||
}
|
||||
char dev_name[32+16];
|
||||
snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, name);
|
||||
|
||||
BLE.server()->setName(dev_name); // GAP device name characteristic
|
||||
BLE.server()->addService(this);
|
||||
BLE.server()->setCallbacks(this);
|
||||
|
||||
// static passkey pairing with MITM protection, matching the esp32/nrf52 interfaces
|
||||
sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
|
||||
sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION | SM_AUTHREQ_BONDING);
|
||||
sm_use_fixed_passkey_in_display_role(pin_code);
|
||||
|
||||
size_t n = strlen(dev_name);
|
||||
if (n > sizeof(_scan_rsp) - 2) n = sizeof(_scan_rsp) - 2;
|
||||
_scan_rsp[0] = n + 1;
|
||||
_scan_rsp[1] = BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME;
|
||||
memcpy(&_scan_rsp[2], dev_name, n);
|
||||
gap_scan_response_set_data(n + 2, _scan_rsp);
|
||||
|
||||
BLE_DEBUG_PRINTLN("begin: name=%s", dev_name);
|
||||
}
|
||||
|
||||
void SerialBLEInterface::clearBuffers() {
|
||||
send_queue_len = 0;
|
||||
recv_queue_len = 0;
|
||||
_tx_pending = false; // a stale registration just fires into an empty queue; BTstack ignores double-adds
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onConnect(BLEServer* s) {
|
||||
BLE_DEBUG_PRINTLN("connected handle=0x%04X", _tx.conHandle());
|
||||
clearBuffers();
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onDisconnect(BLEServer* s) {
|
||||
BLE_DEBUG_PRINTLN("disconnected");
|
||||
clearBuffers(); // BTstack re-enables advertising on its own
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onWrite(BLECharacteristic* c) {
|
||||
if (c != &_rx) return;
|
||||
size_t len = _rx.valueLen();
|
||||
if (len == 0 || len > MAX_FRAME_SIZE) {
|
||||
BLE_DEBUG_PRINTLN("onWrite: bad frame len=%u", (unsigned)len);
|
||||
return;
|
||||
}
|
||||
if (recv_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
BLE_DEBUG_PRINTLN("onWrite: recv queue full, dropping frame");
|
||||
return;
|
||||
}
|
||||
recv_queue[recv_queue_len].len = len;
|
||||
memcpy(recv_queue[recv_queue_len].buf, _rx.valueData(), len);
|
||||
recv_queue_len++;
|
||||
}
|
||||
|
||||
// caller holds the BT lock (or is in the BT context)
|
||||
void SerialBLEInterface::kickSend() {
|
||||
if (_tx_pending) return;
|
||||
_tx_pending = true;
|
||||
_can_send.callback = onCanSend;
|
||||
_can_send.context = this;
|
||||
if (att_server_register_can_send_now_callback(&_can_send, _tx.conHandle()) != 0) {
|
||||
_tx_pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
// BT context: one notification per can-send-now, re-arm while frames remain
|
||||
void SerialBLEInterface::sendNext() {
|
||||
_tx_pending = false;
|
||||
if (send_queue_len == 0) return;
|
||||
if (!isConnected()) {
|
||||
BLE_DEBUG_PRINTLN("sendNext: not connected, clearing send queue");
|
||||
send_queue_len = 0;
|
||||
return;
|
||||
}
|
||||
uint16_t h = _tx.conHandle();
|
||||
Frame& f = send_queue[0];
|
||||
uint16_t mtu = att_server_get_mtu(h);
|
||||
if (f.len + 3 > mtu) {
|
||||
// att would silently truncate; drop instead (client must negotiate MTU >= MAX_FRAME_SIZE+3)
|
||||
BLE_DEBUG_PRINTLN("sendNext: frame len=%u exceeds mtu=%u, dropping", f.len, mtu);
|
||||
} else {
|
||||
uint8_t err = att_server_notify(h, _tx.valueHandle(), f.buf, f.len);
|
||||
if (err == BTSTACK_ACL_BUFFERS_FULL) {
|
||||
kickSend();
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
BLE_DEBUG_PRINTLN("sendNext: notify failed err=%u, dropping", err);
|
||||
} else {
|
||||
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", f.len, f.buf[0]);
|
||||
}
|
||||
}
|
||||
send_queue_len--;
|
||||
memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame));
|
||||
if (send_queue_len > 0) kickSend();
|
||||
}
|
||||
|
||||
void SerialBLEInterface::enable() {
|
||||
if (_isEnabled) return;
|
||||
_isEnabled = true;
|
||||
clearBuffers();
|
||||
BLE.startAdvertising(true);
|
||||
}
|
||||
|
||||
void SerialBLEInterface::disconnect() {
|
||||
uint16_t h = _tx.conHandle();
|
||||
if (h) gap_disconnect(h);
|
||||
}
|
||||
|
||||
void SerialBLEInterface::disable() {
|
||||
_isEnabled = false;
|
||||
BLE_DEBUG_PRINTLN("disable");
|
||||
disconnect();
|
||||
BLE.stopAdvertising();
|
||||
}
|
||||
|
||||
bool SerialBLEInterface::isConnected() const {
|
||||
// notifications can only be enabled once the link is authenticated (CCCD inherits the perms)
|
||||
return _isEnabled && _tx.conHandle() != 0 && _tx.notifyEnabled();
|
||||
}
|
||||
|
||||
bool SerialBLEInterface::isWriteBusy() const {
|
||||
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
if (len == 0 || len > MAX_FRAME_SIZE) {
|
||||
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
|
||||
return 0;
|
||||
}
|
||||
if (!isConnected()) return 0;
|
||||
|
||||
BluetoothLock lock;
|
||||
if (send_queue_len >= FRAME_QUEUE_SIZE) {
|
||||
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
|
||||
return 0;
|
||||
}
|
||||
send_queue[send_queue_len].len = len;
|
||||
memcpy(send_queue[send_queue_len].buf, src, len);
|
||||
send_queue_len++;
|
||||
kickSend();
|
||||
return len;
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
BluetoothLock lock;
|
||||
if (recv_queue_len == 0) return 0;
|
||||
|
||||
size_t len = recv_queue[0].len;
|
||||
memcpy(dest, recv_queue[0].buf, len);
|
||||
recv_queue_len--;
|
||||
memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame));
|
||||
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]);
|
||||
return len;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include <BLE.h>
|
||||
#include <btstack.h>
|
||||
|
||||
// Nordic UART service over the arduino-pico BLE library (BTstack on the CYW43).
|
||||
// Build with -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH so the core links liblwip-bt.
|
||||
class SerialBLEInterface : public BaseSerialInterface, BLEService, BLEServerCallbacks, BLECharacteristicCallbacks {
|
||||
// subclass only to reach the protected connection/notify state
|
||||
struct Characteristic : public BLECharacteristic {
|
||||
using BLECharacteristic::BLECharacteristic;
|
||||
uint16_t valueHandle() const { return _valueHandle; }
|
||||
uint16_t conHandle() const { return con_handle; }
|
||||
bool notifyEnabled() const { return _notificationEnabled; }
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
uint8_t len;
|
||||
uint8_t buf[MAX_FRAME_SIZE];
|
||||
};
|
||||
|
||||
#define FRAME_QUEUE_SIZE 8
|
||||
|
||||
Characteristic _rx;
|
||||
Characteristic _tx;
|
||||
bool _isEnabled;
|
||||
bool _tx_pending; // a can-send-now callback is registered
|
||||
btstack_context_callback_registration_t _can_send;
|
||||
uint8_t _scan_rsp[31]; // complete local name; BTstack keeps the pointer
|
||||
|
||||
uint8_t send_queue_len;
|
||||
Frame send_queue[FRAME_QUEUE_SIZE];
|
||||
uint8_t recv_queue_len;
|
||||
Frame recv_queue[FRAME_QUEUE_SIZE];
|
||||
|
||||
void clearBuffers();
|
||||
void kickSend();
|
||||
void sendNext();
|
||||
static void onCanSend(void* ctx) { ((SerialBLEInterface*)ctx)->sendNext(); }
|
||||
|
||||
// BLE library callbacks (run in the BT context)
|
||||
void onWrite(BLECharacteristic* c) override;
|
||||
void onConnect(BLEServer* s) override;
|
||||
void onDisconnect(BLEServer* s) override;
|
||||
|
||||
public:
|
||||
SerialBLEInterface();
|
||||
|
||||
/**
|
||||
* init the BLE interface.
|
||||
* @param prefix a prefix for the device name
|
||||
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
|
||||
* @param pin_code the BLE security pin
|
||||
*/
|
||||
void begin(const char* prefix, char* name, uint32_t pin_code);
|
||||
|
||||
void disconnect();
|
||||
void enable() override;
|
||||
void disable() override;
|
||||
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;
|
||||
};
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define BLE_DEBUG_PRINT(...) {}
|
||||
#define BLE_DEBUG_PRINTLN(...) {}
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
|
||||
// Board must keep the GPS RTC backup pin powered and can cut GPS power once we return.
|
||||
// Returns true if the command was acked within 3 attempts, otherwise returns false.
|
||||
static inline bool airohaEnterSleep(LocationProvider* nmea) {
|
||||
for (uint8_t attempt = 0; attempt < 3; attempt++) {
|
||||
nmea->drain();
|
||||
nmea->sendSentence("$PAIR650,0");
|
||||
if (nmea->waitFor("$PAIR001,650,0", 50)) { // wait for the command received signal
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
Serial.printf("Airoha RTC Backup sleep command accepted by GPS after %u attempts\r\n", attempt + 1);
|
||||
#endif
|
||||
nmea->waitFor("$PAIR650,0", 50); // give the GPS 50ms grace to signal it is ready for sleep
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -774,14 +774,18 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
|
||||
#endif
|
||||
|
||||
// Give GPS a moment to power up and send data
|
||||
delay(1000);
|
||||
|
||||
// We'll consider GPS detected if we see any data on Serial1
|
||||
// Give the GPS time to power up and emit its first NMEA sentence. Cold-start
|
||||
// modules can take several seconds, so poll for up to ~5s instead of checking
|
||||
// once after a fixed 1s delay -- returning as soon as any data appears, so
|
||||
// fast modules add no boot delay (issue #2258).
|
||||
#ifdef ENV_SKIP_GPS_DETECT
|
||||
gps_detected = true;
|
||||
#else
|
||||
gps_detected = (Serial1.available() > 0);
|
||||
gps_detected = false;
|
||||
for (int i = 0; i < 20 && !gps_detected; i++) {
|
||||
delay(250);
|
||||
gps_detected = (Serial1.available() > 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (gps_detected) {
|
||||
@@ -849,11 +853,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef RAK_3401
|
||||
//set initial waking state
|
||||
pinMode(ioPin,OUTPUT);
|
||||
digitalWrite(ioPin,LOW);
|
||||
delay(500);
|
||||
digitalWrite(ioPin,HIGH);
|
||||
#endif
|
||||
|
||||
// give gps time to power up
|
||||
delay(500);
|
||||
|
||||
//Try to init RAK12500 on I2C
|
||||
@@ -889,7 +897,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef RAK_3401
|
||||
pinMode(ioPin, INPUT);
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
||||
return false;
|
||||
}
|
||||
@@ -948,8 +958,10 @@ void EnvironmentSensorManager::gpsDutyCycleLoop() {
|
||||
void EnvironmentSensorManager::start_gps() {
|
||||
gps_active = true;
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#ifndef RAK_3401
|
||||
pinMode(gpsResetPin, OUTPUT);
|
||||
digitalWrite(gpsResetPin, HIGH);
|
||||
#endif
|
||||
return;
|
||||
#endif
|
||||
|
||||
@@ -964,8 +976,10 @@ void EnvironmentSensorManager::start_gps() {
|
||||
void EnvironmentSensorManager::stop_gps() {
|
||||
gps_active = false;
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA
|
||||
pinMode(gpsResetPin, OUTPUT);
|
||||
digitalWrite(gpsResetPin, LOW);
|
||||
#endif
|
||||
return;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ public:
|
||||
bool isGpsDutySleeping() const override {
|
||||
return gps_master_enabled && gps_duty_sleep_sec > 0 && !_gps_keep_awake && !gps_active;
|
||||
}
|
||||
bool isGPSDetected() const override { return gps_detected; }
|
||||
#else
|
||||
EnvironmentSensorManager(){};
|
||||
#endif
|
||||
|
||||
@@ -63,12 +63,66 @@
|
||||
#define LPP_ERROR_OVERFLOW 1
|
||||
#define LPP_ERROR_UNKOWN_TYPE 2
|
||||
|
||||
class LPPReader {
|
||||
const uint8_t* _buf;
|
||||
uint8_t _len;
|
||||
uint8_t _pos;
|
||||
class LPPData {
|
||||
public:
|
||||
static uint8_t getDataSize(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
return 9;
|
||||
case LPP_POLYLINE:
|
||||
return 8; // TODO: this is MINIMIUM
|
||||
case LPP_GYROMETER:
|
||||
case LPP_ACCELEROMETER:
|
||||
return 6;
|
||||
case LPP_GENERIC_SENSOR:
|
||||
case LPP_FREQUENCY:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
case LPP_UNIXTIME:
|
||||
return 4;
|
||||
case LPP_COLOUR:
|
||||
return 3;
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
case LPP_LUMINOSITY:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
case LPP_DIRECTION:
|
||||
case LPP_POWER:
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
static uint32_t getMultiplier(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_CURRENT:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
return 1000;
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
return 100;
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return 10;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool isSigned(uint8_t type) {
|
||||
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
|
||||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
||||
}
|
||||
|
||||
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
uint32_t value = 0;
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
value = (value << 8) + buffer[i];
|
||||
@@ -85,6 +139,36 @@ class LPPReader {
|
||||
return sign * ((float) value / multiplier);
|
||||
}
|
||||
|
||||
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
// check sign
|
||||
bool sign = value < 0;
|
||||
if (sign) value = -value;
|
||||
|
||||
// get value to store
|
||||
uint32_t v = value * multiplier;
|
||||
|
||||
// format an uint32_t as if it was an int32_t
|
||||
if (is_signed & sign) {
|
||||
uint32_t mask = (1 << (size * 8)) - 1;
|
||||
v = v & mask;
|
||||
if (sign) v = mask - v + 1;
|
||||
}
|
||||
|
||||
// add bytes (MSB first)
|
||||
for (uint8_t i=1; i<=size; i++) {
|
||||
dest[size - i] = (v & 0xFF);
|
||||
v >>= 8;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class LPPReader {
|
||||
const uint8_t* _buf;
|
||||
uint8_t _len;
|
||||
uint8_t _pos;
|
||||
|
||||
public:
|
||||
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
||||
|
||||
@@ -103,88 +187,58 @@ public:
|
||||
}
|
||||
|
||||
bool readGPS(float& lat, float& lon, float& alt) {
|
||||
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
||||
lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readVoltage(float& voltage) {
|
||||
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
||||
voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readCurrent(float& amps) {
|
||||
amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
|
||||
amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readPower(float& watts) {
|
||||
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readTemperature(float& degrees_c) {
|
||||
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
|
||||
degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readPressure(float& pa) {
|
||||
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
|
||||
pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readRelativeHumidity(float& pct) {
|
||||
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
|
||||
pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readAltitude(float& m) {
|
||||
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
|
||||
m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readLuminosity(float& lux) {
|
||||
lux = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
lux = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readPercentage(float& pct) {
|
||||
pct = getFloat(&_buf[_pos], 1, 1, false); _pos += 1;
|
||||
pct = LPPData::getFloat(&_buf[_pos], 1, 1, false); _pos += 1;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readConcentration(float& ppm) {
|
||||
ppm = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
ppm = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readDistance(float& m) {
|
||||
m = getFloat(&_buf[_pos], 4, 1000, false); _pos += 4;
|
||||
m = LPPData::getFloat(&_buf[_pos], 4, 1000, false); _pos += 4;
|
||||
return _pos <= _len;
|
||||
}
|
||||
|
||||
void skipData(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
_pos += 9; break;
|
||||
case LPP_POLYLINE:
|
||||
_pos += 8; break; // TODO: this is MINIMUM
|
||||
case LPP_GYROMETER:
|
||||
case LPP_ACCELEROMETER:
|
||||
_pos += 6; break;
|
||||
case LPP_GENERIC_SENSOR:
|
||||
case LPP_FREQUENCY:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
case LPP_UNIXTIME:
|
||||
_pos += 4; break;
|
||||
case LPP_COLOUR:
|
||||
_pos += 3; break;
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
case LPP_LUMINOSITY:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
case LPP_DIRECTION:
|
||||
case LPP_POWER:
|
||||
_pos += 2; break;
|
||||
default:
|
||||
_pos++;
|
||||
}
|
||||
_pos += LPPData::getDataSize(type);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,6 +255,19 @@ class LPPWriter {
|
||||
public:
|
||||
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
|
||||
|
||||
bool writeData(uint8_t channel, uint8_t type, float v) {
|
||||
uint8_t sz = LPPData::getDataSize(type);
|
||||
bool s = LPPData::isSigned(type);
|
||||
uint32_t mul = LPPData::getMultiplier(type);
|
||||
if (_len + 2 + sz <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = type;
|
||||
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeVoltage(uint8_t channel, float voltage) {
|
||||
if (_len + 4 <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "Mesh.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
class LocationProvider {
|
||||
@@ -22,21 +24,25 @@ public:
|
||||
virtual long getHDOP() { return -1; }
|
||||
virtual bool isValid() = 0;
|
||||
virtual long getTimestamp() = 0;
|
||||
// Default no-op body (every existing subclass overrides this anyway --
|
||||
// MicroNMEALocationProvider.h forwards to the NMEA lib,
|
||||
// EnvironmentSensorManager.cpp's GPS classes no-op it explicitly) --
|
||||
// previously declared with NO definition anywhere in the codebase
|
||||
// (confirmed by repo-wide grep), which is latently a hard link error
|
||||
// ("undefined symbol: typeinfo for LocationProvider") for ANY subclass
|
||||
// that doesn't override it, since base-subobject construction of any
|
||||
// LocationProvider-derived object needs this class's own vtable/RTTI to
|
||||
// exist as a real linkable symbol. Surfaced by variants/sim's
|
||||
// SimLocationProvider (Phase 3 of the sim plan) being the first
|
||||
// consumer to construct one outside of code that always overrides it.
|
||||
// Default no-op body: a subclass that doesn't override it (variants/sim's
|
||||
// SimLocationProvider) still links -- declared without a body, the base
|
||||
// class's vtable was an undefined symbol.
|
||||
virtual void sendSentence(const char * sentence) { }
|
||||
virtual bool waitFor(const char* prefix, uint32_t timeout_ms) { return false; }
|
||||
virtual void drain() { }
|
||||
virtual void reset() = 0;
|
||||
virtual void begin() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void loop() = 0;
|
||||
virtual bool isEnabled() = 0;
|
||||
|
||||
// Format compact diagnostics in the caller-provided buffer. Providers that
|
||||
// have more information (for example UART counters) can override this.
|
||||
virtual void formatDiagnostics(char* out, size_t out_size) {
|
||||
if (out_size == 0) return;
|
||||
snprintf(out, out_size, "en:%u sat:%ld fix:%u",
|
||||
isEnabled() ? 1U : 0U,
|
||||
satellitesCount(),
|
||||
isValid() ? 1U : 0U);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifdef GPS_SKYVIEW
|
||||
#include "GpsSky.h"
|
||||
#endif
|
||||
#include <string.h>
|
||||
|
||||
#ifndef GPS_EN
|
||||
#ifdef PIN_GPS_EN
|
||||
@@ -58,6 +59,42 @@ class MicroNMEALocationProvider : public LocationProvider {
|
||||
unsigned long _last_time_sync = 0;
|
||||
static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes
|
||||
|
||||
#ifdef GPS_L76K
|
||||
void configureL76K() {
|
||||
// Reapply the constellation mode at each provider start instead of
|
||||
// assuming it survives a receiver power cycle.
|
||||
// Mode 7 enables GPS + BeiDou + GLONASS.
|
||||
delay(250);
|
||||
MicroNMEA::sendSentence(*_gps_serial, "$PCAS04,7");
|
||||
delay(250);
|
||||
}
|
||||
#endif
|
||||
// Observation-only diagnostics. These fields do not change GPS power,
|
||||
// timing, reset behaviour, or parser input.
|
||||
uint32_t _uart_bytes = 0;
|
||||
uint32_t _nmea_checksum_ok = 0;
|
||||
uint32_t _nmea_checksum_bad = 0;
|
||||
uint32_t _begin_calls = 0;
|
||||
uint32_t _stop_calls = 0;
|
||||
unsigned long _last_uart_ms = 0;
|
||||
unsigned long _last_nmea_ms = 0;
|
||||
unsigned long _last_fix_ms = 0;
|
||||
bool _uart_seen = false;
|
||||
bool _nmea_seen = false;
|
||||
bool _fix_seen = false;
|
||||
|
||||
static uint32_t ageMs(unsigned long timestamp) {
|
||||
return (uint32_t)(millis() - timestamp);
|
||||
}
|
||||
|
||||
static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) {
|
||||
if (!seen) {
|
||||
snprintf(out, out_size, "never");
|
||||
} else {
|
||||
snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
public :
|
||||
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
|
||||
nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) {
|
||||
@@ -90,6 +127,7 @@ public :
|
||||
// last stop() still reads valid, and the GPS duty cycle put the receiver
|
||||
// back to sleep a tick after waking it, before any new position came in.
|
||||
nmea.clear();
|
||||
_begin_calls++;
|
||||
claim();
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, GPS_EN_ACTIVE);
|
||||
@@ -105,12 +143,18 @@ public :
|
||||
delay(10);
|
||||
digitalWrite(_pin_reset, !GPS_RESET_ACTIVE);
|
||||
}
|
||||
#ifdef GPS_L76K
|
||||
// EnvironmentSensorManager calls reset() immediately after begin().
|
||||
// Configure only after that reset so the setting is not discarded.
|
||||
configureL76K();
|
||||
#endif
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
#ifdef GPS_SKYVIEW
|
||||
_sky.halt(); // a receiver that is off is not searching: stop the search clock
|
||||
#endif
|
||||
_stop_calls++;
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
|
||||
}
|
||||
@@ -154,22 +198,69 @@ public :
|
||||
return dt.unixtime();
|
||||
}
|
||||
|
||||
void drain() { while (_gps_serial->available()) _gps_serial->read(); }
|
||||
|
||||
const char* getSentence() const { return nmea.getSentence(); }
|
||||
|
||||
void sendSentence(const char *sentence) override {
|
||||
nmea.sendSentence(*_gps_serial, sentence);
|
||||
}
|
||||
|
||||
bool waitFor(const char* prefix, uint32_t timeout_ms) {
|
||||
size_t plen = strlen(prefix);
|
||||
uint32_t timeout = millis() + timeout_ms;
|
||||
while ((int32_t)(millis() - timeout) < 0) {
|
||||
if (_gps_serial->available()) {
|
||||
char c = _gps_serial->read();
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
Serial.print(c);
|
||||
#endif
|
||||
if (nmea.process(c) && strncmp(nmea.getSentence(), prefix, plen) == 0) return true;
|
||||
} else {
|
||||
yield();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
|
||||
while (_gps_serial->available()) {
|
||||
char c = _gps_serial->read();
|
||||
_rx_chars++;
|
||||
_uart_bytes++;
|
||||
_last_uart_ms = millis();
|
||||
_uart_seen = true;
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
Serial.print(c);
|
||||
#endif
|
||||
nmea.process(c);
|
||||
bool parsed = nmea.process(c);
|
||||
#ifdef GPS_SKYVIEW
|
||||
_sky.feed(c);
|
||||
#endif
|
||||
|
||||
// MicroNMEA leaves the completed sentence in its buffer after the
|
||||
// first line terminator. CRLF therefore counts once: CR observes the
|
||||
// sentence, then LF clears the now-empty buffer.
|
||||
if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') {
|
||||
if (MicroNMEA::testChecksum(nmea.getSentence())) {
|
||||
_nmea_checksum_ok++;
|
||||
_last_nmea_ms = millis();
|
||||
_nmea_seen = true;
|
||||
|
||||
// Refresh fix age only when a newly parsed GGA/RMC sentence
|
||||
// carries a valid fix. Other valid NMEA sentences must not
|
||||
// make an old position look fresh.
|
||||
const char* message_id = nmea.getMessageID();
|
||||
if (parsed && isValid() &&
|
||||
(strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) {
|
||||
_last_fix_ms = millis();
|
||||
_fix_seen = true;
|
||||
}
|
||||
} else {
|
||||
_nmea_checksum_bad++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid()) time_valid = 0;
|
||||
@@ -192,4 +283,31 @@ public :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void formatDiagnostics(char* out, size_t out_size) override {
|
||||
if (out_size == 0) return;
|
||||
|
||||
char uart_age[11];
|
||||
char nmea_age[11];
|
||||
char fix_age[11];
|
||||
formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms);
|
||||
formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms);
|
||||
formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms);
|
||||
|
||||
// Compact enough for CommonCLI's 160-byte reply buffer, including the
|
||||
// "req:" prefix added by CommonCLI.
|
||||
snprintf(out, out_size,
|
||||
"en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu",
|
||||
isEnabled() ? 1U : 0U,
|
||||
(unsigned long)_uart_bytes,
|
||||
uart_age,
|
||||
(unsigned long)_nmea_checksum_ok,
|
||||
(unsigned long)_nmea_checksum_bad,
|
||||
nmea_age,
|
||||
satellitesCount(),
|
||||
isValid() ? 1U : 0U,
|
||||
fix_age,
|
||||
(unsigned long)_begin_calls,
|
||||
(unsigned long)_stop_calls);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
class STM32Board : public mesh::MainBoard {
|
||||
protected:
|
||||
@@ -12,6 +13,8 @@ public:
|
||||
startup_reason = BD_STARTUP_NORMAL;
|
||||
}
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
uint8_t getStartupReason() const override { return startup_reason; }
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
|
||||
virtual bool isOn() = 0;
|
||||
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
|
||||
virtual void forceFullRefresh() {} // next refresh will be full for eink
|
||||
virtual void turnOn() = 0;
|
||||
virtual void turnOff() = 0;
|
||||
virtual void clear() = 0;
|
||||
|
||||
@@ -66,8 +66,8 @@ private:
|
||||
public:
|
||||
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
|
||||
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
|
||||
void begin();
|
||||
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
|
||||
virtual void begin();
|
||||
virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
|
||||
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
|
||||
uint8_t getPin() { return _pin; }
|
||||
bool isPressed() const;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "RotaryInputGPIO.h"
|
||||
|
||||
bool RotaryInputGPIO::begin() {
|
||||
|
||||
if (_pin_a >= 0) {
|
||||
pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT);
|
||||
}
|
||||
if (_pin_b >= 0) {
|
||||
pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT);
|
||||
}
|
||||
|
||||
b_prec = digitalRead(_pin_b);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RotaryInputEvent RotaryInputGPIO::poll() {
|
||||
RotaryInputEvent ev = RotaryInputEvent::None;
|
||||
bool a = digitalRead(_pin_a);
|
||||
bool b = digitalRead(_pin_b);
|
||||
|
||||
// this is the simplest scheme and it works well
|
||||
// for thinknode M8, just read A when B rises ;)
|
||||
if (!b_prec && b) { // rising edge of A
|
||||
if (a) {
|
||||
ev = RotaryInputEvent::Next;
|
||||
} else {
|
||||
ev = RotaryInputEvent::Prev;
|
||||
}
|
||||
}
|
||||
b_prec = b;
|
||||
|
||||
return ev;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "RotaryInput.h"
|
||||
|
||||
#define BUTTON_EVENT_UP 5
|
||||
#define BUTTON_EVENT_DOWN 6
|
||||
|
||||
class RotaryInputGPIO: public RotaryInput {
|
||||
int8_t _pin_a;
|
||||
bool _pull_a;
|
||||
int8_t _pin_b;
|
||||
bool _pull_b;
|
||||
bool b_prec;
|
||||
public:
|
||||
RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {}
|
||||
|
||||
bool begin() override;
|
||||
RotaryInputEvent poll() override;
|
||||
bool isReady() const override { return true;}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "SH1107Display.h"
|
||||
#include <Adafruit_GrayOLED.h>
|
||||
#include "Adafruit_SH110X.h"
|
||||
|
||||
#ifndef DISPLAY_ROTATION
|
||||
#define DISPLAY_ROTATION 0
|
||||
#endif
|
||||
|
||||
ColorVal UIColor::window_bkg = SH110X_BLACK;
|
||||
ColorVal UIColor::title_bkg = SH110X_BLACK;
|
||||
ColorVal UIColor::title_txt = SH110X_WHITE;
|
||||
ColorVal UIColor::primary_txt = SH110X_WHITE;
|
||||
ColorVal UIColor::secondary_txt = SH110X_WHITE;
|
||||
ColorVal UIColor::warning_txt = SH110X_WHITE;
|
||||
ColorVal UIColor::popup_bkg = SH110X_BLACK;
|
||||
ColorVal UIColor::popup_txt = SH110X_WHITE;
|
||||
ColorVal UIColor::corp_blue = SH110X_WHITE;
|
||||
|
||||
bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr)
|
||||
{
|
||||
wire.beginTransmission(addr);
|
||||
uint8_t error = wire.endTransmission();
|
||||
return (error == 0);
|
||||
}
|
||||
|
||||
bool SH1107Display::begin()
|
||||
{
|
||||
bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
if (result) {
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SH1107Display::turnOn()
|
||||
{
|
||||
display.oled_command(SH110X_DISPLAYON);
|
||||
uint8_t cmd[] = {0xD5, 0xF0};
|
||||
display.oled_commandList(cmd, 2);
|
||||
_isOn = true;
|
||||
}
|
||||
|
||||
void SH1107Display::turnOff()
|
||||
{
|
||||
display.oled_command(SH110X_DISPLAYOFF);
|
||||
_isOn = false;
|
||||
}
|
||||
|
||||
void SH1107Display::clear()
|
||||
{
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
}
|
||||
|
||||
void SH1107Display::startFrame(ColorVal bkg)
|
||||
{
|
||||
display.clearDisplay(); // TODO: apply 'bkg'
|
||||
display.setContrast(120); // 0-127. default setting was causing some flickering.
|
||||
// display.SH110X_SETPRECHARGE(255);
|
||||
_color = SH110X_WHITE;
|
||||
display.setTextColor(_color);
|
||||
display.setTextSize(1);
|
||||
display.cp437(true); // Use full 256 char 'Code Page 437' font
|
||||
}
|
||||
|
||||
void SH1107Display::setTextSize(int sz)
|
||||
{
|
||||
display.setTextSize(sz);
|
||||
}
|
||||
|
||||
void SH1107Display::setColor(ColorVal c)
|
||||
{
|
||||
_color = (c != 0) ? SH110X_WHITE : SH110X_BLACK;
|
||||
display.setTextColor(_color);
|
||||
}
|
||||
|
||||
void SH1107Display::setCursor(int x, int y)
|
||||
{
|
||||
display.setCursor(x, y);
|
||||
}
|
||||
|
||||
void SH1107Display::print(const char *str)
|
||||
{
|
||||
display.print(str);
|
||||
}
|
||||
|
||||
void SH1107Display::fillRect(int x, int y, int w, int h)
|
||||
{
|
||||
display.fillRect(x, y, w, h, _color);
|
||||
}
|
||||
|
||||
void SH1107Display::drawRect(int x, int y, int w, int h)
|
||||
{
|
||||
display.drawRect(x, y, w, h, _color);
|
||||
}
|
||||
|
||||
void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)
|
||||
{
|
||||
display.drawBitmap(x, y, bits, w, h, SH110X_WHITE);
|
||||
}
|
||||
|
||||
uint16_t SH1107Display::getTextWidth(const char *str)
|
||||
{
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
void SH1107Display::endFrame()
|
||||
{
|
||||
display.display();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#define SH110X_NO_SPLASH
|
||||
#include <Adafruit_SH110X.h>
|
||||
|
||||
#ifndef PIN_OLED_RESET
|
||||
#define PIN_OLED_RESET -1
|
||||
#endif
|
||||
|
||||
#ifndef DISPLAY_ADDRESS
|
||||
#define DISPLAY_ADDRESS 0x3c
|
||||
#endif
|
||||
|
||||
class SH1107Display : public DisplayDriver
|
||||
{
|
||||
Adafruit_SH1107 display;
|
||||
bool _isOn;
|
||||
uint8_t _color;
|
||||
|
||||
bool i2c_probe(TwoWire &wire, uint8_t addr);
|
||||
|
||||
public:
|
||||
SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; }
|
||||
bool begin();
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(ColorVal c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char *str) override;
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char *str) override;
|
||||
void endFrame() override;
|
||||
};
|
||||
@@ -1,13 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "helpers/ConfigSerializer.h"
|
||||
|
||||
class NativeFileSystem {
|
||||
public:
|
||||
void mkdir(const char*) { }
|
||||
};
|
||||
#define FILESYSTEM NativeFileSystem
|
||||
#include "helpers/CommonCLI.h"
|
||||
#undef FILESYSTEM
|
||||
#include "helpers/DynamicConfigSerializer.h"
|
||||
|
||||
#define TEST_INT_S "56"
|
||||
#define TEST_INT 56
|
||||
@@ -192,28 +185,131 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
NodePrefs saved;
|
||||
saved.radio_fem_rxgain = 0;
|
||||
saved.radio_fem_txgain = 1;
|
||||
class TestNested : public ConfigSerializer {
|
||||
class Inner : public ConfigSerializer {
|
||||
protected:
|
||||
void structure() override { } // no properties, so it writes as '{}'
|
||||
};
|
||||
Inner inner;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("age", age);
|
||||
def("inner", inner);
|
||||
def("name", name, sizeof(name)); // comes *after* the empty sub-object
|
||||
}
|
||||
public:
|
||||
int32_t age;
|
||||
char name[16];
|
||||
};
|
||||
|
||||
MockPrintStream output;
|
||||
ASSERT_TRUE(saved.saveSerial(output));
|
||||
TEST(ConfigSerializer, LoadSerial_EmptyObject) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}");
|
||||
TestNested data;
|
||||
data.name[0] = 0;
|
||||
|
||||
std::string serialised(reinterpret_cast<const char*>(output.getBytes()), output.getLength());
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
MockInputStream input(serialised.c_str());
|
||||
NodePrefs loaded;
|
||||
loaded.radio_fem_rxgain = 1;
|
||||
loaded.radio_fem_txgain = 0;
|
||||
|
||||
ASSERT_TRUE(loaded.loadSerial(input));
|
||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match); // properties after an empty object must still load
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}");
|
||||
TestNested data;
|
||||
data.name[0] = 0;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, GetSet_Basic) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ("11", tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, Set_Replaces) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
bool s3 = data.setByKey("age", "333");
|
||||
EXPECT_TRUE(s3);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ("333", tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, GetUnknown_Fail) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
EXPECT_TRUE(s1);
|
||||
|
||||
char tmp[32];
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_FALSE(g2);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, SaveCustom_Basic) {
|
||||
MockPrintStream s;
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
char tmp[128];
|
||||
memcpy(tmp, s.getBytes(), l);
|
||||
tmp[l] = 0;
|
||||
|
||||
const char* expect = "{age:\"11\",name:\"Scott\"}";
|
||||
EXPECT_STREQ(expect, tmp);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, LoadCustom_Basic) {
|
||||
MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}");
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ(TEST_INT_S, tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/gat562_30s_mesh_kit
|
||||
-D RAK_4631
|
||||
-D RAK_BOARD
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D PIN_OLED_RESET=-1
|
||||
|
||||
@@ -5,7 +5,6 @@ board_check = true
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/gat562_mesh_evb_pro
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
|
||||
@@ -5,7 +5,6 @@ board_check = true
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/gat562_mesh_tracker_pro
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D PIN_OLED_RESET=-1
|
||||
|
||||
@@ -8,7 +8,6 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/gat562_mesh_watch13
|
||||
-D RAK_4631
|
||||
-D RAK_BOARD
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D PIN_OLED_RESET=-1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Heltec_ct62]
|
||||
extends = esp32_base
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-I variants/heltec_ct62
|
||||
|
||||
@@ -173,6 +173,7 @@ extends = Heltec_RC32
|
||||
build_flags =
|
||||
${Heltec_RC32.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D DISPLAY_CLASS=NullDisplayDriver
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
@@ -185,6 +186,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter}
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<helpers/ui/NullDisplayDriver.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
@@ -314,6 +316,7 @@ extends = Heltec_RC32_with_display
|
||||
build_flags =
|
||||
${Heltec_RC32_with_display.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D UI_HAS_ROTARY_INPUT
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
@@ -324,6 +327,7 @@ build_flags =
|
||||
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
|
||||
@@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool T096Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool T096Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void T096Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -4,28 +4,33 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
#include <helpers/RefCountedDigitalPin.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
class T096Board : public NRF52BoardDCDC {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
protected:
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
void variant_shutdown();
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
public:
|
||||
RefCountedDigitalPin periph_power;
|
||||
LoRaFEMControl loRaFEMControl;
|
||||
|
||||
T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {}
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
const char* getManufacturerName() const override ;
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/heltec_t096
|
||||
-I src/helpers/ui
|
||||
-D HELTEC_T096
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D P_LORA_DIO_1=21
|
||||
-D P_LORA_NSS=5
|
||||
-D P_LORA_RESET=16
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
#define USE_LFXO
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
// This board has no external RTC chip -- the ICM-42607-P IMU sits at 0x68,
|
||||
// the same I2C address DS3231 probing checks, so without this the IMU gets
|
||||
// misidentified as a DS3231 and its unrelated registers get read/written
|
||||
// as if they were time/status registers, producing a bogus, unfixable clock.
|
||||
#define DISABLE_DS3231_PROBE
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Number of pins
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/heltec_t114
|
||||
-I src/helpers/ui
|
||||
-D HELTEC_T114
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D P_LORA_DIO_1=20
|
||||
-D P_LORA_NSS=24
|
||||
-D P_LORA_RESET=25
|
||||
|
||||
@@ -72,10 +72,50 @@ void HeltecTrackerV2Board::begin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
class HeltecTrackerV2Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
public:
|
||||
RefCountedDigitalPin periph_power;
|
||||
@@ -14,13 +18,13 @@ public:
|
||||
HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void powerOff() override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
const char* getManufacturerName() const override ;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
|
||||
};
|
||||
|
||||
@@ -183,6 +183,7 @@ extends = Heltec_tracker_v2
|
||||
build_flags =
|
||||
${Heltec_tracker_v2.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=ST7735Display
|
||||
@@ -196,6 +197,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter}
|
||||
+<helpers/ui/ST7735Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
|
||||
@@ -178,6 +178,7 @@ extends = Heltec_lora32_v2
|
||||
build_flags =
|
||||
${Heltec_lora32_v2.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D MAX_CONTACTS=160
|
||||
-D MAX_GROUP_CHANNELS=8
|
||||
@@ -189,6 +190,7 @@ build_flags =
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
|
||||
@@ -168,12 +168,14 @@ build_flags =
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
@@ -185,6 +187,7 @@ extends = Heltec_lora32_v3
|
||||
build_flags =
|
||||
${Heltec_lora32_v3.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
@@ -198,6 +201,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
@@ -310,12 +314,14 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
; -D BLE_DEBUG_LOGGING=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps =
|
||||
${Heltec_lora32_v3.lib_deps}
|
||||
@@ -340,6 +346,7 @@ lib_deps =
|
||||
extends = Heltec_lora32_v3
|
||||
build_flags =
|
||||
${Heltec_lora32_v3.build_flags}
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D WIFI_DEBUG_LOGGING=1
|
||||
@@ -350,6 +357,7 @@ build_flags =
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps =
|
||||
${Heltec_lora32_v3.lib_deps}
|
||||
|
||||
@@ -73,10 +73,50 @@ void HeltecV4Board::begin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecV4Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#endif
|
||||
|
||||
class HeltecV4Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
protected:
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
@@ -20,12 +24,12 @@ public:
|
||||
HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
|
||||
@@ -211,12 +211,14 @@ build_flags =
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${heltec_v4_oled.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
@@ -228,6 +230,7 @@ extends = heltec_v4_oled
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
@@ -241,6 +244,7 @@ build_src_filter = ${heltec_v4_oled.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
@@ -393,6 +397,7 @@ extends = heltec_v4_tft
|
||||
build_flags =
|
||||
${heltec_v4_tft.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
@@ -406,6 +411,7 @@ build_src_filter = ${heltec_v4_tft.build_src_filter}
|
||||
+<helpers/ui/ST7789LCDDisplay.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user