mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
Merge branch 'dev' into gps-l76k-constellation-init-m6
This commit is contained in:
@@ -123,6 +123,6 @@ There are a number of fairly major features in the pipeline, with no particular
|
|||||||
|
|
||||||
## 📞 Get Support
|
## 📞 Get Support
|
||||||
|
|
||||||
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
|
- Report bugs and request features on the [GitHub Issues](https://github.com/meshcore-dev/meshcore/issues) page.
|
||||||
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
|
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
|
||||||
- Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community.
|
- Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community.
|
||||||
|
|||||||
@@ -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/*>")
|
src_filter.append("+<helpers/stm32/*>")
|
||||||
elif item == "ESP32":
|
elif item == "ESP32":
|
||||||
src_filter.append("+<helpers/esp32/*>")
|
src_filter.append("+<helpers/esp32/*>")
|
||||||
|
src_filter.append("+<helpers/wifi/*>")
|
||||||
elif item == "NRF52_PLATFORM":
|
elif item == "NRF52_PLATFORM":
|
||||||
src_filter.append("+<helpers/nrf52/*>")
|
src_filter.append("+<helpers/nrf52/*>")
|
||||||
elif item == "RP2040_PLATFORM":
|
elif item == "RP2040_PLATFORM":
|
||||||
src_filter.append("+<helpers/rp2040/*>")
|
src_filter.append("+<helpers/rp2040/*>")
|
||||||
|
src_filter.append("+<helpers/wifi/*>")
|
||||||
|
|
||||||
# DISPLAY HANDLING
|
# DISPLAY HANDLING
|
||||||
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
|
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`
|
**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
|
**Note:** Requires reboot to apply
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
|||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `frequency`: Frequency in MHz
|
- `frequency`: Frequency in MHz
|
||||||
|
|
||||||
**Default:** `869.525`
|
**Default:** `869.618`
|
||||||
|
|
||||||
**Note:** Requires reboot to apply
|
**Note:** Requires reboot to apply
|
||||||
**Serial Only:** `set freq <frequency>`
|
**Serial Only:** `set freq <frequency>`
|
||||||
@@ -537,7 +537,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
|||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `value`: Direct transmit delay factor (0-2)
|
- `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.
|
**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>`
|
- `set agc.reset.interval <value>`
|
||||||
|
|
||||||
**Parameters:**
|
**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`
|
**Default:** `0.0`
|
||||||
|
|
||||||
@@ -654,7 +654,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
|||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `hours`: Interval in hours (3-168)
|
- `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:**
|
**Parameters:**
|
||||||
- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
|
- `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**:
|
**Command Format**:
|
||||||
```
|
```
|
||||||
Byte 0: 0x1F
|
Byte 0: 0x1F
|
||||||
Byte 1: Channel Index (0-7)
|
Byte 1: Channel Index (0 .. max_channels-1)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example** (get channel 1):
|
**Example** (get channel 1):
|
||||||
@@ -225,7 +225,7 @@ Byte 1: Channel Index (0-7)
|
|||||||
**Command Format**:
|
**Command Format**:
|
||||||
```
|
```
|
||||||
Byte 0: 0x20
|
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 2-33: Channel Name (32 bytes, UTF-8, null-padded)
|
||||||
Bytes 34-49: Secret (16 bytes)
|
Bytes 34-49: Secret (16 bytes)
|
||||||
```
|
```
|
||||||
@@ -233,8 +233,12 @@ Bytes 34-49: Secret (16 bytes)
|
|||||||
**Total Length**: 50 bytes
|
**Total Length**: 50 bytes
|
||||||
|
|
||||||
**Channel Index**:
|
**Channel Index**:
|
||||||
- Index 0: Reserved for public channels (no secret)
|
- Valid range is `0` to `max_channels - 1`. Read `max_channels` from byte 3 of
|
||||||
- Indices 1-7: Available for private channels
|
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**:
|
**Channel Name**:
|
||||||
- UTF-8 encoded
|
- UTF-8 encoded
|
||||||
@@ -242,8 +246,18 @@ Bytes 34-49: Secret (16 bytes)
|
|||||||
- Padded with null bytes (0x00) if shorter
|
- Padded with null bytes (0x00) if shorter
|
||||||
|
|
||||||
**Secret Field** (16 bytes):
|
**Secret Field** (16 bytes):
|
||||||
- For **private channels**: 16-byte secret
|
- Always a real 16-byte key; the firmware derives the channel hash from
|
||||||
- For **public channels**: All zeros (0x00)
|
`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):
|
**Example** (create channel "YourChannelName" at index 1 with secret):
|
||||||
```
|
```
|
||||||
@@ -265,7 +279,7 @@ Bytes 34-49: Secret (16 bytes)
|
|||||||
```
|
```
|
||||||
Byte 0: 0x03
|
Byte 0: 0x03
|
||||||
Byte 1: 0x00
|
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 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)
|
||||||
Bytes 7+: Message Text (UTF-8, variable length)
|
Bytes 7+: Message Text (UTF-8, variable length)
|
||||||
```
|
```
|
||||||
@@ -288,7 +302,7 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
|||||||
**Command Format**:
|
**Command Format**:
|
||||||
```
|
```
|
||||||
Byte 0: 0x3E
|
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)
|
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
|
||||||
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
|
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
|
||||||
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
|
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.
|
- Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below.
|
||||||
|
|
||||||
**Limits**:
|
**Limits**:
|
||||||
- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes.
|
- Maximum payload length is **165** bytes — `MAX_GROUP_DATA_LENGTH =
|
||||||
- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`).
|
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:
|
**Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of:
|
||||||
- `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx`
|
- `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_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
|
- `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).
|
**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 0: 0x1B (packet type)
|
||||||
Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB)
|
Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB)
|
||||||
Bytes 2-3: Reserved (clients MUST ignore)
|
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)
|
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
|
||||||
Bytes 6-7: Data Type (uint16 little-endian)
|
Bytes 6-7: Data Type (uint16 little-endian)
|
||||||
Byte 8: Data Length
|
Byte 8: Data Length
|
||||||
@@ -550,7 +574,7 @@ def parse_contact_message(data):
|
|||||||
**Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08):
|
**Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08):
|
||||||
```
|
```
|
||||||
Byte 0: 0x08 (packet type)
|
Byte 0: 0x08 (packet type)
|
||||||
Byte 1: Channel Index (0-7)
|
Byte 1: Channel Index (0 .. max_channels-1)
|
||||||
Byte 2: Path Length
|
Byte 2: Path Length
|
||||||
Byte 3: Text Type
|
Byte 3: Text Type
|
||||||
Bytes 4-7: Timestamp (32-bit little-endian)
|
Bytes 4-7: Timestamp (32-bit little-endian)
|
||||||
@@ -562,7 +586,7 @@ Bytes 8+: Message Text (UTF-8)
|
|||||||
Byte 0: 0x11 (packet type)
|
Byte 0: 0x11 (packet type)
|
||||||
Byte 1: SNR (signed byte, multiplied by 4)
|
Byte 1: SNR (signed byte, multiplied by 4)
|
||||||
Bytes 2-3: Reserved
|
Bytes 2-3: Reserved
|
||||||
Byte 4: Channel Index (0-7)
|
Byte 4: Channel Index (0 .. max_channels-1)
|
||||||
Byte 5: Path Length
|
Byte 5: Path Length
|
||||||
Byte 6: Text Type
|
Byte 6: Text Type
|
||||||
Bytes 7-10: Timestamp (32-bit little-endian)
|
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:
|
Then it should be the same for all platforms:
|
||||||
```
|
```
|
||||||
python3 -m venv meshcore
|
python3 -m venv meshcore-venv
|
||||||
cd meshcore && source bin/activate
|
cd meshcore-venv && source bin/activate
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
git clone https://github.com/ripplebiz/MeshCore.git
|
git clone https://github.com/meshcore-dev/meshcore.git
|
||||||
cd MeshCore
|
cd meshcore
|
||||||
```
|
```
|
||||||
open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5`
|
open platformio.ini and in `[arduino_base]` edit `LORA_FREQ` (it defaults to
|
||||||
save, then run:
|
`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?
|
### 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>.
|
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.
|
- 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:
|
- 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.
|
- Non-merged bin keeps the existing Bluetooth pairing database.
|
||||||
- `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin`
|
- `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.
|
- Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations.
|
||||||
|
|||||||
@@ -186,11 +186,19 @@ Power management status can be queried via the CLI:
|
|||||||
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
|
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
|
||||||
| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
|
| `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
|
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
|
## Debug Output
|
||||||
|
|
||||||
When `MESH_DEBUG=1` is enabled, the power management module outputs:
|
When `MESH_DEBUG=1` is enabled, the power management module outputs:
|
||||||
|
|||||||
+46
-9
@@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the
|
|||||||
| Field | Size (bytes) | Description |
|
| Field | Size (bytes) | Description |
|
||||||
|--------------|-----------------|------------------------------------------|
|
|--------------|-----------------|------------------------------------------|
|
||||||
| timestamp | 4 | sender time (unix timestamp) |
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | request sub type |
|
||||||
| request data | rest of payload | application-defined request payload body |
|
| request data | rest of payload | application-defined request payload body |
|
||||||
|
|
||||||
For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
|
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 |
|
| Value | Name | Description |
|
||||||
|--------|-----------|----------------------------------------------------|
|
|--------|-----------|----------------------------------------------------|
|
||||||
| `0x01` | get stats | get stats of repeater or room server |
|
| `0x01` | get stats | get stats of repeater or room server |
|
||||||
| `0x02` | keepalive | keep-alive request used for maintained connections |
|
| `0x02` | keepalive | (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
|
#### Get stats
|
||||||
|
|
||||||
@@ -125,21 +132,50 @@ Gets information about the node, possibly including the following:
|
|||||||
* Number posted (?)
|
* Number posted (?)
|
||||||
* Number of post pushes (?)
|
* 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
|
#### 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)
|
#### 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
|
#### 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
|
#### Get Neighbors
|
||||||
|
|
||||||
@@ -171,8 +207,9 @@ txt_type
|
|||||||
| Value | Description | Message content |
|
| Value | Description | Message content |
|
||||||
|--------|---------------------------|--------------------------------------------------------------------------|
|
|--------|---------------------------|--------------------------------------------------------------------------|
|
||||||
| `0x00` | plain text message | the plain text of the message |
|
| `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 |
|
| `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
|
## Anonymous request
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,17 @@ set lon {longitude}
|
|||||||
```
|
```
|
||||||
Sets your advertisement map longitude. (decimal degrees)
|
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}
|
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}
|
public {text}
|
||||||
```
|
```
|
||||||
Sends the text message to the built-in 'public' group channel
|
Sends the text message to the built-in 'public' group channel
|
||||||
|
|
||||||
|
```
|
||||||
|
help
|
||||||
|
```
|
||||||
|
Lists the available commands.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "NodePrefs.h"
|
#include "NodePrefs.h"
|
||||||
|
#include "MyMesh.h"
|
||||||
|
|
||||||
enum class UIEventType {
|
enum class UIEventType {
|
||||||
none,
|
none,
|
||||||
@@ -22,25 +23,21 @@ enum class UIEventType {
|
|||||||
ack
|
ack
|
||||||
};
|
};
|
||||||
|
|
||||||
class AbstractUITask {
|
class AbstractUITask : public MyMesh::Listener {
|
||||||
protected:
|
protected:
|
||||||
mesh::MainBoard* _board;
|
mesh::MainBoard* _board;
|
||||||
MultiSerialInterface* _interfaceManager;
|
MultiSerialInterface* _interfaceManager;
|
||||||
bool _connected;
|
|
||||||
|
|
||||||
AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) {
|
AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) {
|
||||||
_connected = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void setHasConnection(bool connected) { _connected = connected; }
|
bool hasConnection() const { return _interfaceManager->isConnected(); }
|
||||||
bool hasConnection() const { return _connected; }
|
|
||||||
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
|
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
|
||||||
bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); }
|
bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); }
|
||||||
void enableBluetooth() { _interfaceManager->enableBluetooth(); }
|
void enableBluetooth() { _interfaceManager->enableBluetooth(); }
|
||||||
void disableBluetooth() { _interfaceManager->disableBluetooth(); }
|
void disableBluetooth() { _interfaceManager->disableBluetooth(); }
|
||||||
virtual void msgRead(int msgcount) = 0;
|
|
||||||
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
|
|
||||||
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
||||||
virtual void loop() = 0;
|
virtual void loop() = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#include "MyMesh.h"
|
#include "MyMesh.h"
|
||||||
|
|
||||||
#include <Arduino.h> // needed for PlatformIO
|
#include <Arduino.h> // needed for PlatformIO
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
#include <WiFi.h>
|
||||||
|
#endif
|
||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
|
|
||||||
#define CMD_APP_START 1
|
#define CMD_APP_START 1
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
|
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
|
||||||
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
|
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
|
||||||
#define CMD_SEND_RAW_PACKET 65
|
#define CMD_SEND_RAW_PACKET 65
|
||||||
|
#define CMD_RUN_CLI_COMMAND 66 // v14+
|
||||||
|
|
||||||
// Stats sub-types for CMD_GET_STATS
|
// Stats sub-types for CMD_GET_STATS
|
||||||
#define STATS_TYPE_CORE 0
|
#define STATS_TYPE_CORE 0
|
||||||
@@ -97,6 +101,7 @@
|
|||||||
#define RESP_ALLOWED_REPEAT_FREQ 26
|
#define RESP_ALLOWED_REPEAT_FREQ 26
|
||||||
#define RESP_CODE_CHANNEL_DATA_RECV 27
|
#define RESP_CODE_CHANNEL_DATA_RECV 27
|
||||||
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
|
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
|
||||||
|
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND
|
||||||
|
|
||||||
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
|
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
|
||||||
|
|
||||||
@@ -259,10 +264,10 @@ float MyMesh::getAirtimeBudgetFactor() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int MyMesh::getInterferenceThreshold() const {
|
int MyMesh::getInterferenceThreshold() const {
|
||||||
return 0; // disabled for now, until currentRSSI() problem is resolved
|
return _prefs.interference_threshold;
|
||||||
}
|
}
|
||||||
bool MyMesh::getCADEnabled() const {
|
bool MyMesh::getCADEnabled() const {
|
||||||
return false; // hardware CAD before TX (disabled by default, until configurable)
|
return _prefs.cad_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||||
@@ -271,11 +276,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
||||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||||
return getRNG()->nextInt(0, 5*t + 1);
|
return getRNG()->nextInt(0, 5*t + 1);
|
||||||
}
|
}
|
||||||
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
||||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||||
return getRNG()->nextInt(0, 5*t + 1);
|
return getRNG()->nextInt(0, 5*t + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,10 +364,6 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
|
|||||||
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
|
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
|
||||||
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
|
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
if (_ui) _ui->notify(UIEventType::newContactMessage);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add inbound-path to mem cache
|
// add inbound-path to mem cache
|
||||||
@@ -386,6 +387,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
|
|||||||
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
|
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_listener) _listener->onDiscoveredContact(contact, is_new, path_len, path);
|
||||||
|
|
||||||
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
|
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +415,11 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContactInfo* MyMesh::processAck(const uint8_t *data) {
|
ContactInfo* MyMesh::processAck(const uint8_t *data) {
|
||||||
|
if (_listener) {
|
||||||
|
uint32_t ack_crc;
|
||||||
|
memcpy(&ack_crc, data, 4);
|
||||||
|
_listener->onACKRecv(ack_crc);
|
||||||
|
}
|
||||||
// see if matches any in a table
|
// see if matches any in a table
|
||||||
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
|
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
|
||||||
if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient
|
if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient
|
||||||
@@ -464,16 +472,10 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
|
|||||||
_serial->writeFrame(frame, 1);
|
_serial->writeFrame(frame, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DISPLAY_CLASS
|
if (_listener) {
|
||||||
// we only want to show text messages on display, not cli data
|
_listener->onMessageRecv(pkt, from, txt_type, sender_timestamp, text);
|
||||||
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
|
_listener->onQueueSizeChanged(offline_queue_len);
|
||||||
if (should_display && _ui) {
|
|
||||||
_ui->newMsg(path_len, from.name, text, offline_queue_len);
|
|
||||||
if (!_serial->isConnected()) {
|
|
||||||
_ui->notify(UIEventType::contactMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
|
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
|
||||||
@@ -528,12 +530,23 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
|||||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) {
|
||||||
const char *text) {
|
|
||||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
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);
|
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||||
const uint8_t *sender_prefix, const char *text) {
|
const uint8_t *sender_prefix, const char *text) {
|
||||||
markConnectionActive(from);
|
markConnectionActive(from);
|
||||||
@@ -573,20 +586,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
|||||||
uint8_t frame[1];
|
uint8_t frame[1];
|
||||||
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
|
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
|
||||||
_serial->writeFrame(frame, 1);
|
_serial->writeFrame(frame, 1);
|
||||||
} else {
|
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
if (_ui) _ui->notify(UIEventType::channelMessage);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#ifdef DISPLAY_CLASS
|
if (_listener) {
|
||||||
// Get the channel name from the channel index
|
// Get the channel name from the channel index
|
||||||
const char *channel_name = "Unknown";
|
ChannelDetails channel_details;
|
||||||
ChannelDetails channel_details;
|
if (!getChannel(channel_idx, channel_details)) {
|
||||||
if (getChannel(channel_idx, channel_details)) {
|
strcpy(channel_details.name, "Unknown");
|
||||||
channel_name = channel_details.name;
|
}
|
||||||
|
_listener->onChannelMessageRecv(pkt, channel_details, text);
|
||||||
|
_listener->onQueueSizeChanged(offline_queue_len);
|
||||||
}
|
}
|
||||||
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
|
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
|
||||||
@@ -622,6 +631,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *
|
|||||||
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
|
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
|
||||||
_serial->writeFrame(frame, 1);
|
_serial->writeFrame(frame, 1);
|
||||||
}
|
}
|
||||||
|
if (_listener) {
|
||||||
|
_listener->onChannelDataRecv(pkt, channel, data_type, data, data_len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
|
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
|
||||||
@@ -669,6 +681,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
|
|||||||
memcpy(&reply[4], telemetry.getBuffer(), tlen);
|
memcpy(&reply[4], telemetry.getBuffer(), tlen);
|
||||||
return 4 + tlen;
|
return 4 + tlen;
|
||||||
}
|
}
|
||||||
|
} else if (_listener) {
|
||||||
|
return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply);
|
||||||
}
|
}
|
||||||
return 0; // unknown
|
return 0; // unknown
|
||||||
}
|
}
|
||||||
@@ -744,6 +758,21 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
|||||||
memcpy(&out_frame[i], &data[4], len - 4);
|
memcpy(&out_frame[i], &data[4], len - 4);
|
||||||
i += (len - 4);
|
i += (len - 4);
|
||||||
_serial->writeFrame(out_frame, i);
|
_serial->writeFrame(out_frame, i);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,6 +825,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
|
|||||||
} else {
|
} else {
|
||||||
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
|
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_listener) _listener->onControlDataRecv(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||||
@@ -816,6 +847,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
|||||||
} else {
|
} else {
|
||||||
MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline");
|
MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline");
|
||||||
}
|
}
|
||||||
|
if (_listener) {
|
||||||
|
_listener->onRawDataRecv(packet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
|
void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
|
||||||
@@ -846,6 +880,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code,
|
|||||||
} else {
|
} else {
|
||||||
MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline");
|
MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline");
|
||||||
}
|
}
|
||||||
|
if (_listener) {
|
||||||
|
_listener->onTraceRecv(packet, tag, auth_code, flags, path_snrs, path_hashes, path_len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
|
uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
|
||||||
@@ -860,11 +897,12 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t
|
|||||||
|
|
||||||
void MyMesh::onSendTimeout() {}
|
void MyMesh::onSendTimeout() {}
|
||||||
|
|
||||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
|
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
|
||||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
|
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
|
||||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
|
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) {
|
||||||
_iter_started = false;
|
_iter_started = false;
|
||||||
_cli_rescue = false;
|
_cli_rescue = false;
|
||||||
|
cli_command[0] = 0;
|
||||||
offline_queue_len = 0;
|
offline_queue_len = 0;
|
||||||
app_target_ver = 0;
|
app_target_ver = 0;
|
||||||
clearPendingReqs();
|
clearPendingReqs();
|
||||||
@@ -877,6 +915,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
_prefs.airtime_factor = 1.0;
|
_prefs.airtime_factor = 1.0;
|
||||||
|
_prefs.tx_delay_factor = 0.5f;
|
||||||
|
_prefs.direct_tx_delay_factor = 0.2f;
|
||||||
strcpy(_prefs.node_name, "NONAME");
|
strcpy(_prefs.node_name, "NONAME");
|
||||||
_prefs.freq = LORA_FREQ;
|
_prefs.freq = LORA_FREQ;
|
||||||
_prefs.sf = LORA_SF;
|
_prefs.sf = LORA_SF;
|
||||||
@@ -886,7 +926,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||||
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
||||||
_prefs.radio_fem_rxgain = 1;
|
_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;
|
_prefs.radio_fem_txgain = 0;
|
||||||
|
#endif
|
||||||
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
|
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
|
||||||
_prefs.setRepeatEn(false);
|
_prefs.setRepeatEn(false);
|
||||||
#if defined(USE_SX1262) || defined(USE_SX1268)
|
#if defined(USE_SX1262) || defined(USE_SX1268)
|
||||||
@@ -898,7 +942,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::begin(bool has_display) {
|
void MyMesh::begin() {
|
||||||
BaseChatMesh::begin();
|
BaseChatMesh::begin();
|
||||||
|
|
||||||
if (!_store->loadMainIdentity(self_id)) {
|
if (!_store->loadMainIdentity(self_id)) {
|
||||||
@@ -939,6 +983,8 @@ void MyMesh::begin(bool has_display) {
|
|||||||
|
|
||||||
// sanitise bad pref values
|
// sanitise bad pref values
|
||||||
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
||||||
|
_prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f);
|
||||||
|
_prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f);
|
||||||
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
|
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
|
||||||
_prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f);
|
_prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f);
|
||||||
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
|
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
|
||||||
@@ -948,36 +994,20 @@ void MyMesh::begin(bool has_display) {
|
|||||||
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
|
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
|
||||||
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
|
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
|
||||||
|
|
||||||
#ifdef BLE_PIN_CODE // 123456 by default
|
_active_ble_pin = _prefs.ble_pin;
|
||||||
if (_prefs.ble_pin == 0) {
|
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
if (has_display && BLE_PIN_CODE == 123456) {
|
|
||||||
StdRNG rng;
|
|
||||||
_active_ble_pin = rng.nextInt(100000, 999999); // random pin each session
|
|
||||||
} else {
|
|
||||||
_active_ble_pin = BLE_PIN_CODE; // otherwise static pin
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
_active_ble_pin = BLE_PIN_CODE; // otherwise static pin
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
_active_ble_pin = _prefs.ble_pin;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
_active_ble_pin = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
resetContacts();
|
resetContacts();
|
||||||
_store->loadContacts(this);
|
_store->loadContacts(this);
|
||||||
bootstrapRTCfromContacts();
|
bootstrapRTCfromContacts();
|
||||||
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
|
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure public channel
|
||||||
_store->loadChannels(this);
|
_store->loadChannels(this);
|
||||||
|
|
||||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
|
||||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
board.attachDynamicPrefs(_prefs.getCustom());
|
||||||
|
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
@@ -991,6 +1021,9 @@ NodePrefs *MyMesh::getNodePrefs() {
|
|||||||
uint32_t MyMesh::getBLEPin() {
|
uint32_t MyMesh::getBLEPin() {
|
||||||
return _active_ble_pin;
|
return _active_ble_pin;
|
||||||
}
|
}
|
||||||
|
void MyMesh::setBLEPin(uint32_t active_pin) {
|
||||||
|
_active_ble_pin = active_pin;
|
||||||
|
}
|
||||||
|
|
||||||
struct FreqRange {
|
struct FreqRange {
|
||||||
uint32_t lower_freq, upper_freq;
|
uint32_t lower_freq, upper_freq;
|
||||||
@@ -1082,6 +1115,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
memcpy(&out_frame[i], _prefs.node_name, tlen);
|
memcpy(&out_frame[i], _prefs.node_name, tlen);
|
||||||
i += tlen;
|
i += tlen;
|
||||||
_serial->writeFrame(out_frame, i);
|
_serial->writeFrame(out_frame, i);
|
||||||
|
} else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+
|
||||||
|
int i = 1;
|
||||||
|
char *text = (char *)&cmd_frame[i];
|
||||||
|
int tlen = len - i;
|
||||||
|
text[tlen] = 0; // ensure null
|
||||||
|
|
||||||
|
reply_buf[0] = 0;
|
||||||
|
if (!handleCommand(text, 0, reply_buf)) {
|
||||||
|
strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text'
|
||||||
|
}
|
||||||
|
out_frame[0] = RESP_CODE_CLI_REPLY;
|
||||||
|
int rlen = strlen(reply_buf);
|
||||||
|
memcpy(&out_frame[1], reply_buf, rlen);
|
||||||
|
_serial->writeFrame(out_frame, 1 + rlen);
|
||||||
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
|
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
|
||||||
int i = 1;
|
int i = 1;
|
||||||
uint8_t txt_type = cmd_frame[i++];
|
uint8_t txt_type = cmd_frame[i++];
|
||||||
@@ -1092,16 +1139,16 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
uint8_t *pub_key_prefix = &cmd_frame[i];
|
uint8_t *pub_key_prefix = &cmd_frame[i];
|
||||||
i += 6;
|
i += 6;
|
||||||
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 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];
|
char *text = (char *)&cmd_frame[i];
|
||||||
int tlen = len - i;
|
int tlen = len - i;
|
||||||
uint32_t est_timeout;
|
uint32_t est_timeout;
|
||||||
text[tlen] = 0; // ensure null
|
text[tlen] = 0; // ensure null
|
||||||
int result;
|
int result;
|
||||||
uint32_t expected_ack;
|
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
|
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
|
expected_ack = 0; // no Ack expected
|
||||||
} else {
|
} else {
|
||||||
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
|
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
|
||||||
@@ -1180,8 +1227,8 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
|
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
|
||||||
} else if (data_type == DATA_TYPE_RESERVED) {
|
} else if (data_type == DATA_TYPE_RESERVED) {
|
||||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||||
} else if (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_CHANNEL_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);
|
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||||
} else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) {
|
} else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) {
|
||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
@@ -1369,9 +1416,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
int out_len;
|
int out_len;
|
||||||
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
|
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
|
||||||
_serial->writeFrame(out_frame, out_len);
|
_serial->writeFrame(out_frame, out_len);
|
||||||
#ifdef DISPLAY_CLASS
|
if (_listener) _listener->onQueueSizeChanged(offline_queue_len);
|
||||||
if (_ui) _ui->msgRead(offline_queue_len);
|
|
||||||
#endif
|
|
||||||
} else {
|
} else {
|
||||||
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
|
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
|
||||||
_serial->writeFrame(out_frame, 1);
|
_serial->writeFrame(out_frame, 1);
|
||||||
@@ -1511,16 +1556,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
#endif
|
#endif
|
||||||
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
|
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
|
||||||
int i = 1;
|
int i = 1;
|
||||||
int8_t path_len = cmd_frame[i++];
|
uint8_t path_len = cmd_frame[i++];
|
||||||
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
|
if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) {
|
||||||
uint8_t *path = &cmd_frame[i];
|
uint8_t path[MAX_PATH_SIZE];
|
||||||
i += path_len;
|
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
|
||||||
auto pkt = createRawData(&cmd_frame[i], len - i);
|
if (i + 4 > len) { // min payload 4 bytes
|
||||||
if (pkt) {
|
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||||
sendDirect(pkt, path, path_len);
|
|
||||||
writeOKFrame();
|
|
||||||
} else {
|
} else {
|
||||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
auto pkt = createRawData(&cmd_frame[i], len - i);
|
||||||
|
if (pkt) {
|
||||||
|
sendDirect(pkt, path, path_len);
|
||||||
|
writeOKFrame();
|
||||||
|
} else {
|
||||||
|
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
|
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
|
||||||
@@ -2027,8 +2076,149 @@ void MyMesh::enterCLIRescue() {
|
|||||||
Serial.println("========= CLI Rescue =========");
|
Serial.println("========= CLI Rescue =========");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||||
|
while (*command == ' ') command++; // skip leading spaces
|
||||||
|
|
||||||
|
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||||
|
memcpy(reply, command, 3); // reflect the prefix back
|
||||||
|
reply += 3;
|
||||||
|
*reply = 0;
|
||||||
|
command += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_prefs.getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is radio CLI command?
|
||||||
|
if (_prefs.getRadioPrefs()->isDirty()) { savePrefs(); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hook for variant-specific CLI processing
|
||||||
|
if (board.handleCommand(command, sender_timestamp, reply)) {
|
||||||
|
if (_prefs.isDirty()) { savePrefs(); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(command, "reboot") == 0) {
|
||||||
|
board.reboot(); // doesn't return
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
||||||
|
board.powerOff(); // doesn't return
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(command, "set name ", 9) == 0) {
|
||||||
|
if (AdvertDataParser::isValidName(&command[9])) {
|
||||||
|
StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name));
|
||||||
|
savePrefs();
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "Error, bad chars");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get name") == 0) {
|
||||||
|
sprintf(reply, "> %s", _prefs.node_name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(command, "set pin ", 8) == 0) {
|
||||||
|
_prefs.ble_pin = atoi(&command[8]);
|
||||||
|
savePrefs();
|
||||||
|
sprintf(reply, "> pin is now %06d", _prefs.ble_pin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
if (memcmp(command, "set wifi.ssid ", 14) == 0) {
|
||||||
|
StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid));
|
||||||
|
savePrefs();
|
||||||
|
sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (memcmp(command, "set wifi.pwd ", 13) == 0) {
|
||||||
|
StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd));
|
||||||
|
savePrefs();
|
||||||
|
strcpy(reply, "> wifi.pwd updated (reboot to apply)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get wifi.pwd") == 0) {
|
||||||
|
sprintf(reply, "> %s", _prefs.wifi_pwd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "set wifi.clear") == 0) {
|
||||||
|
_prefs.wifi_ssid[0] = 0;
|
||||||
|
_prefs.wifi_pwd[0] = 0;
|
||||||
|
savePrefs();
|
||||||
|
strcpy(reply, "> wifi config cleared (reboot to apply)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get wifi.ssid") == 0) {
|
||||||
|
sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (memcmp(command, "set wifi.enabled ", 17) == 0) {
|
||||||
|
_prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0;
|
||||||
|
savePrefs();
|
||||||
|
sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get wifi.enabled") == 0) {
|
||||||
|
sprintf(reply, "> %d", _prefs.wifi_enabled);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get wifi.status") == 0) {
|
||||||
|
strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get wifi.ip") == 0) {
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
sprintf(reply, "> %s", WiFi.localIP().toString().c_str());
|
||||||
|
} else {
|
||||||
|
strcpy(reply, "> (not connected)");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (strcmp(command, "board") == 0) {
|
||||||
|
strcpy(reply, board.getManufacturerName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(command, "ver") == 0) {
|
||||||
|
sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(command, "get tz.offset") == 0) {
|
||||||
|
sprintf(reply, "> %d", _prefs.tz_offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (memcmp(command, "set tz.offset ", 14) == 0) {
|
||||||
|
int8_t tz = atof(&command[14]);
|
||||||
|
if (tz < -12 || tz > 14) {
|
||||||
|
strcpy(reply, "Error, must be from -12 to +14");
|
||||||
|
} else {
|
||||||
|
_prefs.tz_offset = tz;
|
||||||
|
savePrefs();
|
||||||
|
strcpy(reply, "OK");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // not handled
|
||||||
|
}
|
||||||
|
|
||||||
void MyMesh::checkCLIRescueCmd() {
|
void MyMesh::checkCLIRescueCmd() {
|
||||||
int len = strlen(cli_command);
|
int len = strlen(cli_command);
|
||||||
|
// `cli_command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||||
|
// strlen() above can return >= sizeof(cli_command) and the loop below would
|
||||||
|
// then index past the buffer, so clamp defensively.
|
||||||
|
if (len >= (int)sizeof(cli_command)) {
|
||||||
|
cli_command[0] = 0;
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
while (Serial.available() && len < sizeof(cli_command)-1) {
|
while (Serial.available() && len < sizeof(cli_command)-1) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c != '\n') {
|
if (c != '\n') {
|
||||||
@@ -2037,22 +2227,18 @@ void MyMesh::checkCLIRescueCmd() {
|
|||||||
}
|
}
|
||||||
Serial.print(c); // echo
|
Serial.print(c); // echo
|
||||||
}
|
}
|
||||||
if (len == sizeof(cli_command)-1) { // command buffer full
|
if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line
|
||||||
cli_command[sizeof(cli_command)-1] = '\r';
|
cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||||
|
cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
||||||
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
||||||
|
|
||||||
if (memcmp(cli_command, "set ", 4) == 0) {
|
reply_buf[0] = 0;
|
||||||
const char* config = &cli_command[4];
|
if (handleCommand(cli_command, 0, reply_buf)) {
|
||||||
if (memcmp(config, "pin ", 4) == 0) {
|
// command was handled, print reply output
|
||||||
_prefs.ble_pin = atoi(&config[4]);
|
Serial.print(" "); Serial.print(reply_buf); Serial.println();
|
||||||
savePrefs();
|
|
||||||
Serial.printf(" > pin is now %06d\n", _prefs.ble_pin);
|
|
||||||
} else {
|
|
||||||
Serial.printf(" Error: unknown config: %s\n", config);
|
|
||||||
}
|
|
||||||
} else if (strcmp(cli_command, "rebuild") == 0) {
|
} else if (strcmp(cli_command, "rebuild") == 0) {
|
||||||
bool success = _store->formatFileSystem();
|
bool success = _store->formatFileSystem();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -2233,6 +2419,11 @@ void MyMesh::loop() {
|
|||||||
checkCLIRescueCmd();
|
checkCLIRescueCmd();
|
||||||
} else {
|
} else {
|
||||||
checkSerialInterface();
|
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?
|
// is there are pending dirty contacts write needed?
|
||||||
@@ -2240,10 +2431,6 @@ void MyMesh::loop() {
|
|||||||
saveContacts();
|
saveContacts();
|
||||||
dirty_contacts_expiry = 0;
|
dirty_contacts_expiry = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
if (_ui) _ui->setHasConnection(_serial->isConnected());
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MyMesh::advert() {
|
bool MyMesh::advert() {
|
||||||
|
|||||||
@@ -2,17 +2,16 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
#include "AbstractUITask.h"
|
|
||||||
|
|
||||||
/*------------ Frame Protocol --------------*/
|
/*------------ Frame Protocol --------------*/
|
||||||
#define FIRMWARE_VER_CODE 13
|
#define FIRMWARE_VER_CODE 14
|
||||||
|
|
||||||
#ifndef FIRMWARE_BUILD_DATE
|
#ifndef FIRMWARE_BUILD_DATE
|
||||||
#define FIRMWARE_BUILD_DATE "9 Aug 2026"
|
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef FIRMWARE_VERSION
|
#ifndef FIRMWARE_VERSION
|
||||||
#define FIRMWARE_VERSION "v1.17.0"
|
#define FIRMWARE_VERSION "v1.17.1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||||
@@ -76,6 +75,10 @@
|
|||||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
#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 {
|
struct AdvertPath {
|
||||||
uint8_t pubkey_prefix[7];
|
uint8_t pubkey_prefix[7];
|
||||||
uint8_t path_len;
|
uint8_t path_len;
|
||||||
@@ -86,14 +89,35 @@ struct AdvertPath {
|
|||||||
|
|
||||||
class MyMesh : public BaseChatMesh, public DataStoreHost {
|
class MyMesh : public BaseChatMesh, public DataStoreHost {
|
||||||
public:
|
public:
|
||||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
|
class Listener {
|
||||||
|
public:
|
||||||
|
virtual void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) = 0;
|
||||||
|
virtual void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) = 0;
|
||||||
|
virtual void onQueueSizeChanged(int msgcount) = 0;
|
||||||
|
virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { }
|
||||||
|
virtual void onControlDataRecv(const mesh::Packet* pkt) { }
|
||||||
|
virtual void onChannelDataRecv(mesh::Packet *pkt, const mesh::GroupChannel &channel, uint16_t data_type,
|
||||||
|
const uint8_t *data, size_t data_len) { }
|
||||||
|
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 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) { }
|
||||||
|
virtual ~Listener() { }
|
||||||
|
};
|
||||||
|
|
||||||
void begin(bool has_display);
|
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store);
|
||||||
|
|
||||||
|
void begin();
|
||||||
void startInterface(BaseSerialInterface &serial);
|
void startInterface(BaseSerialInterface &serial);
|
||||||
|
void setListener(Listener* listener) { _listener = listener; }
|
||||||
|
|
||||||
const char *getNodeName();
|
const char *getNodeName();
|
||||||
NodePrefs *getNodePrefs();
|
NodePrefs *getNodePrefs();
|
||||||
uint32_t getBLEPin();
|
uint32_t getBLEPin();
|
||||||
|
void setBLEPin(uint32_t active_pin);
|
||||||
|
|
||||||
void loop();
|
void loop();
|
||||||
void handleCmdFrame(size_t len);
|
void handleCmdFrame(size_t len);
|
||||||
@@ -106,6 +130,9 @@ protected:
|
|||||||
float getAirtimeBudgetFactor() const override;
|
float getAirtimeBudgetFactor() const override;
|
||||||
int getInterferenceThreshold() const override;
|
int getInterferenceThreshold() const override;
|
||||||
bool getCADEnabled() const override;
|
bool getCADEnabled() const override;
|
||||||
|
int getAGCResetInterval() const override {
|
||||||
|
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||||
|
}
|
||||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||||
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
||||||
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
||||||
@@ -135,6 +162,8 @@ protected:
|
|||||||
const char *text) override;
|
const char *text) override;
|
||||||
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||||
const char *text) override;
|
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,
|
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||||
const uint8_t *sender_prefix, const char *text) override;
|
const uint8_t *sender_prefix, const char *text) override;
|
||||||
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
||||||
@@ -169,6 +198,7 @@ public:
|
|||||||
_prefs.node_lat = sensors.node_lat;
|
_prefs.node_lat = sensors.node_lat;
|
||||||
_prefs.node_lon = sensors.node_lon;
|
_prefs.node_lon = sensors.node_lon;
|
||||||
_store->savePrefs(_prefs);
|
_store->savePrefs(_prefs);
|
||||||
|
_prefs.clearDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if ENV_INCLUDE_GPS == 1
|
#if ENV_INCLUDE_GPS == 1
|
||||||
@@ -201,6 +231,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkCLIRescueCmd();
|
void checkCLIRescueCmd();
|
||||||
|
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
|
||||||
void checkSerialInterface();
|
void checkSerialInterface();
|
||||||
bool isValidClientRepeatFreq(uint32_t f) const;
|
bool isValidClientRepeatFreq(uint32_t f) const;
|
||||||
|
|
||||||
@@ -215,7 +246,7 @@ private:
|
|||||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||||
uint32_t pending_req; // pending _BINARY_REQ
|
uint32_t pending_req; // pending _BINARY_REQ
|
||||||
BaseSerialInterface *_serial;
|
BaseSerialInterface *_serial;
|
||||||
AbstractUITask* _ui;
|
Listener* _listener;
|
||||||
|
|
||||||
ContactsIterator _iter;
|
ContactsIterator _iter;
|
||||||
uint32_t _iter_filter_since;
|
uint32_t _iter_filter_since;
|
||||||
@@ -225,6 +256,7 @@ private:
|
|||||||
bool _cli_rescue;
|
bool _cli_rescue;
|
||||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||||
char cli_command[80];
|
char cli_command[80];
|
||||||
|
char reply_buf[166];
|
||||||
uint8_t app_target_ver;
|
uint8_t app_target_ver;
|
||||||
uint8_t *sign_data;
|
uint8_t *sign_data;
|
||||||
uint32_t sign_data_len;
|
uint32_t sign_data_len;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint> // For uint8_t, uint32_t
|
#include <cstdint> // For uint8_t, uint32_t
|
||||||
#include <helpers/ConfigSerializer.h>
|
#include <helpers/ConfigSerializer.h>
|
||||||
|
#include <helpers/CommonRadioPrefs.h>
|
||||||
|
#include <helpers/DynamicConfigSerializer.h>
|
||||||
|
|
||||||
#define TELEM_MODE_DENY 0
|
#define TELEM_MODE_DENY 0
|
||||||
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
|
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
|
||||||
@@ -25,6 +27,8 @@ public:
|
|||||||
uint8_t telemetry_mode_loc = 0;
|
uint8_t telemetry_mode_loc = 0;
|
||||||
uint8_t telemetry_mode_env = 0;
|
uint8_t telemetry_mode_env = 0;
|
||||||
float rx_delay_base = 0;
|
float rx_delay_base = 0;
|
||||||
|
float tx_delay_factor = 0;
|
||||||
|
float direct_tx_delay_factor = 0;
|
||||||
uint32_t ble_pin = 0;
|
uint32_t ble_pin = 0;
|
||||||
uint8_t advert_loc_policy = 0;
|
uint8_t advert_loc_policy = 0;
|
||||||
uint8_t buzzer_quiet = 0;
|
uint8_t buzzer_quiet = 0;
|
||||||
@@ -38,11 +42,25 @@ public:
|
|||||||
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
|
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
|
||||||
uint8_t path_hash_mode = 0; // which path mode to use when sending
|
uint8_t path_hash_mode = 0; // which path mode to use when sending
|
||||||
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
||||||
|
uint8_t cad_enabled = 0;
|
||||||
|
uint8_t interference_threshold = 0;
|
||||||
|
uint8_t agc_reset_interval = 0; // secs / 4
|
||||||
char default_scope_name[31];
|
char default_scope_name[31];
|
||||||
uint8_t default_scope_key[16];
|
uint8_t default_scope_key[16];
|
||||||
|
int8_t tz_offset = 0;
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
#ifndef WIFI_SSID
|
||||||
|
#define WIFI_SSID ""
|
||||||
|
#endif
|
||||||
|
char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used
|
||||||
|
char wifi_pwd[64] = {0};
|
||||||
|
uint8_t wifi_enabled = 1; // enabled by default to allow wifi only builds to work. wifi won't be started if ssid is empty
|
||||||
|
// use ssid from prefs, or fallback to ssid from build flags
|
||||||
|
const char* getWifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; }
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
|
class RadioPrefs : public CommonRadioPrefs {
|
||||||
NodePrefs* _parent;
|
NodePrefs* _parent;
|
||||||
protected:
|
protected:
|
||||||
void structure() override {
|
void structure() override {
|
||||||
@@ -50,22 +68,58 @@ private:
|
|||||||
def("bw", _parent->bw);
|
def("bw", _parent->bw);
|
||||||
def("sf", _parent->sf);
|
def("sf", _parent->sf);
|
||||||
def("cr", _parent->cr);
|
def("cr", _parent->cr);
|
||||||
//def("cad", _parent->cad_enabled);
|
def("cad", _parent->cad_enabled);
|
||||||
//def("int_thr", _parent->interference_threshold);
|
def("int_thr", _parent->interference_threshold);
|
||||||
def("rxgain", _parent->rx_boosted_gain);
|
def("rxgain", _parent->rx_boosted_gain);
|
||||||
def("fem_rxgain", _parent->radio_fem_rxgain);
|
def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously
|
||||||
def("fem_txgain", _parent->radio_fem_txgain);
|
def("fem_txgain", _parent->radio_fem_txgain);
|
||||||
def("tx", _parent->tx_power_dbm);
|
def("tx", _parent->tx_power_dbm);
|
||||||
def("af", _parent->airtime_factor);
|
def("af", _parent->airtime_factor);
|
||||||
def("rxdelay", _parent->rx_delay_base);
|
def("rxdelay", _parent->rx_delay_base);
|
||||||
//def("f_txdelay", _parent->tx_delay_factor); currently hard-coded
|
def("f_txdelay", _parent->tx_delay_factor);
|
||||||
//def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded
|
def("d_txdelay", _parent->direct_tx_delay_factor);
|
||||||
//def("agc_int", _parent->agc_reset_interval);
|
def("agc_int", _parent->agc_reset_interval);
|
||||||
def("hash_mode", _parent->path_hash_mode);
|
def("hash_mode", _parent->path_hash_mode);
|
||||||
def("multi_ack", _parent->multi_acks);
|
def("multi_ack", _parent->multi_acks);
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
|
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;
|
RadioPrefs radio;
|
||||||
|
|
||||||
@@ -111,12 +165,30 @@ private:
|
|||||||
def("tel_base", _parent->telemetry_mode_base);
|
def("tel_base", _parent->telemetry_mode_base);
|
||||||
def("tel_loc", _parent->telemetry_mode_loc);
|
def("tel_loc", _parent->telemetry_mode_loc);
|
||||||
def("tel_env", _parent->telemetry_mode_env);
|
def("tel_env", _parent->telemetry_mode_env);
|
||||||
|
def("tz_offset", _parent->tz_offset);
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
CompanionPrefs(NodePrefs* parent) : _parent(parent) { }
|
CompanionPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||||
};
|
};
|
||||||
CompanionPrefs companion;
|
CompanionPrefs companion;
|
||||||
|
|
||||||
|
DynamicConfigSerializer custom;
|
||||||
|
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
class WiFiPrefs : public ConfigSerializer {
|
||||||
|
NodePrefs* _parent;
|
||||||
|
protected:
|
||||||
|
void structure() override {
|
||||||
|
def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid));
|
||||||
|
def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd));
|
||||||
|
def("enabled", _parent->wifi_enabled);
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
WiFiPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||||
|
};
|
||||||
|
WiFiPrefs wifi;
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void structure() override {
|
void structure() override {
|
||||||
def("name", node_name, sizeof(node_name));
|
def("name", node_name, sizeof(node_name));
|
||||||
@@ -128,9 +200,17 @@ protected:
|
|||||||
def("gps", gps);
|
def("gps", gps);
|
||||||
def("repeat", repeat);
|
def("repeat", repeat);
|
||||||
def("comp", companion);
|
def("comp", companion);
|
||||||
|
def("custom", custom);
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
def("wifi", wifi);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
NodePrefs() : radio(this), gps(this), companion(this) {
|
NodePrefs() : radio(this), gps(this), companion(this), custom(&radio)
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
, wifi(this)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
node_name[0] = 0;
|
node_name[0] = 0;
|
||||||
default_scope_name[0] = 0;
|
default_scope_name[0] = 0;
|
||||||
memset(default_scope_key, 0, sizeof(default_scope_key));
|
memset(default_scope_key, 0, sizeof(default_scope_key));
|
||||||
@@ -138,4 +218,10 @@ public:
|
|||||||
// new accessor methods
|
// new accessor methods
|
||||||
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
|
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
|
||||||
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
|
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
|
||||||
|
|
||||||
|
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(); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,19 +26,38 @@ MultiSerialInterface interface_manager;
|
|||||||
// include nrf52 bluetooth interface
|
// include nrf52 bluetooth interface
|
||||||
#include <helpers/nrf52/SerialBLEInterface.h>
|
#include <helpers/nrf52/SerialBLEInterface.h>
|
||||||
SerialBLEInterface bluetooth_interface;
|
SerialBLEInterface bluetooth_interface;
|
||||||
|
#elif defined(RP2040_PLATFORM)
|
||||||
|
// include rp2040 (Pico W / CYW43) bluetooth interface
|
||||||
|
#include <helpers/rp2040/SerialBLEInterface.h>
|
||||||
|
SerialBLEInterface bluetooth_interface;
|
||||||
#else
|
#else
|
||||||
#error "SerialBLEInterface is not defined for this platform"
|
#error "SerialBLEInterface is not defined for this platform"
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// include wifi interface
|
// include wifi interface
|
||||||
#ifdef WIFI_SSID
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
|
#ifndef WIFI_SSID
|
||||||
|
#define WIFI_SSID ""
|
||||||
|
#endif
|
||||||
|
#ifndef WIFI_PWD
|
||||||
|
#define WIFI_PWD ""
|
||||||
|
#endif
|
||||||
#ifndef TCP_PORT
|
#ifndef TCP_PORT
|
||||||
#define TCP_PORT 5000
|
#define TCP_PORT 5000
|
||||||
#endif
|
#endif
|
||||||
#ifdef ESP32
|
#ifndef WIFI_RETRY_INTERVAL
|
||||||
// include esp32 wifi interface
|
#if defined(RP2040_PLATFORM)
|
||||||
#include <helpers/esp32/SerialWifiInterface.h>
|
#define WIFI_RETRY_INTERVAL 30000 // each attempt blocks loop(), so retry less often
|
||||||
|
#else
|
||||||
|
#define WIFI_RETRY_INTERVAL 10000 // millis between reconnect attempts
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#ifndef WIFI_RETRY_TIMEOUT
|
||||||
|
#define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop()
|
||||||
|
#endif
|
||||||
|
#if defined(ESP32) || defined(RP2040_PLATFORM)
|
||||||
|
#include <helpers/wifi/SerialWifiInterface.h>
|
||||||
SerialWifiInterface wifi_interface;
|
SerialWifiInterface wifi_interface;
|
||||||
#else
|
#else
|
||||||
#error "SerialWifiInterface is not defined for this platform"
|
#error "SerialWifiInterface is not defined for this platform"
|
||||||
@@ -95,11 +114,7 @@ MultiSerialInterface interface_manager;
|
|||||||
|
|
||||||
StdRNG fast_rng;
|
StdRNG fast_rng;
|
||||||
SimpleMeshTables tables;
|
SimpleMeshTables tables;
|
||||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
, &ui_task
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
|
|
||||||
/* END GLOBAL OBJECTS */
|
/* END GLOBAL OBJECTS */
|
||||||
|
|
||||||
@@ -108,9 +123,13 @@ void halt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* WIFI RECONNECT TRACKERS */
|
/* WIFI RECONNECT TRACKERS */
|
||||||
#if defined(ESP32) && defined(WIFI_SSID)
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
bool wifi_needs_reconnect = false;
|
bool wifi_needs_reconnect = false;
|
||||||
unsigned long last_wifi_reconnect_attempt = 0;
|
unsigned long last_wifi_reconnect_attempt = 0;
|
||||||
|
char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set
|
||||||
|
char wifi_pwd[64] = WIFI_PWD;
|
||||||
|
bool wifi_was_connected = false;
|
||||||
|
bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -153,37 +172,38 @@ void setup() {
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
store.begin();
|
store.begin();
|
||||||
the_mesh.begin(
|
the_mesh.begin();
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
disp != NULL
|
|
||||||
#else
|
|
||||||
false
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
#elif defined(RP2040_PLATFORM)
|
#elif defined(RP2040_PLATFORM)
|
||||||
LittleFS.begin();
|
LittleFS.begin();
|
||||||
store.begin();
|
store.begin();
|
||||||
the_mesh.begin(
|
the_mesh.begin();
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
disp != NULL
|
|
||||||
#else
|
|
||||||
false
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
SPIFFS.begin(true);
|
SPIFFS.begin(true);
|
||||||
store.begin();
|
store.begin();
|
||||||
the_mesh.begin(
|
the_mesh.begin();
|
||||||
#ifdef DISPLAY_CLASS
|
|
||||||
disp != NULL
|
|
||||||
#else
|
|
||||||
false
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
#else
|
#else
|
||||||
#error "need to define filesystem"
|
#error "need to define filesystem"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef BLE_PIN_CODE // 123456 by default
|
||||||
|
if (the_mesh.getNodePrefs()->ble_pin == 0) {
|
||||||
|
#ifdef DISPLAY_CLASS
|
||||||
|
if (disp != NULL && BLE_PIN_CODE == 123456) {
|
||||||
|
StdRNG rng;
|
||||||
|
the_mesh.setBLEPin(rng.nextInt(100000, 999999)); // random pin each session
|
||||||
|
} else {
|
||||||
|
the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
the_mesh.setBLEPin(the_mesh.getNodePrefs()->ble_pin);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
the_mesh.setBLEPin(0);
|
||||||
|
#endif
|
||||||
|
|
||||||
// add bluetooth interface
|
// add bluetooth interface
|
||||||
#if defined(BLE_PIN_CODE)
|
#if defined(BLE_PIN_CODE)
|
||||||
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
|
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
|
||||||
@@ -191,23 +211,47 @@ void setup() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// add wifi interface
|
// add wifi interface
|
||||||
#ifdef WIFI_SSID
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
board.setInhibitSleep(true); // prevent sleep when WiFi is active
|
// use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults
|
||||||
WiFi.setAutoReconnect(true);
|
if (the_mesh.getNodePrefs()->wifi_ssid[0]) {
|
||||||
|
strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid);
|
||||||
|
strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd);
|
||||||
|
}
|
||||||
|
// only start wifi if enabled and ssid is not empty
|
||||||
|
wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled;
|
||||||
|
if (wifi_enabled && wifi_ssid[0]) {
|
||||||
|
#if defined(ESP32)
|
||||||
|
board.setInhibitSleep(true); // prevent sleep when WiFi is active
|
||||||
|
WiFi.setAutoReconnect(true);
|
||||||
|
|
||||||
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
|
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
|
||||||
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
|
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
|
||||||
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
|
WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...",
|
||||||
wifi_needs_reconnect = true;
|
WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason));
|
||||||
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
|
wifi_needs_reconnect = true;
|
||||||
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
|
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
|
||||||
wifi_needs_reconnect = false;
|
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
|
||||||
}
|
wifi_needs_reconnect = false;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
|
||||||
WiFi.begin(WIFI_SSID, WIFI_PWD);
|
WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid);
|
||||||
wifi_interface.begin(TCP_PORT);
|
|
||||||
interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
|
#if defined(RP2040_PLATFORM)
|
||||||
|
// the join itself blocks inside the core (CYW43::begin busy-waits for the
|
||||||
|
// association), so every attempt stalls the mesh loop. beginNoBlock() only skips the
|
||||||
|
// extra DHCP wait. Give the first connect a full window, then bound the retries below.
|
||||||
|
WiFi.beginNoBlock(wifi_ssid, wifi_pwd);
|
||||||
|
last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry
|
||||||
|
#else
|
||||||
|
WiFi.begin(wifi_ssid, wifi_pwd);
|
||||||
|
#endif
|
||||||
|
wifi_interface.begin(TCP_PORT);
|
||||||
|
interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
|
||||||
|
} else {
|
||||||
|
WIFI_DEBUG_PRINTLN("wifi disabled");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// add usb interface
|
// add usb interface
|
||||||
@@ -239,6 +283,7 @@ void setup() {
|
|||||||
|
|
||||||
#ifdef DISPLAY_CLASS
|
#ifdef DISPLAY_CLASS
|
||||||
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
|
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
|
||||||
|
the_mesh.setListener(&ui_task);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
board.onBootComplete();
|
board.onBootComplete();
|
||||||
@@ -252,6 +297,7 @@ void loop() {
|
|||||||
ui_task.loop();
|
ui_task.loop();
|
||||||
#endif
|
#endif
|
||||||
rtc_clock.tick();
|
rtc_clock.tick();
|
||||||
|
board.loop();
|
||||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||||
external_watchdog.loop();
|
external_watchdog.loop();
|
||||||
#endif
|
#endif
|
||||||
@@ -262,13 +308,34 @@ void loop() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(ESP32) && defined(WIFI_SSID)
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
// Safely attempt to reconnect every 10 seconds if flagged
|
if (wifi_enabled) {
|
||||||
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
|
// RP2040 has no WiFi event callbacks, so poll the link state instead
|
||||||
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
|
#if defined(RP2040_PLATFORM)
|
||||||
WiFi.disconnect();
|
wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED);
|
||||||
WiFi.reconnect();
|
if (wifi_was_connected == wifi_needs_reconnect) { // link state changed
|
||||||
last_wifi_reconnect_attempt = millis();
|
wifi_was_connected = !wifi_needs_reconnect;
|
||||||
|
if (wifi_was_connected) {
|
||||||
|
WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT);
|
||||||
|
} else {
|
||||||
|
WIFI_DEBUG_PRINTLN("link lost");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop
|
||||||
|
// for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall.
|
||||||
|
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) {
|
||||||
|
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status());
|
||||||
|
#if defined(RP2040_PLATFORM)
|
||||||
|
WiFi.setTimeout(WIFI_RETRY_TIMEOUT);
|
||||||
|
WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform
|
||||||
|
#else
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.reconnect();
|
||||||
|
#endif
|
||||||
|
last_wifi_reconnect_attempt = millis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
#include <helpers/TxtDataHelpers.h>
|
#include <helpers/TxtDataHelpers.h>
|
||||||
#include "../MyMesh.h"
|
#include "../MyMesh.h"
|
||||||
#include "target.h"
|
#include "target.h"
|
||||||
#ifdef WIFI_SSID
|
#include <time.h>
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -19,11 +20,12 @@
|
|||||||
|
|
||||||
#define LONG_PRESS_MILLIS 1200
|
#define LONG_PRESS_MILLIS 1200
|
||||||
|
|
||||||
|
// Used both for recent adverts and discovered nodes
|
||||||
#ifndef UI_RECENT_LIST_SIZE
|
#ifndef UI_RECENT_LIST_SIZE
|
||||||
#define UI_RECENT_LIST_SIZE 4
|
#define UI_RECENT_LIST_SIZE 4
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if UI_HAS_JOYSTICK
|
#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT
|
||||||
#define PRESS_LABEL "press Enter"
|
#define PRESS_LABEL "press Enter"
|
||||||
#else
|
#else
|
||||||
#define PRESS_LABEL "long press"
|
#define PRESS_LABEL "long press"
|
||||||
@@ -97,7 +99,12 @@ class HomeScreen : public UIScreen {
|
|||||||
#if UI_SENSORS_PAGE == 1
|
#if UI_SENSORS_PAGE == 1
|
||||||
SENSORS,
|
SENSORS,
|
||||||
#endif
|
#endif
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
DISCOVERY,
|
||||||
|
#endif
|
||||||
|
#ifndef UI_NO_HIBERNATE
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
|
#endif
|
||||||
Count // keep as last
|
Count // keep as last
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +115,13 @@ class HomeScreen : public UIScreen {
|
|||||||
uint8_t _page;
|
uint8_t _page;
|
||||||
bool _shutdown_init;
|
bool _shutdown_init;
|
||||||
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
DiscoveredNode discovered[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept
|
||||||
|
uint32_t disc_node_req_tag = 0;
|
||||||
|
uint32_t disc_nodes_count = 0;
|
||||||
|
uint32_t discovery_req_time = 0;
|
||||||
|
bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O)
|
||||||
|
#endif
|
||||||
|
|
||||||
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
||||||
// Convert millivolts to percentage
|
// Convert millivolts to percentage
|
||||||
@@ -141,11 +154,24 @@ class HomeScreen : public UIScreen {
|
|||||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||||
|
|
||||||
// show muted icon if buzzer is muted
|
// while charging, show a bolt (or a plug once full) just left of the battery,
|
||||||
|
// keeping the fill bar itself clean and uninterrupted
|
||||||
|
bool charging = board.isExternalPowered();
|
||||||
|
if (charging) {
|
||||||
|
// There's no charge-complete signal on most boards, so "full" is a high
|
||||||
|
// voltage band rather than an exact 100% (a real pack rarely reads 4.2V).
|
||||||
|
const int BATT_FULL_PCT = 95;
|
||||||
|
const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon;
|
||||||
|
display.setColor(UIColor::title_txt);
|
||||||
|
display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show muted icon if buzzer is muted (shifted further left when the charging
|
||||||
|
// icon already occupies the slot immediately left of the battery)
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
if (_task->isBuzzerQuiet()) {
|
if (_task->isBuzzerQuiet()) {
|
||||||
display.setColor(UIColor::warning_txt);
|
display.setColor(UIColor::warning_txt);
|
||||||
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
|
display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -182,6 +208,40 @@ public:
|
|||||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||||
_shutdown_init(false), sensors_lpp(200) { }
|
_shutdown_init(false), sensors_lpp(200) { }
|
||||||
|
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
bool sendDiscoverRequest() {
|
||||||
|
uint8_t cmd_bytes[6];
|
||||||
|
cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only
|
||||||
|
cmd_bytes[1] = 0xFF; // Repeaters
|
||||||
|
the_mesh.getRNG()->random((uint8_t *) &disc_node_req_tag, 4); // generate random tag
|
||||||
|
memcpy(&cmd_bytes[2], &disc_node_req_tag, 4);
|
||||||
|
disc_nodes_count = 0;
|
||||||
|
mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes));
|
||||||
|
if (req) {
|
||||||
|
the_mesh.sendZeroHop(req);
|
||||||
|
discovery_req_time = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDiscoverResponse(const mesh::Packet* packet) {
|
||||||
|
if (disc_nodes_count < DISCOVERED_NODES_TABLE_SIZE && memcmp(&packet->payload[2], &disc_node_req_tag, 4) == 0) {
|
||||||
|
auto d = &discovered[disc_nodes_count++];
|
||||||
|
memcpy(d->pubkey_prefix, &packet->payload[6], 8);
|
||||||
|
d->type = packet->payload[0] & 0xF;
|
||||||
|
d->snr_out = ((int8_t)packet->payload[1]) / 4.0;
|
||||||
|
d->snr_in = radio_driver.getLastSNR();
|
||||||
|
ContactInfo* c = the_mesh.lookupContactByPubKey(&packet->payload[6], 8);
|
||||||
|
if (c != NULL) {
|
||||||
|
strncpy(d->name, c->name, 32);
|
||||||
|
} else {
|
||||||
|
d->name[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void poll() override {
|
void poll() override {
|
||||||
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
||||||
_task->shutdown();
|
_task->shutdown();
|
||||||
@@ -224,8 +284,19 @@ public:
|
|||||||
display.setTextSize(2);
|
display.setTextSize(2);
|
||||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||||
display.drawTextCentered(display.width() / 2, 22, tmp);
|
display.drawTextCentered(display.width() / 2, 22, tmp);
|
||||||
|
|
||||||
#ifdef WIFI_SSID
|
#ifdef UI_SHOW_CLOCK
|
||||||
|
display.setTextSize(3);
|
||||||
|
uint32_t now = _rtc->getCurrentTime();
|
||||||
|
now += (int32_t)_node_prefs->tz_offset * 3600;
|
||||||
|
DateTime dt (now);
|
||||||
|
sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute());
|
||||||
|
display.drawTextCentered(display.width() / 2, 60, tmp);
|
||||||
|
display.setTextSize(1);
|
||||||
|
sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year());
|
||||||
|
display.drawTextCentered(display.width() / 2, 80, tmp);
|
||||||
|
#endif
|
||||||
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
IPAddress ip = WiFi.localIP();
|
IPAddress ip = WiFi.localIP();
|
||||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
@@ -234,13 +305,21 @@ public:
|
|||||||
if (_task->hasConnection()) {
|
if (_task->hasConnection()) {
|
||||||
display.setColor(UIColor::warning_txt);
|
display.setColor(UIColor::warning_txt);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
|
#ifdef UI_SHOW_CLOCK
|
||||||
|
display.drawTextCentered(display.width() / 2, 110, "< Connected >");
|
||||||
|
#else
|
||||||
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
|
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
|
||||||
|
#endif
|
||||||
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
||||||
display.setColor(UIColor::warning_txt);
|
display.setColor(UIColor::warning_txt);
|
||||||
display.setTextSize(2);
|
|
||||||
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
||||||
|
#ifdef UI_SHOW_CLOCK
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.drawTextCentered(display.width() / 2, 110, tmp);
|
||||||
|
#else
|
||||||
|
display.setTextSize(2);
|
||||||
display.drawTextCentered(display.width() / 2, 43, tmp);
|
display.drawTextCentered(display.width() / 2, 43, tmp);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else if (_page == HomePage::RECENT) {
|
} else if (_page == HomePage::RECENT) {
|
||||||
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
||||||
@@ -421,6 +500,41 @@ public:
|
|||||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||||
else sensors_scroll_offset = 0;
|
else sensors_scroll_offset = 0;
|
||||||
#endif
|
#endif
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
} else if (_page == HomePage::DISCOVERY) {
|
||||||
|
int count = disc_nodes_count;
|
||||||
|
display.setColor(UIColor::primary_txt);
|
||||||
|
int y = 20;
|
||||||
|
for (int i = 0; i < count; i++, y += 11) {
|
||||||
|
char name[32];
|
||||||
|
auto a = &discovered[i];
|
||||||
|
if ((a->name[0] == 0) || !discovery_disp_names) {
|
||||||
|
mesh::Utils::toHex(name, a->pubkey_prefix, 4);
|
||||||
|
} else {
|
||||||
|
strncpy(name, a->name, 32);
|
||||||
|
}
|
||||||
|
char filtered_name[sizeof(name)];
|
||||||
|
char snr_s[12];
|
||||||
|
if (strlen(name) <= 8) { // display snr_o
|
||||||
|
sprintf(snr_s, "%02.1f>%02.1f", a->snr_out, a->snr_in);
|
||||||
|
} else {
|
||||||
|
sprintf(snr_s, "%02.1f", a->snr_in);
|
||||||
|
}
|
||||||
|
int snr_width = display.getTextWidth(snr_s);
|
||||||
|
int max_name_width = display.width() - snr_width - 1;
|
||||||
|
display.translateUTF8ToBlocks(filtered_name, name, sizeof(filtered_name));
|
||||||
|
display.drawTextEllipsized(0, y, max_name_width, filtered_name);
|
||||||
|
display.setCursor(display.width() - snr_width - 1, y);
|
||||||
|
display.print(snr_s);
|
||||||
|
}
|
||||||
|
if (millis() < discovery_req_time + 5000) {
|
||||||
|
return 1000; // more frequent updates just after req
|
||||||
|
} else if (count < DISCOVERED_NODES_TABLE_SIZE -1) { // show only 5 sec after last disc
|
||||||
|
y = 10 + 11 * DISCOVERED_NODES_TABLE_SIZE;
|
||||||
|
display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifndef UI_NO_HIBERNATE
|
||||||
} else if (_page == HomePage::SHUTDOWN) {
|
} else if (_page == HomePage::SHUTDOWN) {
|
||||||
display.setColor(UIColor::corp_blue);
|
display.setColor(UIColor::corp_blue);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
@@ -432,6 +546,7 @@ public:
|
|||||||
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
|
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
|
||||||
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
|
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
return 5000; // next render after 5000 ms
|
return 5000; // next render after 5000 ms
|
||||||
}
|
}
|
||||||
@@ -446,6 +561,11 @@ public:
|
|||||||
if (_page == HomePage::RECENT) {
|
if (_page == HomePage::RECENT) {
|
||||||
_task->showAlert("Recent adverts", 800);
|
_task->showAlert("Recent adverts", 800);
|
||||||
}
|
}
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
if (_page == HomePage::DISCOVERY) {
|
||||||
|
_task->showAlert("Repeater disc", 800);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
||||||
@@ -478,14 +598,32 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
if (c == KEY_ENTER && _page == HomePage::DISCOVERY) {
|
||||||
|
if (millis() > discovery_req_time + 5000) { // rate limiter
|
||||||
|
sendDiscoverRequest();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c == KEY_SELECT && _page == HomePage::DISCOVERY) {
|
||||||
|
discovery_disp_names = !discovery_disp_names;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifndef UI_NO_HIBERNATE
|
||||||
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
||||||
_shutdown_init = true; // need to wait for button to be released
|
_shutdown_init = true; // need to wait for button to be released
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef UI_MSG_PREVIEW_SIZE
|
||||||
|
#define UI_MSG_PREVIEW_SIZE 78
|
||||||
|
#endif
|
||||||
|
|
||||||
class MsgPreviewScreen : public UIScreen {
|
class MsgPreviewScreen : public UIScreen {
|
||||||
UITask* _task;
|
UITask* _task;
|
||||||
mesh::RTCClock* _rtc;
|
mesh::RTCClock* _rtc;
|
||||||
@@ -493,7 +631,7 @@ class MsgPreviewScreen : public UIScreen {
|
|||||||
struct MsgEntry {
|
struct MsgEntry {
|
||||||
uint32_t timestamp;
|
uint32_t timestamp;
|
||||||
char origin[62];
|
char origin[62];
|
||||||
char msg[78];
|
char msg[UI_MSG_PREVIEW_SIZE];
|
||||||
};
|
};
|
||||||
#define MAX_UNREAD_MSGS 32
|
#define MAX_UNREAD_MSGS 32
|
||||||
int num_unread;
|
int num_unread;
|
||||||
@@ -648,18 +786,12 @@ switch(t){
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) {
|
||||||
|
// we only want to show text messages on display, not cli data
|
||||||
|
if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return;
|
||||||
|
|
||||||
void UITask::msgRead(int msgcount) {
|
uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
|
||||||
_msgcount = msgcount;
|
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text);
|
||||||
if (msgcount == 0) {
|
|
||||||
gotoHomeScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
|
||||||
_msgcount = msgcount;
|
|
||||||
|
|
||||||
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
|
|
||||||
setCurrScreen(msg_preview);
|
setCurrScreen(msg_preview);
|
||||||
|
|
||||||
if (_display != NULL) {
|
if (_display != NULL) {
|
||||||
@@ -671,6 +803,51 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
|||||||
_next_refresh = 100; // trigger refresh
|
_next_refresh = 100; // trigger refresh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::contactMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) {
|
||||||
|
uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
|
||||||
|
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text);
|
||||||
|
setCurrScreen(msg_preview);
|
||||||
|
|
||||||
|
if (_display != NULL) {
|
||||||
|
if (!_display->isOn() && !hasConnection()) {
|
||||||
|
_display->turnOn();
|
||||||
|
}
|
||||||
|
if (_display->isOn()) {
|
||||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||||
|
_next_refresh = 100; // trigger refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::channelMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onQueueSizeChanged(int msgcount) {
|
||||||
|
_msgcount = msgcount;
|
||||||
|
if (msgcount == 0) {
|
||||||
|
gotoHomeScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::newContactMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onControlDataRecv(const mesh::Packet* packet) {
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
if (packet->payload_len >= 14 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) {
|
||||||
|
((HomeScreen *) home)->handleDiscoverResponse(packet);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::userLedHandler() {
|
void UITask::userLedHandler() {
|
||||||
@@ -720,6 +897,9 @@ void UITask::shutdown(bool restart){
|
|||||||
if (restart) {
|
if (restart) {
|
||||||
_board->reboot();
|
_board->reboot();
|
||||||
} else {
|
} else {
|
||||||
|
display.forceFullRefresh();
|
||||||
|
display.clear();
|
||||||
|
display.endFrame();
|
||||||
// Power off board including radio, display, GPS and components
|
// Power off board including radio, display, GPS and components
|
||||||
_board->powerOff();
|
_board->powerOff();
|
||||||
}
|
}
|
||||||
@@ -760,6 +940,17 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
#elif defined(PIN_USER_BTN)
|
#elif defined(PIN_USER_BTN)
|
||||||
int ev = user_btn.check();
|
int ev = user_btn.check();
|
||||||
|
#ifdef UI_HAS_NAV_INPUT
|
||||||
|
if (ev == BUTTON_EVENT_CLICK) {
|
||||||
|
c = checkDisplayOn(KEY_ENTER);
|
||||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
|
display.turnOff();
|
||||||
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||||
|
c = handleDoubleClick(KEY_SELECT);
|
||||||
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||||
|
c = handleTripleClick(KEY_SELECT);
|
||||||
|
}
|
||||||
|
#else
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) {
|
||||||
c = checkDisplayOn(KEY_NEXT);
|
c = checkDisplayOn(KEY_NEXT);
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
@@ -769,6 +960,7 @@ void UITask::loop() {
|
|||||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||||
c = handleTripleClick(KEY_SELECT);
|
c = handleTripleClick(KEY_SELECT);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#if defined(UI_HAS_ROTARY_INPUT)
|
#if defined(UI_HAS_ROTARY_INPUT)
|
||||||
RotaryInputEvent rotaryEv = rotary_input.poll();
|
RotaryInputEvent rotaryEv = rotary_input.poll();
|
||||||
|
|||||||
@@ -22,6 +22,30 @@
|
|||||||
#include "../AbstractUITask.h"
|
#include "../AbstractUITask.h"
|
||||||
#include "../NodePrefs.h"
|
#include "../NodePrefs.h"
|
||||||
|
|
||||||
|
#ifdef UI_NO_DISCOVER_SCREEN
|
||||||
|
#error "UI_NO_DISCOVER_SCREEN is obsolete - use -D UI_DISCOVER_SCREEN=0 instead"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef UI_DISCOVER_SCREEN
|
||||||
|
#define UI_DISCOVER_SCREEN 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if UI_DISCOVER_SCREEN
|
||||||
|
#ifdef UI_RECENT_LIST_SIZE
|
||||||
|
#define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE
|
||||||
|
#else
|
||||||
|
#define DISCOVERED_NODES_TABLE_SIZE 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct DiscoveredNode {
|
||||||
|
float snr_in;
|
||||||
|
float snr_out;
|
||||||
|
uint8_t pubkey_prefix[9];
|
||||||
|
uint8_t type;
|
||||||
|
char name[32];
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
class UITask : public AbstractUITask {
|
class UITask : public AbstractUITask {
|
||||||
DisplayDriver* _display;
|
DisplayDriver* _display;
|
||||||
SensorManager* _sensors;
|
SensorManager* _sensors;
|
||||||
@@ -90,10 +114,14 @@ public:
|
|||||||
bool getGPSState();
|
bool getGPSState();
|
||||||
void toggleGPS();
|
void toggleGPS();
|
||||||
|
|
||||||
|
// MyMesh::Listener
|
||||||
|
void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override;
|
||||||
|
void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override;
|
||||||
|
void onQueueSizeChanged(int offline_queue_size) override;
|
||||||
|
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||||
|
void onControlDataRecv(const mesh::Packet* pkt) override;
|
||||||
|
|
||||||
// from AbstractUITask
|
// from AbstractUITask
|
||||||
void msgRead(int msgcount) override;
|
|
||||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
|
||||||
void notify(UIEventType t = UIEventType::none) override;
|
void notify(UIEventType t = UIEventType::none) override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
|
|||||||
@@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = {
|
|||||||
|
|
||||||
static const uint8_t muted_icon[] = {
|
static const uint8_t muted_icon[] = {
|
||||||
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
|
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
|
||||||
|
};
|
||||||
|
|
||||||
|
// small lightning bolt, 8x8px, shown next to the battery icon while charging
|
||||||
|
static const uint8_t charging_icon[] = {
|
||||||
|
0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0
|
||||||
|
};
|
||||||
|
|
||||||
|
// small power plug, 8x8px, shown next to the battery icon once fully charged
|
||||||
|
static const uint8_t plug_icon[] = {
|
||||||
|
0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18
|
||||||
};
|
};
|
||||||
@@ -126,7 +126,7 @@ switch(t){
|
|||||||
// Serial.println((int) t);
|
// Serial.println((int) t);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::msgRead(int msgcount) {
|
void UITask::onQueueSizeChanged(int msgcount) {
|
||||||
_msgcount = msgcount;
|
_msgcount = msgcount;
|
||||||
if (msgcount == 0) {
|
if (msgcount == 0) {
|
||||||
clearMsgPreview();
|
clearMsgPreview();
|
||||||
@@ -139,17 +139,19 @@ void UITask::clearMsgPreview() {
|
|||||||
_need_refresh = true;
|
_need_refresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) {
|
||||||
_msgcount = msgcount;
|
// we only want to show text messages on display, not cli data
|
||||||
|
if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return;
|
||||||
|
|
||||||
#ifdef HAS_DRV2605
|
#ifdef HAS_DRV2605
|
||||||
vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown)
|
vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
|
||||||
if (path_len == 0xFF) {
|
if (path_len == 0xFF) {
|
||||||
sprintf(_origin, "(F) %s", from_name);
|
sprintf(_origin, "(F) %s", from.name);
|
||||||
} else {
|
} else {
|
||||||
sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name);
|
sprintf(_origin, "(%d) %s", (uint32_t) path_len, from.name);
|
||||||
}
|
}
|
||||||
StrHelper::strncpy(_msg, text, sizeof(_msg));
|
StrHelper::strncpy(_msg, text, sizeof(_msg));
|
||||||
|
|
||||||
@@ -158,10 +160,46 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
|||||||
_display->turnOn();
|
_display->turnOn();
|
||||||
}
|
}
|
||||||
if (_display->isOn()) {
|
if (_display->isOn()) {
|
||||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||||
_need_refresh = true;
|
_need_refresh = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::contactMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) {
|
||||||
|
#ifdef HAS_DRV2605
|
||||||
|
vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
|
||||||
|
if (path_len == 0xFF) {
|
||||||
|
sprintf(_origin, "(F) %s", channel_details.name);
|
||||||
|
} else {
|
||||||
|
sprintf(_origin, "(%d) %s", (uint32_t) path_len, channel_details.name);
|
||||||
|
}
|
||||||
|
StrHelper::strncpy(_msg, text, sizeof(_msg));
|
||||||
|
|
||||||
|
if (_display != NULL) {
|
||||||
|
if (!_display->isOn() && !hasConnection()) {
|
||||||
|
_display->turnOn();
|
||||||
|
}
|
||||||
|
if (_display->isOn()) {
|
||||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||||
|
_need_refresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::channelMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::newContactMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
|
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
|
||||||
@@ -262,7 +300,7 @@ void UITask::renderCurrScreen() {
|
|||||||
_display->print(tmp);
|
_display->print(tmp);
|
||||||
|
|
||||||
// BT pin
|
// BT pin
|
||||||
if (!_connected && the_mesh.getBLEPin() != 0) {
|
if (!hasConnection() && the_mesh.getBLEPin() != 0) {
|
||||||
_display->setColor(UIColor::warning_txt);
|
_display->setColor(UIColor::warning_txt);
|
||||||
_display->setTextSize(2);
|
_display->setTextSize(2);
|
||||||
_display->setCursor(0, 43);
|
_display->setCursor(0, 43);
|
||||||
@@ -372,7 +410,7 @@ void UITask::userLedHandler() {
|
|||||||
statusLedWrite(255, 0, 0); // red: battery low
|
statusLedWrite(255, 0, 0); // red: battery low
|
||||||
} else if (_msgcount > 0) {
|
} else if (_msgcount > 0) {
|
||||||
statusLedWrite(255, 90, 0); // amber: unread messages
|
statusLedWrite(255, 90, 0); // amber: unread messages
|
||||||
} else if (_connected) {
|
} else if (hasConnection()) {
|
||||||
statusLedWrite(0, 0, 255); // blue: app connected
|
statusLedWrite(0, 0, 255); // blue: app connected
|
||||||
} else {
|
} else {
|
||||||
statusLedWrite(0, 255, 0); // green: heartbeat
|
statusLedWrite(0, 255, 0); // green: heartbeat
|
||||||
|
|||||||
@@ -70,9 +70,13 @@ public:
|
|||||||
bool hasDisplay() const { return _display != NULL; }
|
bool hasDisplay() const { return _display != NULL; }
|
||||||
void clearMsgPreview();
|
void clearMsgPreview();
|
||||||
|
|
||||||
|
// MyMesh::Listener
|
||||||
|
void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override;
|
||||||
|
void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override;
|
||||||
|
void onQueueSizeChanged(int offline_queue_size) override;
|
||||||
|
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||||
|
|
||||||
// from AbstractUITask
|
// from AbstractUITask
|
||||||
void msgRead(int msgcount) override;
|
|
||||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
|
||||||
void notify(UIEventType t = UIEventType::none) override;
|
void notify(UIEventType t = UIEventType::none) override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include "target.h"
|
#include "target.h"
|
||||||
#include "u8g2_icons.h"
|
#include "u8g2_icons.h"
|
||||||
|
|
||||||
#ifdef WIFI_SSID
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ public:
|
|||||||
display.setCursor(0, 19);
|
display.setCursor(0, 19);
|
||||||
display.print(tmp);
|
display.print(tmp);
|
||||||
|
|
||||||
#ifdef WIFI_SSID
|
#ifdef ENABLE_WIFI_INTERFACE
|
||||||
IPAddress ip = WiFi.localIP();
|
IPAddress ip = WiFi.localIP();
|
||||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
@@ -503,15 +503,16 @@ switch(t){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void UITask::msgRead(int msgcount) {
|
void UITask::onQueueSizeChanged(int msgcount) {
|
||||||
_msgcount = msgcount;
|
_msgcount = msgcount;
|
||||||
if (msgcount == 0) {
|
if (msgcount == 0) {
|
||||||
gotoHomeScreen();
|
gotoHomeScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) {
|
||||||
_msgcount = msgcount;
|
// we only want to show text messages on display, not cli data
|
||||||
|
if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return;
|
||||||
|
|
||||||
if (_display != NULL) {
|
if (_display != NULL) {
|
||||||
if (!_display->isOn() && !hasConnection()) {
|
if (!_display->isOn() && !hasConnection()) {
|
||||||
@@ -522,6 +523,30 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
|||||||
_next_refresh = 100; // trigger refresh
|
_next_refresh = 100; // trigger refresh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::contactMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) {
|
||||||
|
if (_display != NULL) {
|
||||||
|
if (!_display->isOn() && !hasConnection()) {
|
||||||
|
_display->turnOn();
|
||||||
|
}
|
||||||
|
if (_display->isOn()) {
|
||||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||||
|
_next_refresh = 100; // trigger refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::channelMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
|
||||||
|
if (!hasConnection()) {
|
||||||
|
notify(UIEventType::newContactMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::userLedHandler() {
|
void UITask::userLedHandler() {
|
||||||
|
|||||||
@@ -98,10 +98,13 @@ public:
|
|||||||
bool getGPSState();
|
bool getGPSState();
|
||||||
void toggleGPS();
|
void toggleGPS();
|
||||||
|
|
||||||
|
// MyMesh::Listener
|
||||||
|
void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override;
|
||||||
|
void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override;
|
||||||
|
void onQueueSizeChanged(int offline_queue_size) override;
|
||||||
|
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||||
|
|
||||||
// from AbstractUITask
|
// from AbstractUITask
|
||||||
void msgRead(int msgcount) override;
|
|
||||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
|
||||||
void notify(UIEventType t = UIEventType::none) override;
|
void notify(UIEventType t = UIEventType::none) override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ void setup() {
|
|||||||
uint32_t start = millis();
|
uint32_t start = millis();
|
||||||
while (!Serial && millis() - start < 3000) delay(10);
|
while (!Serial && millis() - start < 3000) delay(10);
|
||||||
delay(100);
|
delay(100);
|
||||||
#if defined(ESP32) && ARDUINO_USB_MODE
|
#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||||
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
|
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
|
||||||
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
|
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
|
||||||
#endif
|
#endif
|
||||||
@@ -149,6 +149,8 @@ void loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
board.loop();
|
||||||
|
|
||||||
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
|
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
|
||||||
radio_driver.triggerNoiseFloorCalibrate(0);
|
radio_driver.triggerNoiseFloorCalibrate(0);
|
||||||
next_noise_floor_calib_ms = millis();
|
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;
|
int results_offset = 0;
|
||||||
uint8_t results_buffer[130];
|
uint8_t results_buffer[130];
|
||||||
for(int index = 0; index < count && index + offset < neighbours_count; index++){
|
for(int index = 0; index < count && index + offset < neighbours_count; index++){
|
||||||
|
|
||||||
// stop if we can't fit another entry in results
|
// stop if we can't fit another entry in results
|
||||||
int entry_size = pubkey_prefix_length + 4 + 1;
|
int entry_size = pubkey_prefix_length + 4 + 1;
|
||||||
if(results_offset + entry_size > sizeof(results_buffer)){
|
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)
|
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
|
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);
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
||||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||||
@@ -907,6 +907,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
|||||||
_prefs.flood_max_advert = 8;
|
_prefs.flood_max_advert = 8;
|
||||||
_prefs.interference_threshold = 0; // disabled
|
_prefs.interference_threshold = 0; // disabled
|
||||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||||
|
_prefs.loop_detect = LOOP_DETECT_MINIMAL;
|
||||||
|
|
||||||
// bridge defaults
|
// bridge defaults
|
||||||
_prefs.bridge_enabled = 1; // enabled
|
_prefs.bridge_enabled = 1; // enabled
|
||||||
@@ -981,8 +982,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
|||||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
|
||||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
board.attachDynamicPrefs(_prefs.getCustom());
|
||||||
|
|
||||||
updateAdvertTimer();
|
updateAdvertTimer();
|
||||||
updateFloodAdvertTimer();
|
updateFloodAdvertTimer();
|
||||||
@@ -1170,7 +1171,7 @@ void MyMesh::formatRadioStatsReply(char *reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MyMesh::formatPacketStatsReply(char *reply) {
|
void MyMesh::formatPacketStatsReply(char *reply) {
|
||||||
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
||||||
getNumRecvFlood(), getNumRecvDirect());
|
getNumRecvFlood(), getNumRecvDirect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ struct RepeaterStats {
|
|||||||
uint32_t n_recv_errors;
|
uint32_t n_recv_errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifndef MAX_CLIENTS
|
|
||||||
#define MAX_CLIENTS 32
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct NeighbourInfo {
|
struct NeighbourInfo {
|
||||||
mesh::Identity id;
|
mesh::Identity id;
|
||||||
uint32_t advert_timestamp;
|
uint32_t advert_timestamp;
|
||||||
@@ -71,11 +67,11 @@ struct NeighbourInfo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#ifndef FIRMWARE_BUILD_DATE
|
#ifndef FIRMWARE_BUILD_DATE
|
||||||
#define FIRMWARE_BUILD_DATE "9 Aug 2026"
|
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef FIRMWARE_VERSION
|
#ifndef FIRMWARE_VERSION
|
||||||
#define FIRMWARE_VERSION "v1.17.0"
|
#define FIRMWARE_VERSION "v1.17.1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define FIRMWARE_ROLE "repeater"
|
#define FIRMWARE_ROLE "repeater"
|
||||||
|
|||||||
@@ -140,8 +140,10 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_powering_off_at > 0) { // power off timer armed
|
if (_powering_off_at > 0) { // power off timer armed
|
||||||
#ifdef LED_PIN
|
#if defined(LED_PIN) && defined(LED_STATE_ON)
|
||||||
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
|
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
|
||||||
|
delay(1000);
|
||||||
|
digitalWrite(LED_PIN, !LED_STATE_ON); // off LED
|
||||||
#endif
|
#endif
|
||||||
if (millis() > _powering_off_at) {
|
if (millis() > _powering_off_at) {
|
||||||
_board->powerOff(); // should not return
|
_board->powerOff(); // should not return
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ void setup() {
|
|||||||
void loop() {
|
void loop() {
|
||||||
// Handle Serial CLI
|
// Handle Serial CLI
|
||||||
int len = strlen(command);
|
int len = strlen(command);
|
||||||
|
// `command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||||
|
// strlen() above can return >= sizeof(command) and the loop below would then
|
||||||
|
// index past the buffer, so clamp defensively.
|
||||||
|
if (len >= (int)sizeof(command)) {
|
||||||
|
command[0] = 0;
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
while (Serial.available() && len < sizeof(command)-1) {
|
while (Serial.available() && len < sizeof(command)-1) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c != '\n') {
|
if (c != '\n') {
|
||||||
@@ -134,8 +141,9 @@ void loop() {
|
|||||||
}
|
}
|
||||||
if (c == '\r') break;
|
if (c == '\r') break;
|
||||||
}
|
}
|
||||||
if (len == sizeof(command)-1) { // command buffer full
|
if (len == sizeof(command)-1) { // buffer full: treat as a completed line
|
||||||
command[sizeof(command)-1] = '\r';
|
command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||||
|
command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
@@ -157,6 +165,8 @@ void loop() {
|
|||||||
command[0] = 0; // reset command buffer
|
command[0] = 0; // reset command buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
board.loop(); // let the board feed its watchdog, run periodic housekeeping
|
||||||
|
|
||||||
#ifdef ETHERNET_ENABLED
|
#ifdef ETHERNET_ENABLED
|
||||||
ethernet_loop_maintain();
|
ethernet_loop_maintain();
|
||||||
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
|
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)
|
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
|
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);
|
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
|
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
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];
|
uint8_t temp[166];
|
||||||
bool send_ack;
|
bool send_ack;
|
||||||
if (flags == TXT_TYPE_CLI_DATA) {
|
if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
||||||
if (client->isAdmin()) {
|
if (client->isAdmin()) {
|
||||||
if (is_retry) {
|
if (is_retry) {
|
||||||
temp[5] = 0; // no reply
|
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.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
|
||||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
board.attachDynamicPrefs(_prefs.getCustom());
|
||||||
|
|
||||||
updateAdvertTimer();
|
updateAdvertTimer();
|
||||||
updateFloodAdvertTimer();
|
updateFloodAdvertTimer();
|
||||||
|
|||||||
@@ -28,11 +28,11 @@
|
|||||||
/* ------------------------------ Config -------------------------------- */
|
/* ------------------------------ Config -------------------------------- */
|
||||||
|
|
||||||
#ifndef FIRMWARE_BUILD_DATE
|
#ifndef FIRMWARE_BUILD_DATE
|
||||||
#define FIRMWARE_BUILD_DATE "9 Aug 2026"
|
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef FIRMWARE_VERSION
|
#ifndef FIRMWARE_VERSION
|
||||||
#define FIRMWARE_VERSION "v1.17.0"
|
#define FIRMWARE_VERSION "v1.17.1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef LORA_FREQ
|
#ifndef LORA_FREQ
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
int len = strlen(command);
|
int len = strlen(command);
|
||||||
|
// `command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||||
|
// strlen() above can return >= sizeof(command) and the loop below would then
|
||||||
|
// index past the buffer, so clamp defensively.
|
||||||
|
if (len >= (int)sizeof(command)) {
|
||||||
|
command[0] = 0;
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
while (Serial.available() && len < sizeof(command)-1) {
|
while (Serial.available() && len < sizeof(command)-1) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c != '\n') {
|
if (c != '\n') {
|
||||||
@@ -113,8 +120,9 @@ void loop() {
|
|||||||
}
|
}
|
||||||
Serial.print(c);
|
Serial.print(c);
|
||||||
}
|
}
|
||||||
if (len == sizeof(command)-1) { // command buffer full
|
if (len == sizeof(command)-1) { // buffer full: treat as a completed line
|
||||||
command[sizeof(command)-1] = '\r';
|
command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||||
|
command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
@@ -154,6 +162,7 @@ void loop() {
|
|||||||
ui_task.loop();
|
ui_task.loop();
|
||||||
#endif
|
#endif
|
||||||
rtc_clock.tick();
|
rtc_clock.tick();
|
||||||
|
board.loop();
|
||||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||||
external_watchdog.loop();
|
external_watchdog.loop();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ protected:
|
|||||||
|
|
||||||
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
|
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 {
|
void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +530,13 @@ public:
|
|||||||
BaseChatMesh::loop();
|
BaseChatMesh::loop();
|
||||||
|
|
||||||
int len = strlen(command);
|
int len = strlen(command);
|
||||||
|
// `command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||||
|
// strlen() above can return >= sizeof(command) and the loop below would then
|
||||||
|
// index past the buffer, so clamp defensively.
|
||||||
|
if (len >= (int)sizeof(command)) {
|
||||||
|
command[0] = 0;
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
while (Serial.available() && len < sizeof(command)-1) {
|
while (Serial.available() && len < sizeof(command)-1) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c != '\n') {
|
if (c != '\n') {
|
||||||
@@ -534,8 +545,9 @@ public:
|
|||||||
}
|
}
|
||||||
Serial.print(c);
|
Serial.print(c);
|
||||||
}
|
}
|
||||||
if (len == sizeof(command)-1) { // command buffer full
|
if (len == sizeof(command)-1) { // buffer full: treat as a completed line
|
||||||
command[sizeof(command)-1] = '\r';
|
command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||||
|
command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "SensorMesh.h"
|
#include "SensorMesh.h"
|
||||||
|
#include <helpers/RoutingPolicy.h>
|
||||||
|
#include <helpers/sensors/LPPDataHelpers.h>
|
||||||
|
|
||||||
/* ------------------------------ Config -------------------------------- */
|
/* ------------------------------ Config -------------------------------- */
|
||||||
|
|
||||||
@@ -54,6 +56,10 @@
|
|||||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||||
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
||||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
#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
|
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||||
|
|
||||||
@@ -73,104 +79,70 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t getDataSize(uint8_t type) {
|
/* --------------------- Cayenne LPP helpers ----------------------------*/
|
||||||
switch (type) {
|
|
||||||
case LPP_GPS:
|
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
|
||||||
return 9;
|
uint8_t i = 0;
|
||||||
case LPP_POLYLINE:
|
|
||||||
return 8; // TODO: this is MINIMIUM
|
while (i + 2 < size) {
|
||||||
case LPP_GYROMETER:
|
// Get channel #
|
||||||
case LPP_ACCELEROMETER:
|
uint8_t ch = buf[i++];
|
||||||
return 6;
|
// Get data type
|
||||||
case LPP_GENERIC_SENSOR:
|
uint8_t t = buf[i++];
|
||||||
case LPP_FREQUENCY:
|
uint8_t sz = LPPData::getDataSize(t);
|
||||||
case LPP_DISTANCE:
|
|
||||||
case LPP_ENERGY:
|
if (ch == channel && t == type) {
|
||||||
case LPP_UNIXTIME:
|
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||||
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;
|
i += sz; // skip
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
return def_value; // not found
|
||||||
|
}
|
||||||
|
|
||||||
int sign = 1;
|
/* ------------------ end Cayenne LPP helpers ----------------------*/
|
||||||
if (is_signed) {
|
|
||||||
uint32_t bit = 1ul << ((size * 8) - 1);
|
bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
||||||
if ((value & bit) == bit) {
|
auto buf = telemetry.getBuffer();
|
||||||
value = (bit << 1) - value;
|
uint8_t size = telemetry.getSize();
|
||||||
sign = -1;
|
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) {
|
uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
||||||
// check sign
|
uint8_t perms = from->isAdmin() ? 0xFF : from->permissions;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
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
|
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
|
// query other sensors -- target specific
|
||||||
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
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);
|
// 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();
|
uint8_t tlen = telemetry.getSize();
|
||||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
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];
|
auto d = &data[i];
|
||||||
reply_data[ofs++] = d->_channel;
|
reply_data[ofs++] = d->_channel;
|
||||||
reply_data[ofs++] = d->_lpp_type;
|
reply_data[ofs++] = d->_lpp_type;
|
||||||
uint8_t sz = getDataSize(d->_lpp_type);
|
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
||||||
uint32_t mult = getMultiplier(d->_lpp_type);
|
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
|
||||||
bool is_signed = isSigned(d->_lpp_type);
|
bool is_signed = LPPData::isSigned(d->_lpp_type);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
||||||
}
|
}
|
||||||
return ofs;
|
return ofs;
|
||||||
}
|
}
|
||||||
@@ -234,6 +210,45 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
|||||||
return ofs;
|
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
|
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);
|
sendDirect(pkt, c->out_path, c->out_path_len);
|
||||||
} else {
|
} else {
|
||||||
unsigned long delay_millis = 0;
|
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);
|
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
|
||||||
@@ -330,6 +345,25 @@ int SensorMesh::getAGCResetInterval() const {
|
|||||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
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) {
|
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;
|
ClientInfo* client;
|
||||||
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
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 {
|
} else {
|
||||||
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
||||||
#if MESH_DEBUG
|
#if MESH_DEBUG
|
||||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]);
|
||||||
#endif
|
#endif
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -379,7 +413,41 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
|
|||||||
return 13; // reply length
|
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
|
while (*command == ' ') command++; // skip leading spaces
|
||||||
|
|
||||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
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) {
|
if (sp == NULL) {
|
||||||
strcpy(reply, "Err - bad params");
|
strcpy(reply, "Err - bad params");
|
||||||
} else {
|
} else {
|
||||||
|
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||||
|
uint8_t pubkey[PUB_KEY_SIZE];
|
||||||
|
|
||||||
*sp++ = 0; // replace space with null terminator
|
*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)) {
|
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||||
uint8_t perms = atoi(sp);
|
uint8_t perms = atoi(sp);
|
||||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
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) {
|
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)
|
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
||||||
uint32_t timestamp;
|
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
|
// 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,
|
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||||
} else {
|
} else {
|
||||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
||||||
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) {
|
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
|
||||||
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
||||||
mesh::Packet* ack = createAck(ack_hash);
|
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 {
|
} else {
|
||||||
uint32_t d = TXT_ACK_DELAY;
|
uint32_t d = TXT_ACK_DELAY;
|
||||||
if (getExtraAckTransmitCount() > 0) {
|
if (getExtraAckTransmitCount() > 0) {
|
||||||
@@ -531,7 +615,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
memcpy(×tamp, data, 4);
|
memcpy(×tamp, data, 4);
|
||||||
|
|
||||||
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
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
|
if (reply_len == 0) return; // invalid command
|
||||||
|
|
||||||
from->last_timestamp = timestamp;
|
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
|
// 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,
|
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_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 {
|
} else {
|
||||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
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);
|
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
|
||||||
} else {
|
} 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
|
// 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,
|
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
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 {
|
} else {
|
||||||
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
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_timestamp = sender_timestamp;
|
||||||
from->last_activity = getRTCClock()->getCurrentTime();
|
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];
|
uint8_t temp[166];
|
||||||
char *command = (char *) &data[5];
|
char *command = (char *) &data[5];
|
||||||
char *reply = (char *) &temp[5];
|
char *reply = (char *) &temp[5];
|
||||||
handleCommand(sender_timestamp, command, reply);
|
handleCommand(from, sender_timestamp, command, reply);
|
||||||
|
|
||||||
int text_len = strlen(reply);
|
int text_len = strlen(reply);
|
||||||
if (text_len > 0) {
|
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);
|
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
if (from->out_path_len == OUT_PATH_UNKNOWN) {
|
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 {
|
} else {
|
||||||
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
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)
|
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),
|
: 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),
|
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
||||||
telemetry(MAX_PACKET_PAYLOAD - 4)
|
telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||||
{
|
{
|
||||||
@@ -708,6 +792,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
|||||||
last_read_time = 0;
|
last_read_time = 0;
|
||||||
num_alert_tasks = 0;
|
num_alert_tasks = 0;
|
||||||
set_radio_at = revert_radio_at = 0;
|
set_radio_at = revert_radio_at = 0;
|
||||||
|
recv_pkt_region = NULL;
|
||||||
|
region_load_active = false;
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
_prefs.airtime_factor = 1.0;
|
_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.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
|
||||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
board.attachDynamicPrefs(_prefs.getCustom());
|
||||||
|
|
||||||
updateAdvertTimer();
|
updateAdvertTimer();
|
||||||
updateFloodAdvertTimer();
|
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
|
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) {
|
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
|
||||||
mesh::Packet* pkt = createSelfAdvert();
|
mesh::Packet* pkt = createSelfAdvert();
|
||||||
if (pkt) {
|
if (pkt) {
|
||||||
if (flood) {
|
if (flood) {
|
||||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||||
} else {
|
} else {
|
||||||
sendZeroHop(pkt, delay_millis);
|
sendZeroHop(pkt, delay_millis);
|
||||||
}
|
}
|
||||||
@@ -866,23 +984,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
||||||
auto buf = telemetry.getBuffer();
|
return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
||||||
@@ -902,7 +1004,7 @@ void SensorMesh::loop() {
|
|||||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||||
mesh::Packet* pkt = createSelfAdvert();
|
mesh::Packet* pkt = createSelfAdvert();
|
||||||
unsigned long delay_millis = 0;
|
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
|
updateFloodAdvertTimer(); // schedule next flood advert
|
||||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||||
@@ -931,6 +1033,39 @@ void SensorMesh::loop() {
|
|||||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||||
// query other sensors -- target specific
|
// query other sensors -- target specific
|
||||||
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
|
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();
|
onSensorDataRead();
|
||||||
|
|
||||||
@@ -973,7 +1108,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)) {
|
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||||
acl.save(_fs);
|
acl.save(_fs);
|
||||||
dirty_contacts_expiry = 0;
|
dirty_contacts_expiry = 0;
|
||||||
|
|||||||
@@ -34,11 +34,11 @@
|
|||||||
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
||||||
|
|
||||||
#ifndef FIRMWARE_BUILD_DATE
|
#ifndef FIRMWARE_BUILD_DATE
|
||||||
#define FIRMWARE_BUILD_DATE "9 Aug 2026"
|
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef FIRMWARE_VERSION
|
#ifndef FIRMWARE_VERSION
|
||||||
#define FIRMWARE_VERSION "v1.17.0"
|
#define FIRMWARE_VERSION "v1.17.1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define FIRMWARE_ROLE "sensor"
|
#define FIRMWARE_ROLE "sensor"
|
||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||||
void begin(FILESYSTEM* fs);
|
void begin(FILESYSTEM* fs);
|
||||||
void loop();
|
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
|
// CommonCLI callbacks
|
||||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||||
@@ -78,9 +78,15 @@ public:
|
|||||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||||
void clearStats() override { }
|
void clearStats() override { }
|
||||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) 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);
|
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:
|
protected:
|
||||||
// current telemetry data queries
|
// current telemetry data queries
|
||||||
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
|
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);
|
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
|
||||||
|
|
||||||
// alerts
|
// alerts
|
||||||
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
||||||
|
|
||||||
struct Trigger {
|
struct Trigger {
|
||||||
uint32_t timestamp;
|
uint32_t timestamp;
|
||||||
AlertPriority pri;
|
|
||||||
uint32_t expected_acks[4];
|
uint32_t expected_acks[4];
|
||||||
|
AlertPriority pri;
|
||||||
int8_t curr_contact_idx;
|
int8_t curr_contact_idx;
|
||||||
uint8_t attempt;
|
uint8_t attempt;
|
||||||
unsigned long send_expiry;
|
unsigned long send_expiry;
|
||||||
@@ -122,6 +128,7 @@ protected:
|
|||||||
int getInterferenceThreshold() const override;
|
int getInterferenceThreshold() const override;
|
||||||
bool getCADEnabled() const override;
|
bool getCADEnabled() const override;
|
||||||
int getAGCResetInterval() 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;
|
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||||
int searchPeersByHash(const uint8_t* hash) override;
|
int searchPeersByHash(const uint8_t* hash) override;
|
||||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) 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;
|
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 onControlDataRecv(mesh::Packet* packet) override;
|
||||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) 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);
|
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);
|
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
|
||||||
private:
|
private:
|
||||||
@@ -141,7 +149,9 @@ private:
|
|||||||
unsigned long dirty_contacts_expiry;
|
unsigned long dirty_contacts_expiry;
|
||||||
CayenneLPP telemetry;
|
CayenneLPP telemetry;
|
||||||
TransportKeyStore key_store;
|
TransportKeyStore key_store;
|
||||||
RegionMap region_map;
|
RegionMap region_map, temp_map;
|
||||||
|
RegionEntry* recv_pkt_region;
|
||||||
|
RegionEntry* load_stack[8];
|
||||||
TransportKey default_scope;
|
TransportKey default_scope;
|
||||||
uint32_t last_read_time;
|
uint32_t last_read_time;
|
||||||
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
||||||
@@ -152,9 +162,11 @@ private:
|
|||||||
float pending_bw;
|
float pending_bw;
|
||||||
uint8_t pending_sf;
|
uint8_t pending_sf;
|
||||||
uint8_t pending_cr;
|
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 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();
|
mesh::Packet* createSelfAdvert();
|
||||||
|
|
||||||
void sendAlert(const ClientInfo* c, Trigger* t);
|
void sendAlert(const ClientInfo* c, Trigger* t);
|
||||||
|
|||||||
@@ -122,6 +122,13 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
int len = strlen(command);
|
int len = strlen(command);
|
||||||
|
// `command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||||
|
// strlen() above can return >= sizeof(command) and the loop below would then
|
||||||
|
// index past the buffer, so clamp defensively.
|
||||||
|
if (len >= (int)sizeof(command)) {
|
||||||
|
command[0] = 0;
|
||||||
|
len = 0;
|
||||||
|
}
|
||||||
while (Serial.available() && len < sizeof(command)-1) {
|
while (Serial.available() && len < sizeof(command)-1) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c != '\n') {
|
if (c != '\n') {
|
||||||
@@ -130,14 +137,15 @@ void loop() {
|
|||||||
}
|
}
|
||||||
Serial.print(c);
|
Serial.print(c);
|
||||||
}
|
}
|
||||||
if (len == sizeof(command)-1) { // command buffer full
|
if (len == sizeof(command)-1) { // buffer full: treat as a completed line
|
||||||
command[sizeof(command)-1] = '\r';
|
command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||||
|
command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
command[len - 1] = 0; // replace newline with C string null terminator
|
command[len - 1] = 0; // replace newline with C string null terminator
|
||||||
char reply[160];
|
char reply[160];
|
||||||
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]) {
|
if (reply[0]) {
|
||||||
Serial.print(" -> "); Serial.println(reply);
|
Serial.print(" -> "); Serial.println(reply);
|
||||||
}
|
}
|
||||||
@@ -145,6 +153,8 @@ void loop() {
|
|||||||
command[0] = 0; // reset command buffer
|
command[0] = 0; // reset command buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
board.loop(); // let the board feed its watchdog, run periodic housekeeping
|
||||||
|
|
||||||
the_mesh.loop();
|
the_mesh.loop();
|
||||||
sensors.loop();
|
sensors.loop();
|
||||||
#ifdef DISPLAY_CLASS
|
#ifdef DISPLAY_CLASS
|
||||||
|
|||||||
+6
-2
@@ -85,7 +85,8 @@ platform_packages =
|
|||||||
; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect
|
; 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/1177
|
||||||
; https://github.com/meshcore-dev/MeshCore/pull/1295
|
; 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
|
extra_scripts = create-uf2.py
|
||||||
build_flags = ${arduino_base.build_flags}
|
build_flags = ${arduino_base.build_flags}
|
||||||
-D NRF52_PLATFORM
|
-D NRF52_PLATFORM
|
||||||
@@ -109,7 +110,9 @@ build_flags = ${arduino_base.build_flags}
|
|||||||
|
|
||||||
[stm32_base]
|
[stm32_base]
|
||||||
extends = arduino_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
|
extra_scripts = post:arch/stm32/build_hex.py
|
||||||
build_flags = ${arduino_base.build_flags}
|
build_flags = ${arduino_base.build_flags}
|
||||||
-D STM32_PLATFORM
|
-D STM32_PLATFORM
|
||||||
@@ -170,6 +173,7 @@ build_src_filter =
|
|||||||
+<../src/Utils.cpp>
|
+<../src/Utils.cpp>
|
||||||
+<../src/Packet.cpp>
|
+<../src/Packet.cpp>
|
||||||
+<../src/helpers/ConfigSerializer.cpp>
|
+<../src/helpers/ConfigSerializer.cpp>
|
||||||
|
+<../src/helpers/DynamicConfigSerializer.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
google/googletest @ 1.17.0
|
google/googletest @ 1.17.0
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -64,21 +64,22 @@ public:
|
|||||||
virtual uint8_t getStartupReason() const = 0;
|
virtual uint8_t getStartupReason() const = 0;
|
||||||
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
|
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
|
||||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||||
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
|
|
||||||
virtual bool canControlLoRaFemLna() const { return false; }
|
|
||||||
virtual bool isLoRaFemLnaEnabled() const { return false; }
|
|
||||||
// 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)
|
// Power management interface (boards with power management override these)
|
||||||
|
virtual bool isPwrMgtInitialised() const { return false; }
|
||||||
virtual bool isExternalPowered() { return false; }
|
virtual bool isExternalPowered() { return false; }
|
||||||
virtual uint16_t getBootVoltage() { return 0; }
|
virtual uint16_t getBootVoltage() { return 0; }
|
||||||
|
virtual bool getWakeLpcompSupported() const { return false; }
|
||||||
virtual uint32_t getResetReason() const { return 0; }
|
virtual uint32_t getResetReason() const { return 0; }
|
||||||
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
||||||
virtual uint8_t getShutdownReason() const { return 0; }
|
virtual uint8_t getShutdownReason() const { return 0; }
|
||||||
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
|
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 */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -203,6 +203,15 @@ bool Utils::isHexChar(char c) {
|
|||||||
return c == '0' || hexVal(c) > 0;
|
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) {
|
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
|
||||||
int len = strlen(src_hex);
|
int len = strlen(src_hex);
|
||||||
if (len != dest_size*2) return false; // incorrect length
|
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 int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
|
||||||
|
|
||||||
static bool isHexChar(char c);
|
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;
|
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) {
|
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
|
||||||
_name[0] = 0;
|
_name[0] = 0;
|
||||||
_lat = _lon = 0;
|
_lat = _lon = 0;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class AdvertDataParser {
|
|||||||
public:
|
public:
|
||||||
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
|
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
|
||||||
|
|
||||||
|
static bool isValidName(const char* name);
|
||||||
|
|
||||||
bool isValid() const { return _valid; }
|
bool isValid() const { return _valid; }
|
||||||
uint8_t getType() const { return _flags & 0x0F; }
|
uint8_t getType() const { return _flags & 0x0F; }
|
||||||
uint16_t getFeat1() const { return _extra1; }
|
uint16_t getFeat1() const { return _extra1; }
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#define TXT_ACK_DELAY 200
|
#define TXT_ACK_DELAY 200
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define CLI_REPLY_DELAY_MILLIS 600
|
||||||
|
|
||||||
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||||
sendFlood(pkt, delay_millis);
|
sendFlood(pkt, delay_millis);
|
||||||
}
|
}
|
||||||
@@ -227,8 +229,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
|||||||
ContactInfo& from = contacts[i];
|
ContactInfo& from = contacts[i];
|
||||||
|
|
||||||
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
|
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
|
||||||
uint32_t timestamp;
|
uint32_t sender_timestamp;
|
||||||
memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
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
|
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
|
||||||
|
|
||||||
// len can be > original length, but 'text' will be padded with zeroes
|
// len can be > original length, but 'text' will be padded with zeroes
|
||||||
@@ -236,7 +238,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
|||||||
|
|
||||||
if (flags == TXT_TYPE_PLAIN) {
|
if (flags == TXT_TYPE_PLAIN) {
|
||||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
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]);
|
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
|
uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||||
@@ -254,20 +256,43 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
|||||||
sendAckTo(from, ack_hash, 6);
|
sendAckTo(from, ack_hash, 6);
|
||||||
}
|
}
|
||||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
} 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
|
// 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()) {
|
onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know
|
||||||
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
|
// NOTE: no ack expected for CLI_COMMAND replies
|
||||||
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
|
|
||||||
if (path) sendFloodScoped(from, path);
|
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) {
|
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
|
||||||
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
|
if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
|
||||||
from.sync_since = timestamp;
|
from.sync_since = sender_timestamp;
|
||||||
}
|
}
|
||||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
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
|
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);
|
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE);
|
||||||
@@ -368,6 +393,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8
|
|||||||
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
|
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) {
|
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]) {
|
if (channels[i].channel.hash[0] == hash[0]) {
|
||||||
dest[n++] = channels[i].channel;
|
dest[n++] = channels[i].channel;
|
||||||
}
|
}
|
||||||
@@ -458,13 +489,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
|
|||||||
return rc;
|
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);
|
int text_len = strlen(text);
|
||||||
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
|
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
|
||||||
|
|
||||||
uint8_t temp[5+MAX_TEXT_LEN+1];
|
uint8_t temp[5+MAX_TEXT_LEN+1];
|
||||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
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);
|
memcpy(&temp[5], text, text_len + 1);
|
||||||
|
|
||||||
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
|
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
|
||||||
|
|||||||
@@ -114,6 +114,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 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 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 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 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 calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
|
||||||
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
|
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
|
||||||
@@ -156,7 +157,7 @@ public:
|
|||||||
mesh::Packet* createSelfAdvert(const char* name);
|
mesh::Packet* createSelfAdvert(const char* name);
|
||||||
mesh::Packet* createSelfAdvert(const char* name, double lat, double lon);
|
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 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 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);
|
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);
|
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
|
||||||
|
|||||||
@@ -28,13 +28,21 @@ struct ClientInfo {
|
|||||||
unsigned long ack_timeout;
|
unsigned long ack_timeout;
|
||||||
uint8_t push_failures;
|
uint8_t push_failures;
|
||||||
} room;
|
} 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;
|
} extra;
|
||||||
|
|
||||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifndef MAX_CLIENTS
|
#ifndef MAX_CLIENTS
|
||||||
#define MAX_CLIENTS 20
|
#define MAX_CLIENTS 32
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class ClientACL {
|
class ClientACL {
|
||||||
|
|||||||
+28
-193
@@ -164,6 +164,7 @@ void CommonCLI::savePrefs() {
|
|||||||
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
|
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
|
||||||
}
|
}
|
||||||
_callbacks->savePrefs();
|
_callbacks->savePrefs();
|
||||||
|
_prefs->clearDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
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) {
|
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) {
|
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
|
||||||
_board->powerOff(); // doesn't return
|
_board->powerOff(); // doesn't return
|
||||||
} else if (memcmp(command, "reboot", 6) == 0) {
|
} else if (memcmp(command, "reboot", 6) == 0) {
|
||||||
@@ -344,6 +355,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
|||||||
} else {
|
} else {
|
||||||
strcpy(reply, "gps provider not found");
|
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) {
|
} else if (memcmp(command, "gps setloc", 10) == 0) {
|
||||||
_prefs->node_lat = _sensors->node_lat;
|
_prefs->node_lat = _sensors->node_lat;
|
||||||
_prefs->node_lon = _sensors->node_lon;
|
_prefs->node_lon = _sensors->node_lon;
|
||||||
@@ -447,39 +472,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
|||||||
|
|
||||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||||
const char* config = &command[4];
|
const char* config = &command[4];
|
||||||
if (memcmp(config, "dutycycle ", 10) == 0) {
|
|
||||||
float dc = atof(&config[10]);
|
if (memcmp(config, "allow.read.only ", 16) == 0) {
|
||||||
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) {
|
|
||||||
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
|
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
|
||||||
savePrefs();
|
savePrefs();
|
||||||
strcpy(reply, "OK");
|
strcpy(reply, "OK");
|
||||||
@@ -532,77 +526,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
|
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
|
||||||
savePrefs();
|
savePrefs();
|
||||||
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
|
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
|
||||||
} 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) {
|
} else if (memcmp(config, "lat ", 4) == 0) {
|
||||||
_prefs->node_lat = atof(&config[4]);
|
_prefs->node_lat = atof(&config[4]);
|
||||||
savePrefs();
|
savePrefs();
|
||||||
@@ -611,24 +534,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
_prefs->node_lon = atof(&config[4]);
|
_prefs->node_lon = atof(&config[4]);
|
||||||
savePrefs();
|
savePrefs();
|
||||||
strcpy(reply, "OK");
|
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) {
|
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
|
||||||
uint8_t m = atoi(&config[19]);
|
uint8_t m = atoi(&config[19]);
|
||||||
if (m <= 64) {
|
if (m <= 64) {
|
||||||
@@ -656,15 +561,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
} else {
|
} else {
|
||||||
strcpy(reply, "Error, max 64");
|
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) {
|
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
||||||
config += 11;
|
config += 11;
|
||||||
char *dp = _prefs->owner_info;
|
char *dp = _prefs->owner_info;
|
||||||
@@ -675,16 +571,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
*dp = 0;
|
*dp = 0;
|
||||||
savePrefs();
|
savePrefs();
|
||||||
strcpy(reply, "OK");
|
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) {
|
} else if (memcmp(config, "loop.detect ", 12) == 0) {
|
||||||
config += 12;
|
config += 12;
|
||||||
uint8_t mode;
|
uint8_t mode;
|
||||||
@@ -705,11 +591,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
savePrefs();
|
savePrefs();
|
||||||
strcpy(reply, "OK");
|
strcpy(reply, "OK");
|
||||||
}
|
}
|
||||||
} else if (memcmp(config, "tx ", 3) == 0) {
|
|
||||||
_prefs->tx_power_dbm = atoi(&config[3]);
|
|
||||||
savePrefs();
|
|
||||||
_callbacks->setTxPower(_prefs->tx_power_dbm);
|
|
||||||
strcpy(reply, "OK");
|
|
||||||
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
|
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
|
||||||
_prefs->freq = atof(&config[5]);
|
_prefs->freq = atof(&config[5]);
|
||||||
savePrefs();
|
savePrefs();
|
||||||
@@ -806,22 +687,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
|
|
||||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||||
const char* config = &command[4];
|
const char* config = &command[4];
|
||||||
if (memcmp(config, "dutycycle", 9) == 0) {
|
if (memcmp(config, "allow.read.only", 15) == 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) {
|
|
||||||
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
||||||
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
||||||
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
||||||
@@ -842,37 +708,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
||||||
} else if (memcmp(config, "lon", 3) == 0) {
|
} else if (memcmp(config, "lon", 3) == 0) {
|
||||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
||||||
} 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) {
|
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
||||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
||||||
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
||||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
|
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
|
||||||
} else if (memcmp(config, "flood.max", 9) == 0) {
|
} else if (memcmp(config, "flood.max", 9) == 0) {
|
||||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
|
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) {
|
} else if (memcmp(config, "owner.info", 10) == 0) {
|
||||||
auto start = reply;
|
auto start = reply;
|
||||||
*reply++ = '>';
|
*reply++ = '>';
|
||||||
@@ -883,8 +724,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
sp++;
|
sp++;
|
||||||
}
|
}
|
||||||
*reply = 0; // set null terminator
|
*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) {
|
} else if (memcmp(config, "loop.detect", 11) == 0) {
|
||||||
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
|
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
|
||||||
strcpy(reply, "> off");
|
strcpy(reply, "> off");
|
||||||
@@ -895,10 +734,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||||||
} else {
|
} else {
|
||||||
strcpy(reply, "> strict");
|
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) {
|
} else if (memcmp(config, "public.key", 10) == 0) {
|
||||||
strcpy(reply, "> ");
|
strcpy(reply, "> ");
|
||||||
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
||||||
|
|||||||
+48
-2
@@ -6,6 +6,8 @@
|
|||||||
#include <helpers/ClientACL.h>
|
#include <helpers/ClientACL.h>
|
||||||
#include <helpers/RegionMap.h>
|
#include <helpers/RegionMap.h>
|
||||||
#include <helpers/ConfigSerializer.h>
|
#include <helpers/ConfigSerializer.h>
|
||||||
|
#include <helpers/CommonRadioPrefs.h>
|
||||||
|
#include <helpers/DynamicConfigSerializer.h>
|
||||||
|
|
||||||
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
|
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
|
||||||
#define WITH_BRIDGE
|
#define WITH_BRIDGE
|
||||||
@@ -72,7 +74,7 @@ public:
|
|||||||
uint8_t extra_sf[4];
|
uint8_t extra_sf[4];
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class RadioPrefs : public ConfigSerializer {
|
class RadioPrefs : public CommonRadioPrefs {
|
||||||
NodePrefs* _parent;
|
NodePrefs* _parent;
|
||||||
protected:
|
protected:
|
||||||
void structure() override {
|
void structure() override {
|
||||||
@@ -96,6 +98,41 @@ private:
|
|||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
|
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;
|
RadioPrefs radio;
|
||||||
|
|
||||||
@@ -166,6 +203,8 @@ private:
|
|||||||
};
|
};
|
||||||
RoomPrefs room;
|
RoomPrefs room;
|
||||||
|
|
||||||
|
DynamicConfigSerializer custom;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void structure() override {
|
void structure() override {
|
||||||
def("name", node_name, sizeof(node_name));
|
def("name", node_name, sizeof(node_name));
|
||||||
@@ -182,16 +221,23 @@ protected:
|
|||||||
def("repeat", repeat);
|
def("repeat", repeat);
|
||||||
def("room", room);
|
def("room", room);
|
||||||
def("power", power);
|
def("power", power);
|
||||||
|
def("custom", custom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
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;
|
node_name[0] = 0;
|
||||||
password[0] = 0;
|
password[0] = 0;
|
||||||
guest_password[0] = 0;
|
guest_password[0] = 0;
|
||||||
bridge_secret[0] = 0;
|
bridge_secret[0] = 0;
|
||||||
owner_info[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 {
|
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 "ConfigSerializer.h"
|
||||||
|
#include <stdlib.h> // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not)
|
||||||
|
|
||||||
bool ConfigSerializer::saveSerial(Stream& s) {
|
bool ConfigSerializer::saveSerial(Stream& s) {
|
||||||
Context context(&s, OP::WRITE);
|
Context context(&s, OP::WRITE);
|
||||||
@@ -23,7 +24,9 @@ static bool is_whitespace(char c) {
|
|||||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||||
}
|
}
|
||||||
static bool is_key_char(char c) {
|
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) {
|
static bool is_value_char(char c) {
|
||||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || 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:
|
case EXPECT_COMMA_OR_KEY:
|
||||||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
||||||
case EXPECT_KEY:
|
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 && 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 == 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; }
|
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 {
|
class ConfigSerializer {
|
||||||
bool _first;
|
bool _first;
|
||||||
int8_t _depth;
|
int8_t _depth;
|
||||||
|
bool _dirty = false;
|
||||||
|
|
||||||
|
protected:
|
||||||
enum OP { READ, WRITE };
|
enum OP { READ, WRITE };
|
||||||
|
|
||||||
class Context {
|
class Context {
|
||||||
@@ -38,13 +40,14 @@ class ConfigSerializer {
|
|||||||
const char* getToken() const { return rd_buf; }
|
const char* getToken() const { return rd_buf; }
|
||||||
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
|
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); }
|
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
|
||||||
|
const char* getKey(uint8_t depth) { return _keys[depth]; }
|
||||||
};
|
};
|
||||||
|
|
||||||
Context* _context = NULL;
|
Context* _context = NULL;
|
||||||
|
int8_t getDepth() const { return _depth; }
|
||||||
|
|
||||||
void writeComma();
|
void writeComma();
|
||||||
|
|
||||||
protected:
|
|
||||||
ConfigSerializer() { }
|
ConfigSerializer() { }
|
||||||
|
|
||||||
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
|
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;
|
virtual void structure() = 0;
|
||||||
|
|
||||||
|
void markDirty() { _dirty = true; }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool loadSerial(Stream& s);
|
bool loadSerial(Stream& s);
|
||||||
bool saveSerial(Stream& s);
|
bool saveSerial(Stream& s);
|
||||||
|
|
||||||
|
virtual bool isDirty() const { return _dirty; }
|
||||||
|
virtual void clearDirty() { _dirty = false; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ struct ContactInfo {
|
|||||||
return shared_secret;
|
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:
|
private:
|
||||||
mutable uint8_t shared_secret[PUB_KEY_SIZE];
|
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 "soc/rtc.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include <driver/rtc_io.h>
|
#include <driver/rtc_io.h>
|
||||||
|
#include <helpers/KeyValueStore.h>
|
||||||
|
|
||||||
class ESP32Board : public mesh::MainBoard {
|
class ESP32Board : public mesh::MainBoard {
|
||||||
protected:
|
protected:
|
||||||
@@ -51,6 +52,8 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||||
|
|
||||||
// Temperature from ESP32 MCU
|
// Temperature from ESP32 MCU
|
||||||
float getMCUTemperature() override {
|
float getMCUTemperature() override {
|
||||||
uint32_t raw = 0;
|
uint32_t raw = 0;
|
||||||
|
|||||||
@@ -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;
|
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NRF52Board::initPowerMgr() {
|
void NRF52Board::pwrmgtInit() {
|
||||||
|
if (pwrmgt_initialised) return;
|
||||||
// Copy early-captured register values
|
// Copy early-captured register values
|
||||||
reset_reason = g_nrf52_reset_reason;
|
reset_reason = g_nrf52_reset_reason;
|
||||||
shutdown_reason = g_nrf52_shutdown_reason;
|
shutdown_reason = g_nrf52_shutdown_reason;
|
||||||
@@ -74,6 +75,7 @@ void NRF52Board::initPowerMgr() {
|
|||||||
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
|
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
|
||||||
getResetReasonString(reset_reason), (unsigned long)reset_reason);
|
getResetReasonString(reset_reason), (unsigned long)reset_reason);
|
||||||
}
|
}
|
||||||
|
pwrmgt_initialised = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* NRF52Board::getResetReasonString(uint32_t reason) {
|
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) {
|
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
|
case SHUTDOWN_REASON_NONE: return "None";
|
||||||
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
|
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
|
||||||
case SHUTDOWN_REASON_USER: return "User Request";
|
case SHUTDOWN_REASON_USER: return "User Request";
|
||||||
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
|
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) {
|
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
|
||||||
initPowerMgr();
|
pwrmgtInit();
|
||||||
|
|
||||||
// Read boot voltage
|
// Read boot voltage
|
||||||
boot_voltage_mv = getBattMilliVolts();
|
boot_voltage_mv = getBattMilliVolts();
|
||||||
@@ -173,24 +176,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
|
|||||||
NVIC_SystemReset();
|
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
|
// LPCOMP is not managed by SoftDevice - direct register access required
|
||||||
// Halt and disable before reconfiguration
|
// Halt and disable before reconfiguration
|
||||||
NRF_LPCOMP->TASKS_STOP = 1;
|
NRF_LPCOMP->TASKS_STOP = 1;
|
||||||
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
|
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
|
||||||
|
|
||||||
// Select analog input (AIN0-7 maps to PSEL 0-7)
|
// 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)
|
// 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)
|
// Detect UP events (voltage rises above threshold for battery recovery)
|
||||||
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
|
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
|
// Clear stale events/interrupts before enabling wake
|
||||||
NRF_LPCOMP->EVENTS_READY = 0;
|
NRF_LPCOMP->EVENTS_READY = 0;
|
||||||
NRF_LPCOMP->EVENTS_DOWN = 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->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
|
||||||
NRF_LPCOMP->TASKS_START = 1;
|
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++) {
|
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
|
||||||
delayMicroseconds(50);
|
delayMicroseconds(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refsel == 7) {
|
if (lpcomp_refsel == 7) {
|
||||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN);
|
||||||
} else if (refsel <= 6) {
|
} else if (lpcomp_refsel <= 6) {
|
||||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1);
|
||||||
ain_channel, refsel + 1);
|
|
||||||
} else {
|
} else {
|
||||||
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
|
uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1);
|
||||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num);
|
||||||
ain_channel, ref_num);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configure VBUS (USB power) wake alongside LPCOMP
|
void NRF52Board::pwrmgtWakeArmVbus() {
|
||||||
uint8_t sd_enabled = 0;
|
uint8_t sd_enabled = 0;
|
||||||
sd_softdevice_is_enabled(&sd_enabled);
|
sd_softdevice_is_enabled(&sd_enabled);
|
||||||
if (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->EVENTS_USBDETECTED = 0;
|
||||||
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
|
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
|
||||||
}
|
}
|
||||||
|
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed");
|
||||||
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <MeshCore.h>
|
#include <MeshCore.h>
|
||||||
|
#include <helpers/KeyValueStore.h>
|
||||||
|
|
||||||
#if defined(NRF52_PLATFORM)
|
#if defined(NRF52_PLATFORM)
|
||||||
|
|
||||||
@@ -25,15 +26,15 @@ struct PowerMgtConfig {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
class NRF52Board : public mesh::MainBoard {
|
class NRF52Board : public mesh::MainBoard {
|
||||||
#ifdef NRF52_POWER_MANAGEMENT
|
private:
|
||||||
void initPowerMgr();
|
bool pwrmgt_initialised = false;
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint8_t startup_reason;
|
uint8_t startup_reason;
|
||||||
char *ota_name;
|
char *ota_name;
|
||||||
|
|
||||||
#ifdef NRF52_POWER_MANAGEMENT
|
#ifdef NRF52_POWER_MANAGEMENT
|
||||||
|
void pwrmgtInit();
|
||||||
uint32_t reset_reason; // RESETREAS register value
|
uint32_t reset_reason; // RESETREAS register value
|
||||||
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
|
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
|
||||||
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
|
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
|
||||||
@@ -41,6 +42,7 @@ protected:
|
|||||||
bool checkBootVoltage(const PowerMgtConfig* config);
|
bool checkBootVoltage(const PowerMgtConfig* config);
|
||||||
void enterSystemOff(uint8_t reason);
|
void enterSystemOff(uint8_t reason);
|
||||||
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
|
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
|
||||||
|
void pwrmgtWakeArmVbus();
|
||||||
virtual void initiateShutdown(uint8_t reason);
|
virtual void initiateShutdown(uint8_t reason);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -57,12 +59,18 @@ public:
|
|||||||
virtual void sleep(uint32_t secs) override;
|
virtual void sleep(uint32_t secs) override;
|
||||||
bool isExternalPowered() override;
|
bool isExternalPowered() override;
|
||||||
|
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||||
|
|
||||||
#ifdef NRF52_POWER_MANAGEMENT
|
#ifdef NRF52_POWER_MANAGEMENT
|
||||||
uint16_t getBootVoltage() override { return boot_voltage_mv; }
|
uint16_t getBootVoltage() override { return boot_voltage_mv; }
|
||||||
virtual uint32_t getResetReason() const override { return reset_reason; }
|
virtual uint32_t getResetReason() const override { return reset_reason; }
|
||||||
uint8_t getShutdownReason() const override { return shutdown_reason; }
|
uint8_t getShutdownReason() const override { return shutdown_reason; }
|
||||||
const char* getResetReasonString(uint32_t reason) override;
|
const char* getResetReasonString(uint32_t reason) override;
|
||||||
const char* getShutdownReasonString(uint8_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
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
virtual const char* getSettingValue(int i) const { return NULL; }
|
virtual const char* getSettingValue(int i) const { return NULL; }
|
||||||
virtual bool setSettingValue(const char* name, const char* value) { return false; }
|
virtual bool setSettingValue(const char* name, const char* value) { return false; }
|
||||||
virtual LocationProvider* getLocationProvider() { return NULL; }
|
virtual LocationProvider* getLocationProvider() { return NULL; }
|
||||||
|
virtual bool isGPSDetected() const { return false; }
|
||||||
|
|
||||||
// Helper functions to manage setting by keys (useful in many places ...)
|
// Helper functions to manage setting by keys (useful in many places ...)
|
||||||
const char* getSettingByKey(const char* key) {
|
const char* getSettingByKey(const char* key) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mesh::Packet* PacketQueue::removeByIdx(int i) {
|
mesh::Packet* PacketQueue::removeByIdx(int i) {
|
||||||
if (i >= _num) return NULL; // invalid index
|
if (i < 0 || i >= _num) return NULL; // invalid index
|
||||||
|
|
||||||
mesh::Packet* item = _table[i];
|
mesh::Packet* item = _table[i];
|
||||||
_num--;
|
_num--;
|
||||||
@@ -80,10 +80,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StaticPoolPacketManager::free(mesh::Packet* packet) {
|
void StaticPoolPacketManager::free(mesh::Packet* packet) {
|
||||||
unused.add(packet, 0, 0);
|
if (packet == NULL) return;
|
||||||
|
if (!unused.add(packet, 0, 0)) {
|
||||||
|
MESH_DEBUG_PRINTLN("free: unused queue full, possible double-free detected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
|
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
|
||||||
|
if (packet == NULL) return;
|
||||||
if (!send_queue.add(packet, priority, scheduled_for)) {
|
if (!send_queue.add(packet, priority, scheduled_for)) {
|
||||||
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
|
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
|
||||||
free(packet);
|
free(packet);
|
||||||
@@ -115,6 +119,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
|
void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
|
||||||
|
if (packet == NULL) return;
|
||||||
if (!rx_queue.add(packet, 0, scheduled_for)) {
|
if (!rx_queue.add(packet, 0, scheduled_for)) {
|
||||||
MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
|
MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
|
||||||
free(packet);
|
free(packet);
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ public:
|
|||||||
bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
|
bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
|
||||||
int count() const { return _num; }
|
int count() const { return _num; }
|
||||||
int countBefore(uint32_t now) const;
|
int countBefore(uint32_t now) const;
|
||||||
mesh::Packet* itemAt(int i) const { return _table[i]; }
|
mesh::Packet* itemAt(int i) const {
|
||||||
|
if (i < 0 || i >= _num) return NULL;
|
||||||
|
return _table[i];
|
||||||
|
}
|
||||||
mesh::Packet* removeByIdx(int i);
|
mesh::Packet* removeByIdx(int i);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ bool StrHelper::isBlank(const char* str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include <Arduino.h>
|
#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;
|
int32_t Long;
|
||||||
float Float;
|
float Float;
|
||||||
@@ -100,7 +105,7 @@ static void _ftoa(float f, char *p, int *status)
|
|||||||
*p++ = '-';
|
*p++ = '-';
|
||||||
if (int_part == 0)
|
if (int_part == 0)
|
||||||
*p++ = '0';
|
*p++ = '0';
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ltoa(int_part, p, 10);
|
ltoa(int_part, p, 10);
|
||||||
while (*p)
|
while (*p)
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#define TXT_TYPE_PLAIN 0 // a plain text message
|
#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_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_RESERVED 0x0000 // reserved for future use
|
||||||
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
|
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 {
|
|||||||
public:
|
public:
|
||||||
CustomLR1110(Module *mod) : LR1110(mod) { }
|
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 getPacketLength(bool update) override {
|
||||||
size_t len = LR1110::getPacketLength(update);
|
size_t len = LR1110::getPacketLength(update);
|
||||||
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
int16_t startReceive() override {
|
||||||
// include the PREAMBLE_DETECTED irq bit in reported flags
|
// 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() {
|
bool isReceiving() {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||||
|
#if defined(USE_LR2021)
|
||||||
|
idle();
|
||||||
|
#endif
|
||||||
_radio->setOutputPower(dbm);
|
_radio->setOutputPower(dbm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,4 +257,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t
|
|||||||
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
|
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
|
||||||
|
|
||||||
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
|
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ public:
|
|||||||
|
|
||||||
void random(uint8_t* dest, size_t sz) override {
|
void random(uint8_t* dest, size_t sz) override {
|
||||||
#ifdef USE_CC310_HW_CRYPTO
|
#ifdef USE_CC310_HW_CRYPTO
|
||||||
// CC310 TRNG is higher quality and environment-independent vs radio RSSI noise.
|
|
||||||
nRFCrypto.Random.generate(dest, (uint16_t)sz);
|
nRFCrypto.Random.generate(dest, (uint16_t)sz);
|
||||||
|
for (int i = 0; i < sz; i++) {
|
||||||
|
dest[i] ^= _radio->randomByte() ^ (::random(0, 256) & 0xFF); // combine with Radio's entropy
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
for (int i = 0; i < sz; i++) {
|
for (int i = 0; i < sz; i++) {
|
||||||
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
|
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -539,6 +539,8 @@ static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|||||||
// are compiled in. The sentinel at the end keeps the array
|
// are compiled in. The sentinel at the end keeps the array
|
||||||
// non-empty regardless of which sensors are enabled.
|
// non-empty regardless of which sensors are enabled.
|
||||||
//
|
//
|
||||||
|
// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both.
|
||||||
|
//
|
||||||
// Ordering here determines channel assignment at runtime:
|
// Ordering here determines channel assignment at runtime:
|
||||||
// the first detected+initialized sensor gets channel 2, the
|
// the first detected+initialized sensor gets channel 2, the
|
||||||
// next gets channel 3, and so on.
|
// next gets channel 3, and so on.
|
||||||
@@ -551,21 +553,27 @@ struct SensorDef {
|
|||||||
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
|
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76))
|
||||||
|
|
||||||
static const SensorDef SENSOR_TABLE[] = {
|
static const SensorDef SENSOR_TABLE[] = {
|
||||||
#if ENV_INCLUDE_AHTX0
|
#if ENV_INCLUDE_AHTX0
|
||||||
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
|
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
|
||||||
#endif
|
#endif
|
||||||
#ifdef ENV_INCLUDE_BME680
|
#ifdef ENV_INCLUDE_BME680
|
||||||
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
|
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
|
||||||
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 },
|
||||||
#endif
|
#endif
|
||||||
#if ENV_INCLUDE_BME680_BSEC
|
#if ENV_INCLUDE_BME680_BSEC
|
||||||
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
|
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
|
||||||
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
|
||||||
#endif
|
#endif
|
||||||
#if ENV_INCLUDE_BME280
|
#if ENV_INCLUDE_BME280
|
||||||
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
|
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
|
||||||
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 },
|
||||||
#endif
|
#endif
|
||||||
#if ENV_INCLUDE_BMP280
|
#if ENV_INCLUDE_BMP280
|
||||||
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
|
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
|
||||||
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 },
|
||||||
#endif
|
#endif
|
||||||
#if ENV_INCLUDE_SHTC3
|
#if ENV_INCLUDE_SHTC3
|
||||||
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
|
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
|
||||||
@@ -603,6 +611,8 @@ static const SensorDef SENSOR_TABLE[] = {
|
|||||||
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
|
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#undef TELEM_BOSCH_ALT_ADDR
|
||||||
|
|
||||||
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
|
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -640,6 +650,12 @@ bool EnvironmentSensorManager::begin() {
|
|||||||
_active_sensor_count = 0;
|
_active_sensor_count = 0;
|
||||||
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
|
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
|
||||||
const SensorDef& def = SENSOR_TABLE[i];
|
const SensorDef& def = SENSOR_TABLE[i];
|
||||||
|
// One static driver instance per type: an alternate address is a fallback, not a second device.
|
||||||
|
bool already_active = false;
|
||||||
|
for (int j = 0; j < _active_sensor_count; j++) {
|
||||||
|
if (_active_sensors[j].query == def.query) { already_active = true; break; }
|
||||||
|
}
|
||||||
|
if (already_active) continue;
|
||||||
if (!detected[def.address]) {
|
if (!detected[def.address]) {
|
||||||
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
|
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
|
||||||
continue;
|
continue;
|
||||||
@@ -650,6 +666,7 @@ bool EnvironmentSensorManager::begin() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
|
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
|
||||||
|
detected[def.address] = false; // consumed; later entries must not re-claim this device
|
||||||
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
|
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
|
||||||
_active_sensors[_active_sensor_count++] = { def.query, sub };
|
_active_sensors[_active_sensor_count++] = { def.query, sub };
|
||||||
}
|
}
|
||||||
@@ -814,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef RAK_3401
|
||||||
//set initial waking state
|
//set initial waking state
|
||||||
pinMode(ioPin,OUTPUT);
|
pinMode(ioPin,OUTPUT);
|
||||||
digitalWrite(ioPin,LOW);
|
digitalWrite(ioPin,LOW);
|
||||||
delay(500);
|
delay(500);
|
||||||
digitalWrite(ioPin,HIGH);
|
digitalWrite(ioPin,HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// give gps time to power up
|
||||||
delay(500);
|
delay(500);
|
||||||
|
|
||||||
//Try to init RAK12500 on I2C
|
//Try to init RAK12500 on I2C
|
||||||
@@ -854,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef RAK_3401
|
||||||
pinMode(ioPin, INPUT);
|
pinMode(ioPin, INPUT);
|
||||||
|
#endif
|
||||||
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -863,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
|||||||
void EnvironmentSensorManager::start_gps() {
|
void EnvironmentSensorManager::start_gps() {
|
||||||
gps_active = true;
|
gps_active = true;
|
||||||
#ifdef RAK_WISBLOCK_GPS
|
#ifdef RAK_WISBLOCK_GPS
|
||||||
|
#ifndef RAK_3401
|
||||||
pinMode(gpsResetPin, OUTPUT);
|
pinMode(gpsResetPin, OUTPUT);
|
||||||
digitalWrite(gpsResetPin, HIGH);
|
digitalWrite(gpsResetPin, HIGH);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -879,8 +904,10 @@ void EnvironmentSensorManager::start_gps() {
|
|||||||
void EnvironmentSensorManager::stop_gps() {
|
void EnvironmentSensorManager::stop_gps() {
|
||||||
gps_active = false;
|
gps_active = false;
|
||||||
#ifdef RAK_WISBLOCK_GPS
|
#ifdef RAK_WISBLOCK_GPS
|
||||||
|
#ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA
|
||||||
pinMode(gpsResetPin, OUTPUT);
|
pinMode(gpsResetPin, OUTPUT);
|
||||||
digitalWrite(gpsResetPin, LOW);
|
digitalWrite(gpsResetPin, LOW);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public:
|
|||||||
#if ENV_INCLUDE_GPS
|
#if ENV_INCLUDE_GPS
|
||||||
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
|
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
|
||||||
LocationProvider* getLocationProvider() { return _location; }
|
LocationProvider* getLocationProvider() { return _location; }
|
||||||
|
bool isGPSDetected() const override { return gps_detected; }
|
||||||
#else
|
#else
|
||||||
EnvironmentSensorManager(){};
|
EnvironmentSensorManager(){};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -63,12 +63,66 @@
|
|||||||
#define LPP_ERROR_OVERFLOW 1
|
#define LPP_ERROR_OVERFLOW 1
|
||||||
#define LPP_ERROR_UNKOWN_TYPE 2
|
#define LPP_ERROR_UNKOWN_TYPE 2
|
||||||
|
|
||||||
class LPPReader {
|
class LPPData {
|
||||||
const uint8_t* _buf;
|
public:
|
||||||
uint8_t _len;
|
static uint8_t getDataSize(uint8_t type) {
|
||||||
uint8_t _pos;
|
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;
|
uint32_t value = 0;
|
||||||
for (uint8_t i = 0; i < size; i++) {
|
for (uint8_t i = 0; i < size; i++) {
|
||||||
value = (value << 8) + buffer[i];
|
value = (value << 8) + buffer[i];
|
||||||
@@ -85,6 +139,36 @@ class LPPReader {
|
|||||||
return sign * ((float) value / multiplier);
|
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:
|
public:
|
||||||
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
||||||
|
|
||||||
@@ -103,72 +187,42 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool readGPS(float& lat, float& lon, float& alt) {
|
bool readGPS(float& lat, float& lon, float& alt) {
|
||||||
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||||
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||||
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readVoltage(float& voltage) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readCurrent(float& amps) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readPower(float& watts) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readTemperature(float& degrees_c) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readPressure(float& pa) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readRelativeHumidity(float& pct) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readAltitude(float& m) {
|
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;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipData(uint8_t type) {
|
void skipData(uint8_t type) {
|
||||||
switch (type) {
|
_pos += LPPData::getDataSize(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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +239,19 @@ class LPPWriter {
|
|||||||
public:
|
public:
|
||||||
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
|
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) {
|
bool writeVoltage(uint8_t channel, float voltage) {
|
||||||
if (_len + 4 <= _max_len) {
|
if (_len + 4 <= _max_len) {
|
||||||
_buf[_len++] = channel;
|
_buf[_len++] = channel;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Mesh.h"
|
#include "Mesh.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
class LocationProvider {
|
class LocationProvider {
|
||||||
@@ -22,4 +24,14 @@ public:
|
|||||||
virtual void stop() = 0;
|
virtual void stop() = 0;
|
||||||
virtual void loop() = 0;
|
virtual void loop() = 0;
|
||||||
virtual bool isEnabled() = 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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <MicroNMEA.h>
|
#include <MicroNMEA.h>
|
||||||
#include <RTClib.h>
|
#include <RTClib.h>
|
||||||
#include <helpers/RefCountedDigitalPin.h>
|
#include <helpers/RefCountedDigitalPin.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#ifndef GPS_EN
|
#ifndef GPS_EN
|
||||||
#ifdef PIN_GPS_EN
|
#ifdef PIN_GPS_EN
|
||||||
@@ -61,6 +62,31 @@ class MicroNMEALocationProvider : public LocationProvider {
|
|||||||
delay(250);
|
delay(250);
|
||||||
}
|
}
|
||||||
#endif
|
#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 :
|
public :
|
||||||
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
|
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
|
||||||
@@ -87,6 +113,7 @@ public :
|
|||||||
}
|
}
|
||||||
|
|
||||||
void begin() override {
|
void begin() override {
|
||||||
|
_begin_calls++;
|
||||||
claim();
|
claim();
|
||||||
if (_pin_en != -1) {
|
if (_pin_en != -1) {
|
||||||
digitalWrite(_pin_en, GPS_EN_ACTIVE);
|
digitalWrite(_pin_en, GPS_EN_ACTIVE);
|
||||||
@@ -110,6 +137,7 @@ public :
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stop() override {
|
void stop() override {
|
||||||
|
_stop_calls++;
|
||||||
if (_pin_en != -1) {
|
if (_pin_en != -1) {
|
||||||
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
|
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
|
||||||
}
|
}
|
||||||
@@ -153,10 +181,36 @@ public :
|
|||||||
|
|
||||||
while (_gps_serial->available()) {
|
while (_gps_serial->available()) {
|
||||||
char c = _gps_serial->read();
|
char c = _gps_serial->read();
|
||||||
|
_uart_bytes++;
|
||||||
|
_last_uart_ms = millis();
|
||||||
|
_uart_seen = true;
|
||||||
#ifdef GPS_NMEA_DEBUG
|
#ifdef GPS_NMEA_DEBUG
|
||||||
Serial.print(c);
|
Serial.print(c);
|
||||||
#endif
|
#endif
|
||||||
nmea.process(c);
|
bool parsed = nmea.process(c);
|
||||||
|
|
||||||
|
// 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;
|
if (!isValid()) time_valid = 0;
|
||||||
@@ -179,4 +233,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 <MeshCore.h>
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <helpers/KeyValueStore.h>
|
||||||
|
|
||||||
class STM32Board : public mesh::MainBoard {
|
class STM32Board : public mesh::MainBoard {
|
||||||
protected:
|
protected:
|
||||||
@@ -12,6 +13,8 @@ public:
|
|||||||
startup_reason = BD_STARTUP_NORMAL;
|
startup_reason = BD_STARTUP_NORMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||||
|
|
||||||
uint8_t getStartupReason() const override { return startup_reason; }
|
uint8_t getStartupReason() const override { return startup_reason; }
|
||||||
|
|
||||||
uint16_t getBattMilliVolts() override {
|
uint16_t getBattMilliVolts() override {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
|
|
||||||
virtual bool isOn() = 0;
|
virtual bool isOn() = 0;
|
||||||
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
|
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 turnOn() = 0;
|
||||||
virtual void turnOff() = 0;
|
virtual void turnOff() = 0;
|
||||||
virtual void clear() = 0;
|
virtual void clear() = 0;
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
SPIClass SPI1 = SPIClass(FSPI);
|
SPIClass SPI1 = SPIClass(FSPI);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef EPD_WASHING_MACHINE_CYCLES
|
||||||
|
#define EPD_WASHING_MACHINE_CYCLES 0
|
||||||
|
#endif
|
||||||
|
|
||||||
// Color scheme
|
// Color scheme
|
||||||
ColorVal UIColor::window_bkg = GxEPD_WHITE;
|
ColorVal UIColor::window_bkg = GxEPD_WHITE;
|
||||||
ColorVal UIColor::title_bkg = GxEPD_WHITE;
|
ColorVal UIColor::title_bkg = GxEPD_WHITE;
|
||||||
@@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE;
|
|||||||
ColorVal UIColor::popup_txt = GxEPD_BLACK;
|
ColorVal UIColor::popup_txt = GxEPD_BLACK;
|
||||||
ColorVal UIColor::corp_blue = GxEPD_BLACK;
|
ColorVal UIColor::corp_blue = GxEPD_BLACK;
|
||||||
|
|
||||||
|
|
||||||
bool GxEPDDisplay::begin() {
|
bool GxEPDDisplay::begin() {
|
||||||
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||||
#ifdef ESP32
|
#ifdef ESP32
|
||||||
@@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() {
|
|||||||
display.init(115200, true, 2, false);
|
display.init(115200, true, 2, false);
|
||||||
display.setRotation(DISPLAY_ROTATION);
|
display.setRotation(DISPLAY_ROTATION);
|
||||||
setTextSize(1); // Default to size 1
|
setTextSize(1); // Default to size 1
|
||||||
display.setPartialWindow(0, 0, display.width(), display.height());
|
|
||||||
|
|
||||||
display.fillScreen(GxEPD_WHITE);
|
display.setFullWindow();
|
||||||
display.display(true);
|
|
||||||
|
for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) {
|
||||||
|
display.fillScreen(GxEPD_BLACK);
|
||||||
|
display.display(false);
|
||||||
|
delay(2000);
|
||||||
|
display.fillScreen(GxEPD_WHITE);
|
||||||
|
display.display(false);
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setPartialWindow(0, 0, display.width(), display.height());
|
||||||
|
resetPartialRefreshCounter();
|
||||||
|
|
||||||
#if DISP_BACKLIGHT
|
#if DISP_BACKLIGHT
|
||||||
digitalWrite(DISP_BACKLIGHT, LOW);
|
digitalWrite(DISP_BACKLIGHT, LOW);
|
||||||
pinMode(DISP_BACKLIGHT, OUTPUT);
|
pinMode(DISP_BACKLIGHT, OUTPUT);
|
||||||
#endif
|
#endif
|
||||||
_init = true;
|
_init = true;
|
||||||
|
_isOn = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +70,9 @@ void GxEPDDisplay::turnOn() {
|
|||||||
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
||||||
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
|
||||||
#endif
|
#endif
|
||||||
_isOn = true;
|
if (!_isOn) {
|
||||||
|
_isOn = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GxEPDDisplay::turnOff() {
|
void GxEPDDisplay::turnOff() {
|
||||||
@@ -65,6 +82,13 @@ void GxEPDDisplay::turnOff() {
|
|||||||
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
|
||||||
#endif
|
#endif
|
||||||
_isOn = false;
|
_isOn = false;
|
||||||
|
// do full refresh before powering off to clear screen
|
||||||
|
// no full refresh needed at wakeup
|
||||||
|
display.clearScreen(0xFF); // Clears microcontroller side RAM
|
||||||
|
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
|
||||||
|
resetPartialRefreshCounter();
|
||||||
|
last_display_crc_value=0;
|
||||||
|
display.hibernate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GxEPDDisplay::clear() {
|
void GxEPDDisplay::clear() {
|
||||||
@@ -77,6 +101,16 @@ void GxEPDDisplay::startFrame(ColorVal bkg) {
|
|||||||
display.fillScreen(bkg);
|
display.fillScreen(bkg);
|
||||||
display.setTextColor(_curr_color = UIColor::primary_txt);
|
display.setTextColor(_curr_color = UIColor::primary_txt);
|
||||||
display_crc.reset();
|
display_crc.reset();
|
||||||
|
if (_cycles_before_full_refresh != 0) {
|
||||||
|
display.setPartialWindow(0, 0, display.width(), display.height());
|
||||||
|
} else {
|
||||||
|
// forces a full wipe of the screen ...
|
||||||
|
display.clearScreen(0xFF); // Clears microcontroller side RAM
|
||||||
|
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
|
||||||
|
// we'll need a partial refresh after that (whatever crc value is)
|
||||||
|
last_display_crc_value = 0;
|
||||||
|
resetPartialRefreshCounter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GxEPDDisplay::setTextSize(int sz) {
|
void GxEPDDisplay::setTextSize(int sz) {
|
||||||
@@ -178,9 +212,13 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GxEPDDisplay::endFrame() {
|
void GxEPDDisplay::endFrame() {
|
||||||
|
if (_isOn == false) return;
|
||||||
uint32_t crc = display_crc.finalize();
|
uint32_t crc = display_crc.finalize();
|
||||||
if (crc != last_display_crc_value) {
|
if (crc != last_display_crc_value) {
|
||||||
display.display(true);
|
display.display(true);
|
||||||
last_display_crc_value = crc;
|
if (_cycles_before_full_refresh > 0) {
|
||||||
|
_cycles_before_full_refresh--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
last_display_crc_value = crc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
|
|
||||||
#include "DisplayDriver.h"
|
#include "DisplayDriver.h"
|
||||||
|
|
||||||
|
#ifndef EINK_MAX_PARTIAL_REFRESH
|
||||||
|
// 60 to prevent ghosting when refreshing every minute ...
|
||||||
|
// set to -1 to disable the counter
|
||||||
|
#define EINK_MAX_PARTIAL_REFRESH -1
|
||||||
|
#endif
|
||||||
|
|
||||||
class GxEPDDisplay : public DisplayDriver {
|
class GxEPDDisplay : public DisplayDriver {
|
||||||
|
|
||||||
#if defined(EINK_DISPLAY_MODEL)
|
#if defined(EINK_DISPLAY_MODEL)
|
||||||
@@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver {
|
|||||||
uint16_t _curr_color;
|
uint16_t _curr_color;
|
||||||
CRC32 display_crc;
|
CRC32 display_crc;
|
||||||
int last_display_crc_value = 0;
|
int last_display_crc_value = 0;
|
||||||
|
int _cycles_before_full_refresh;
|
||||||
|
int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after
|
||||||
|
|
||||||
public:
|
public:
|
||||||
#if defined(EINK_DISPLAY_MODEL)
|
#if defined(EINK_DISPLAY_MODEL)
|
||||||
@@ -48,6 +56,8 @@ public:
|
|||||||
|
|
||||||
bool isOn() override { return _isOn; }
|
bool isOn() override { return _isOn; }
|
||||||
bool isEink() override { return true; }
|
bool isEink() override { return true; }
|
||||||
|
void forceFullRefresh() override { _cycles_before_full_refresh = 0; };
|
||||||
|
void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; };
|
||||||
void turnOn() override;
|
void turnOn() override;
|
||||||
void turnOff() override;
|
void turnOff() override;
|
||||||
void clear() override;
|
void clear() override;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ class MomentaryButton {
|
|||||||
public:
|
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=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
|
||||||
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
|
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
|
||||||
void begin();
|
virtual void begin();
|
||||||
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
|
virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
|
||||||
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
|
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
|
||||||
uint8_t getPin() { return _pin; }
|
uint8_t getPin() { return _pin; }
|
||||||
bool isPressed() const;
|
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;
|
||||||
|
};
|
||||||
@@ -50,6 +50,8 @@ public:
|
|||||||
const std::string& text() const { return _text; }
|
const std::string& text() const { return _text; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
// Re-enable test once we can SET fem_ values in companion
|
||||||
TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {
|
TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {
|
||||||
NodePrefs saved;
|
NodePrefs saved;
|
||||||
saved.rx_boosted_gain = 0;
|
saved.rx_boosted_gain = 0;
|
||||||
@@ -73,6 +75,7 @@ TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {
|
|||||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
||||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include "helpers/ConfigSerializer.h"
|
#include "helpers/ConfigSerializer.h"
|
||||||
|
#include "helpers/DynamicConfigSerializer.h"
|
||||||
class NativeFileSystem {
|
|
||||||
public:
|
|
||||||
void mkdir(const char*) { }
|
|
||||||
};
|
|
||||||
#define FILESYSTEM NativeFileSystem
|
|
||||||
#include "helpers/CommonCLI.h"
|
|
||||||
#undef FILESYSTEM
|
|
||||||
|
|
||||||
#define TEST_INT_S "56"
|
#define TEST_INT_S "56"
|
||||||
#define TEST_INT 56
|
#define TEST_INT 56
|
||||||
@@ -192,28 +185,131 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
|
|||||||
EXPECT_TRUE(match);
|
EXPECT_TRUE(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
class TestNested : public ConfigSerializer {
|
||||||
NodePrefs saved;
|
class Inner : public ConfigSerializer {
|
||||||
saved.radio_fem_rxgain = 0;
|
protected:
|
||||||
saved.radio_fem_txgain = 1;
|
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;
|
TEST(ConfigSerializer, LoadSerial_EmptyObject) {
|
||||||
ASSERT_TRUE(saved.saveSerial(output));
|
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());
|
bool success = data.loadSerial(s);
|
||||||
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
|
EXPECT_TRUE(success);
|
||||||
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
|
|
||||||
|
|
||||||
MockInputStream input(serialised.c_str());
|
EXPECT_EQ(TEST_INT, data.age);
|
||||||
NodePrefs loaded;
|
bool match = strcmp("Scott", data.name) == 0;
|
||||||
loaded.radio_fem_rxgain = 1;
|
EXPECT_TRUE(match); // properties after an empty object must still load
|
||||||
loaded.radio_fem_txgain = 0;
|
|
||||||
|
|
||||||
ASSERT_TRUE(loaded.loadSerial(input));
|
|
||||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
|
||||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ───────────────────────────────────────────────────────
|
// ── main ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[Heltec_ct62]
|
[Heltec_ct62]
|
||||||
extends = esp32_base
|
extends = esp32_base
|
||||||
board = esp32-c3-devkitm-1
|
board = esp32-c3-devkitm-1
|
||||||
|
board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
|
||||||
build_flags =
|
build_flags =
|
||||||
${esp32_base.build_flags}
|
${esp32_base.build_flags}
|
||||||
-I variants/heltec_ct62
|
-I variants/heltec_ct62
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ extends = Heltec_RC32
|
|||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_RC32.build_flags}
|
${Heltec_RC32.build_flags}
|
||||||
-I examples/companion_radio/ui-new
|
-I examples/companion_radio/ui-new
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D DISPLAY_CLASS=NullDisplayDriver
|
-D DISPLAY_CLASS=NullDisplayDriver
|
||||||
-D MAX_CONTACTS=350
|
-D MAX_CONTACTS=350
|
||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
@@ -185,6 +186,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter}
|
|||||||
+<helpers/ui/buzzer.cpp>
|
+<helpers/ui/buzzer.cpp>
|
||||||
+<helpers/ui/NullDisplayDriver.cpp>
|
+<helpers/ui/NullDisplayDriver.cpp>
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
+<../examples/companion_radio/ui-new/*.cpp>
|
+<../examples/companion_radio/ui-new/*.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
@@ -314,6 +316,7 @@ extends = Heltec_RC32_with_display
|
|||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_RC32_with_display.build_flags}
|
${Heltec_RC32_with_display.build_flags}
|
||||||
-I examples/companion_radio/ui-new
|
-I examples/companion_radio/ui-new
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D UI_HAS_ROTARY_INPUT
|
-D UI_HAS_ROTARY_INPUT
|
||||||
-D MAX_CONTACTS=350
|
-D MAX_CONTACTS=350
|
||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
@@ -324,6 +327,7 @@ build_flags =
|
|||||||
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
|
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
|
||||||
+<helpers/ui/buzzer.cpp>
|
+<helpers/ui/buzzer.cpp>
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
+<../examples/companion_radio/ui-new/*.cpp>
|
+<../examples/companion_radio/ui-new/*.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
|||||||
@@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool T096Board::canControlLoRaFemLna() const {
|
|
||||||
return loRaFEMControl.isLnaCanControl();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool T096Board::isLoRaFemLnaEnabled() const {
|
bool T096Board::isLoRaFemLnaEnabled() const {
|
||||||
return loRaFEMControl.isLNAEnabled();
|
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 <Arduino.h>
|
||||||
#include <helpers/NRF52Board.h>
|
#include <helpers/NRF52Board.h>
|
||||||
#include <helpers/RefCountedDigitalPin.h>
|
#include <helpers/RefCountedDigitalPin.h>
|
||||||
|
#include <helpers/KeyValueStore.h>
|
||||||
#include "LoRaFEMControl.h"
|
#include "LoRaFEMControl.h"
|
||||||
|
|
||||||
class T096Board : public NRF52BoardDCDC {
|
class T096Board : public NRF52BoardDCDC {
|
||||||
|
KeyValueStore* _prefs = NULL;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#ifdef NRF52_POWER_MANAGEMENT
|
#ifdef NRF52_POWER_MANAGEMENT
|
||||||
void initiateShutdown(uint8_t reason) override;
|
void initiateShutdown(uint8_t reason) override;
|
||||||
#endif
|
#endif
|
||||||
void variant_shutdown();
|
void variant_shutdown();
|
||||||
|
|
||||||
|
bool setLoRaFemLnaEnabled(bool enable);
|
||||||
|
bool isLoRaFemLnaEnabled() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RefCountedDigitalPin periph_power;
|
RefCountedDigitalPin periph_power;
|
||||||
LoRaFEMControl loRaFEMControl;
|
LoRaFEMControl loRaFEMControl;
|
||||||
|
|
||||||
T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {}
|
T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {}
|
||||||
void begin();
|
void begin();
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||||
|
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||||
|
|
||||||
void onBeforeTransmit(void) override;
|
void onBeforeTransmit(void) override;
|
||||||
void onAfterTransmit(void) override;
|
void onAfterTransmit(void) override;
|
||||||
uint16_t getBattMilliVolts() override;
|
uint16_t getBattMilliVolts() override;
|
||||||
const char* getManufacturerName() const override ;
|
const char* getManufacturerName() const override ;
|
||||||
void powerOff() override;
|
void powerOff() override;
|
||||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
|
||||||
bool canControlLoRaFemLna() const override;
|
|
||||||
bool isLoRaFemLnaEnabled() const override;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,10 +72,50 @@ void HeltecTrackerV2Board::begin() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HeltecTrackerV2Board::canControlLoRaFemLna() const {
|
|
||||||
return loRaFEMControl.isLnaCanControl();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
|
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
|
||||||
return loRaFEMControl.isLNAEnabled();
|
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"
|
#include "LoRaFEMControl.h"
|
||||||
|
|
||||||
class HeltecTrackerV2Board : public ESP32Board {
|
class HeltecTrackerV2Board : public ESP32Board {
|
||||||
|
KeyValueStore* _prefs = NULL;
|
||||||
|
|
||||||
|
bool setLoRaFemLnaEnabled(bool enable);
|
||||||
|
bool isLoRaFemLnaEnabled() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RefCountedDigitalPin periph_power;
|
RefCountedDigitalPin periph_power;
|
||||||
@@ -14,13 +18,13 @@ public:
|
|||||||
HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||||
|
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||||
|
|
||||||
void onBeforeTransmit(void) override;
|
void onBeforeTransmit(void) override;
|
||||||
void onAfterTransmit(void) override;
|
void onAfterTransmit(void) override;
|
||||||
void powerOff() override;
|
void powerOff() override;
|
||||||
uint16_t getBattMilliVolts() override;
|
uint16_t getBattMilliVolts() override;
|
||||||
const char* getManufacturerName() const override ;
|
const char* getManufacturerName() const override ;
|
||||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
|
||||||
bool canControlLoRaFemLna() const override;
|
|
||||||
bool isLoRaFemLnaEnabled() const override;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ extends = Heltec_tracker_v2
|
|||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_tracker_v2.build_flags}
|
${Heltec_tracker_v2.build_flags}
|
||||||
-I examples/companion_radio/ui-new
|
-I examples/companion_radio/ui-new
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D MAX_CONTACTS=350
|
-D MAX_CONTACTS=350
|
||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
-D DISPLAY_CLASS=ST7735Display
|
-D DISPLAY_CLASS=ST7735Display
|
||||||
@@ -195,6 +196,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter}
|
|||||||
+<helpers/ui/ST7735Display.cpp>
|
+<helpers/ui/ST7735Display.cpp>
|
||||||
+<helpers/ui/MomentaryButton.cpp>
|
+<helpers/ui/MomentaryButton.cpp>
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
+<../examples/companion_radio/ui-new/*.cpp>
|
+<../examples/companion_radio/ui-new/*.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ extends = Heltec_lora32_v2
|
|||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_lora32_v2.build_flags}
|
${Heltec_lora32_v2.build_flags}
|
||||||
-I examples/companion_radio/ui-new
|
-I examples/companion_radio/ui-new
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D DISPLAY_CLASS=SSD1306Display
|
-D DISPLAY_CLASS=SSD1306Display
|
||||||
-D MAX_CONTACTS=160
|
-D MAX_CONTACTS=160
|
||||||
-D MAX_GROUP_CHANNELS=8
|
-D MAX_GROUP_CHANNELS=8
|
||||||
@@ -189,6 +190,7 @@ build_flags =
|
|||||||
; -D MESH_DEBUG=1
|
; -D MESH_DEBUG=1
|
||||||
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
|
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<helpers/ui/SSD1306Display.cpp>
|
+<helpers/ui/SSD1306Display.cpp>
|
||||||
+<helpers/ui/MomentaryButton.cpp>
|
+<helpers/ui/MomentaryButton.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ extends = Heltec_lora32_v3
|
|||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_lora32_v3.build_flags}
|
${Heltec_lora32_v3.build_flags}
|
||||||
-I examples/companion_radio/ui-new
|
-I examples/companion_radio/ui-new
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D MAX_CONTACTS=350
|
-D MAX_CONTACTS=350
|
||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
-D DISPLAY_CLASS=SSD1306Display
|
-D DISPLAY_CLASS=SSD1306Display
|
||||||
@@ -198,6 +199,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
|||||||
+<helpers/ui/SSD1306Display.cpp>
|
+<helpers/ui/SSD1306Display.cpp>
|
||||||
+<helpers/ui/MomentaryButton.cpp>
|
+<helpers/ui/MomentaryButton.cpp>
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
+<../examples/companion_radio/ui-new/*.cpp>
|
+<../examples/companion_radio/ui-new/*.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
@@ -340,6 +342,7 @@ lib_deps =
|
|||||||
extends = Heltec_lora32_v3
|
extends = Heltec_lora32_v3
|
||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_lora32_v3.build_flags}
|
${Heltec_lora32_v3.build_flags}
|
||||||
|
-D ENABLE_WIFI_INTERFACE
|
||||||
-D MAX_CONTACTS=350
|
-D MAX_CONTACTS=350
|
||||||
-D MAX_GROUP_CHANNELS=40
|
-D MAX_GROUP_CHANNELS=40
|
||||||
-D WIFI_DEBUG_LOGGING=1
|
-D WIFI_DEBUG_LOGGING=1
|
||||||
@@ -350,6 +353,7 @@ build_flags =
|
|||||||
; -D MESH_DEBUG=1
|
; -D MESH_DEBUG=1
|
||||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
|
+<helpers/wifi/*.cpp>
|
||||||
+<../examples/companion_radio/*.cpp>
|
+<../examples/companion_radio/*.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${Heltec_lora32_v3.lib_deps}
|
${Heltec_lora32_v3.lib_deps}
|
||||||
|
|||||||
@@ -73,10 +73,50 @@ void HeltecV4Board::begin() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HeltecV4Board::canControlLoRaFemLna() const {
|
|
||||||
return loRaFEMControl.isLnaCanControl();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
|
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
|
||||||
return loRaFEMControl.isLNAEnabled();
|
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
|
#endif
|
||||||
|
|
||||||
class HeltecV4Board : public ESP32Board {
|
class HeltecV4Board : public ESP32Board {
|
||||||
|
KeyValueStore* _prefs = NULL;
|
||||||
|
|
||||||
|
bool setLoRaFemLnaEnabled(bool enable);
|
||||||
|
bool isLoRaFemLnaEnabled() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float adc_mult = ADC_MULTIPLIER;
|
float adc_mult = ADC_MULTIPLIER;
|
||||||
@@ -20,12 +24,12 @@ public:
|
|||||||
HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||||
|
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||||
|
|
||||||
void onBeforeTransmit(void) override;
|
void onBeforeTransmit(void) override;
|
||||||
void onAfterTransmit(void) override;
|
void onAfterTransmit(void) override;
|
||||||
void powerOff() override;
|
void powerOff() override;
|
||||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
|
||||||
bool canControlLoRaFemLna() const override;
|
|
||||||
bool isLoRaFemLnaEnabled() const override;
|
|
||||||
uint16_t getBattMilliVolts() override;
|
uint16_t getBattMilliVolts() override;
|
||||||
bool setAdcMultiplier(float multiplier) override {
|
bool setAdcMultiplier(float multiplier) override {
|
||||||
if (multiplier == 0.0f) {
|
if (multiplier == 0.0f) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user