diff --git a/README.md b/README.md index af56aaf33..6cc45d7f4 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,6 @@ There are a number of fairly major features in the pipeline, with no particular ## 📞 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). - Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community. diff --git a/boards/muzi_base.json b/boards/muzi_base.json new file mode 100644 index 000000000..1b2cf7df1 --- /dev/null +++ b/boards/muzi_base.json @@ -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" +} diff --git a/boards/thinknode_m4.json b/boards/thinknode_m4.json new file mode 100644 index 000000000..babc3e52f --- /dev/null +++ b/boards/thinknode_m4.json @@ -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" +} \ No newline at end of file diff --git a/boards/thinknode_m8.json b/boards/thinknode_m8.json new file mode 100644 index 000000000..acd7f89c0 --- /dev/null +++ b/boards/thinknode_m8.json @@ -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" +} diff --git a/build_as_lib.py b/build_as_lib.py index d8e95378e..fbc7c15a7 100644 --- a/build_as_lib.py +++ b/build_as_lib.py @@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []): src_filter.append("+") elif item == "ESP32": src_filter.append("+") + src_filter.append("+") elif item == "NRF52_PLATFORM": src_filter.append("+") elif item == "RP2040_PLATFORM": src_filter.append("+") + src_filter.append("+") # DISPLAY HANDLING elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS": diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929f..9fde4d436 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -211,7 +211,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` -**Default:** `869.525,250,11,5` +**Default:** `869.618,62.5,8,5` **Note:** Requires reboot to apply @@ -256,7 +256,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `frequency`: Frequency in MHz -**Default:** `869.525` +**Default:** `869.618` **Note:** Requires reboot to apply **Serial Only:** `set freq ` @@ -537,7 +537,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.2` +**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor) **Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. @@ -628,7 +628,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set agc.reset.interval ` **Parameters:** -- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable. +- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable. **Default:** `0.0` @@ -654,7 +654,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `hours`: Interval in hours (3-168) -**Default:** `12` (Repeater) - `0` (Sensor) +**Default:** `47` (Repeater, Room Server) - `0`, disabled (Sensor) --- @@ -666,7 +666,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240) -**Default:** `0` +**Default:** `2` on a factory-fresh node, then `0` (disabled) once configured. + +**Note:** A new install ships with a 2 minute zero-hop advert interval. Saving +any setting resets an interval below the 60 minute minimum to `0`, on the +assumption that the node has now been deliberately configured. To keep zero-hop +adverts running, set an explicit value in the 60-240 range. --- diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 8c6b84b97..12d81bc49 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -206,7 +206,7 @@ Byte 1: 0x03 **Command Format**: ``` Byte 0: 0x1F -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) ``` **Example** (get channel 1): @@ -225,7 +225,7 @@ Byte 1: Channel Index (0-7) **Command Format**: ``` Byte 0: 0x20 -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded) Bytes 34-49: Secret (16 bytes) ``` @@ -233,8 +233,12 @@ Bytes 34-49: Secret (16 bytes) **Total Length**: 50 bytes **Channel Index**: -- Index 0: Reserved for public channels (no secret) -- Indices 1-7: Available for private channels +- Valid range is `0` to `max_channels - 1`. Read `max_channels` from byte 3 of + the `PACKET_DEVICE_INFO` response; do not assume 8. Builds ship with + `MAX_GROUP_CHANNELS` set to 1, 8 or 40 depending on the board. +- Index 0: pre-populated by the firmware with the built-in "Public" channel. + It can be overwritten like any other slot. +- An out-of-range index is rejected with `PACKET_ERROR` / `ERR_CODE_NOT_FOUND`. **Channel Name**: - UTF-8 encoded @@ -242,8 +246,18 @@ Bytes 34-49: Secret (16 bytes) - Padded with null bytes (0x00) if shorter **Secret Field** (16 bytes): -- For **private channels**: 16-byte secret -- For **public channels**: All zeros (0x00) +- Always a real 16-byte key; the firmware derives the channel hash from + `SHA256(secret)` (`BaseChatMesh.cpp:936-942`). There is no "no secret" form. +- Do **not** write an all-zero secret. `SHA256` over 16 zero bytes is a fixed, + globally identical value, so a zero-secret channel is a well-known channel + with an all-zero AES key that any node can read — not a private one. The + firmware skips *unnamed* slots when matching inbound group traffic for + exactly this reason (`BaseChatMesh.cpp:392-401`), but a slot that has been + given a name and a zero secret is not skipped, and will absorb null-key + group traffic from any node on the mesh. +- The built-in public channel uses the well-known key + `izOH6cXN6mrJ5e26oRXNcg==` (base64), i.e. + `8b3387e9c5cdea6ac9e5edbaa115cd72` in hex. **Example** (create channel "YourChannelName" at index 1 with secret): ``` @@ -265,7 +279,7 @@ Bytes 34-49: Secret (16 bytes) ``` Byte 0: 0x03 Byte 1: 0x00 -Byte 2: Channel Index (0-7) +Byte 2: Channel Index (0 .. max_channels-1) Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds) Bytes 7+: Message Text (UTF-8, variable length) ``` @@ -288,7 +302,7 @@ Bytes 7+: Message Text (UTF-8, variable length) **Command Format**: ``` Byte 0: 0x3E -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length (0xFF = flood, otherwise actual path length) Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF) Next 2 bytes (little-endian): Data Type (`data_type`, uint16) @@ -306,13 +320,23 @@ Remaining bytes: Binary payload (variable length) - Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below. **Limits**: -- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes. -- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`). +- Maximum payload length is **165** bytes — `MAX_GROUP_DATA_LENGTH = + MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3` (`src/MeshCore.h:21`). This is the + radio-side limit and the one clients should enforce. +- Two different bounds are checked, and they do not agree. The host-frame check + uses the larger `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167` + (`companion_radio/MyMesh.cpp:1265`); the radio-side check uses 165 + (`BaseChatMesh.cpp:544`). +- Payloads **above 167** are rejected with `ERR_CODE_ILLEGAL_ARG` (6). +- Payloads of **166 or 167** pass the frame check, then fail the radio-side + check and return `ERR_CODE_TABLE_FULL` (3). Despite that code's usual + meaning, such a send can never succeed — do not retry it. Keep payloads at + 165 bytes or fewer and this case cannot arise. **Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of: - `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx` -- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` -- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later +- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` (167) +- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full (retry later), **or** a payload of 166-167 bytes that exceeds the radio-side limit (permanent; see Limits above) **Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram). @@ -341,7 +365,7 @@ Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarde Byte 0: 0x1B (packet type) Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB) Bytes 2-3: Reserved (clients MUST ignore) -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct) Bytes 6-7: Data Type (uint16 little-endian) Byte 8: Data Length @@ -550,7 +574,7 @@ def parse_contact_message(data): **Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08): ``` Byte 0: 0x08 (packet type) -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length Byte 3: Text Type Bytes 4-7: Timestamp (32-bit little-endian) @@ -562,7 +586,7 @@ Bytes 8+: Message Text (UTF-8) Byte 0: 0x11 (packet type) Byte 1: SNR (signed byte, multiplied by 4) Bytes 2-3: Reserved -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length Byte 6: Text Type Bytes 7-10: Timestamp (32-bit little-endian) diff --git a/docs/faq.md b/docs/faq.md index c7d7a118e..ade59bf00 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -537,18 +537,18 @@ Mac: python3 should be already installed. Then it should be the same for all platforms: ``` -python3 -m venv meshcore -cd meshcore && source bin/activate +python3 -m venv meshcore-venv +cd meshcore-venv && source bin/activate pip install -U platformio -git clone https://github.com/ripplebiz/MeshCore.git -cd MeshCore +git clone https://github.com/meshcore-dev/meshcore.git +cd meshcore ``` -open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5` -save, then run: +open platformio.ini and in `[arduino_base]` edit `LORA_FREQ` (it defaults to +`869.618`) to the frequency for your region, save, then run: ``` -pio run -e RAK_4631_Repeater +pio run -e RAK_4631_repeater ``` -then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater` +then you'll find `firmware.zip` in `.pio/build/RAK_4631_repeater` ### 5.10. Q: Are there other MeshCore related open source projects? @@ -592,7 +592,7 @@ For ESP-based devices (e.g. Heltec V3) you need: 1. Download the firmware file from . - Go to the website in a browser and find the section that has the firmware you need. - Click the Download button, right-click on the file you need, for example: - - `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin` + - `Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` - Non-merged bin keeps the existing Bluetooth pairing database. - `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin` - Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations. diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 6d97f2c30..417ee6dd6 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -186,11 +186,19 @@ Power management status can be queried via the CLI: | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | -On boards without power management enabled, all commands except `get pwrmgt.support` return: +On boards without power management enabled, `get pwrmgt.source` and +`get pwrmgt.bootmv` return: ``` ERROR: Power management not supported ``` +`get pwrmgt.support` returns `unsupported`. `get pwrmgt.bootreason` is not +compiled out at all and answers on every board: `getResetReason()` and +`getShutdownReason()` are virtuals on the base board class, so a board that +does not override them reports `Not available` rather than an error. ESP32 +boards override the reset half with `esp_reset_reason()`, so they return a real +reset reason and `Not available` for the shutdown reason. + ## Debug Output When `MESH_DEBUG=1` is enabled, the power management module outputs: diff --git a/docs/payloads.md b/docs/payloads.md index a2945e27f..493f628ce 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | request sub type | | request data | rest of payload | application-defined request payload body | For the common chat/server helpers in `BaseChatMesh`, the current request type values are: @@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | Value | Name | Description | |--------|-----------|----------------------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | keep-alive request used for maintained connections | +| `0x02` | keepalive | (deprecated) | +| `0x03` | get telemetry | request node telemetry | +| `0x04` | get min/max/avg | get sensor node stats on time series data | +| `0x05` | get acl | node ACL query | +| `0x06` | get neighbors | node neighbors query | +| `0x08` | subscribe | subscribe to telemetry push | +| `0x09` | ubsubscribe | unsubscribe from telemetry push | #### Get stats @@ -125,21 +132,50 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -#### Get telemetry data - -Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. - #### Get Telemetry -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x03 (request sub type) | +| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) | #### Get Min/Max/Ave (Sensor nodes) -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x04 (request sub type) | +| start | 4 | starting time, seconds ago | +| end | 4 | ending time, seconds ago | +| reserved | 2 | should be zeroes | + +#### Subscribe to Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x08 (request sub type) | +| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads | +| timeout secs | 2 | subscription timeout (seconds) | +| reserved | 1 | should be zero | +| min deltas len | 1 | byte length of LPP encoded min_deltas | +| min deltas | (variable) | LPP encoded min_deltas | + +#### Unsubscribe from Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x09 (request sub type) | #### Get Access List -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x05 (request sub type) | +| reserved | 2 | should be zeroes | #### Get Neighbors @@ -171,8 +207,9 @@ txt_type | Value | Description | Message content | |--------|---------------------------|--------------------------------------------------------------------------| | `0x00` | plain text message | the plain text of the message | -| `0x01` | CLI command | the command text of the message | +| `0x01` | CLI data | CLI command OR reply text | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | +| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) | ## Anonymous request diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index 316161445..d5bac7ede 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -27,15 +27,17 @@ set lon {longitude} ``` Sets your advertisement map longitude. (decimal degrees) -``` -set dutycycle {percent} -``` -Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. - ``` set af {air-time-factor} ``` -Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead. +Sets the transmit air-time-factor. + +> **Note:** `set dutycycle` is *not* available in Terminal Chat. Every other +> firmware — Repeater, Room Server, Sensor and Companion Radio — routes its +> `set`/`get` commands through the shared radio prefs handler and does support +> it (see [CLI Commands](./cli_commands.md)). Terminal Chat is the sole +> exception: it has its own inline `set` handler covering only the six options +> listed above. ``` @@ -99,3 +101,8 @@ Resets the path to current recipient, for new path discovery. public {text} ``` Sends the text message to the built-in 'public' group channel + +``` +help +``` +Lists the available commands. diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index b25b1442f..d927c1a3f 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -12,6 +12,7 @@ #endif #include "NodePrefs.h" +#include "MyMesh.h" enum class UIEventType { none, @@ -22,25 +23,21 @@ enum class UIEventType { ack }; -class AbstractUITask { +class AbstractUITask : public MyMesh::Listener { protected: mesh::MainBoard* _board; MultiSerialInterface* _interfaceManager; - bool _connected; AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { - _connected = false; } public: - void setHasConnection(bool connected) { _connected = connected; } - bool hasConnection() const { return _connected; } + bool hasConnection() const { return _interfaceManager->isConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); } 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 loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c3340663..f32ea67ae 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,6 +1,9 @@ #include "MyMesh.h" #include // needed for PlatformIO +#ifdef ENABLE_WIFI_INTERFACE +#include +#endif #include #define CMD_APP_START 1 @@ -62,6 +65,7 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 +#define CMD_RUN_CLI_COMMAND 66 // v14+ // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -97,6 +101,7 @@ #define RESP_ALLOWED_REPEAT_FREQ 26 #define RESP_CODE_CHANNEL_DATA_RECV 27 #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) @@ -259,10 +264,10 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return _prefs.interference_threshold; } 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 { @@ -271,11 +276,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } 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); } 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); } @@ -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); _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 @@ -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); } + 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[] } @@ -412,6 +415,11 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { } 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 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 @@ -464,16 +472,10 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _ui) { - _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_serial->isConnected()) { - _ui->notify(UIEventType::contactMessage); - } + if (_listener) { + _listener->onMessageRecv(pkt, from, txt_type, sender_timestamp, text); + _listener->onQueueSizeChanged(offline_queue_len); } -#endif } 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); } -void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) { +void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) { markConnectionActive(from); // in case this is from a server, and we have a connection queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); } +void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text, char* reply) { + markConnectionActive(from); // in case this is from a server, and we have a connection + if (from.isRemoteCLIAllowed()) { + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + } + } else { + queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text); + } +} + void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) { markConnectionActive(from); @@ -573,20 +586,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::channelMessage); -#endif } -#ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; + if (_listener) { + // Get the channel name from the channel index + ChannelDetails channel_details; + if (!getChannel(channel_idx, channel_details)) { + strcpy(channel_details.name, "Unknown"); + } + _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, @@ -622,6 +631,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _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, @@ -669,6 +681,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim memcpy(&reply[4], telemetry.getBuffer(), tlen); return 4 + tlen; } + } else if (_listener) { + return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply); } 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); i += (len - 4); _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 { MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); } + + if (_listener) _listener->onControlDataRecv(packet); } void MyMesh::onRawDataRecv(mesh::Packet *packet) { @@ -816,6 +847,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } else { 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, @@ -846,6 +880,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { 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 { @@ -860,11 +897,12 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t 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), - _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; _cli_rescue = false; + cli_command[0] = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -877,6 +915,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe // defaults _prefs.airtime_factor = 1.0; + _prefs.tx_delay_factor = 0.5f; + _prefs.direct_tx_delay_factor = 0.2f; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; _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_interval = 0; // No automatic GPS updates by default _prefs.radio_fem_rxgain = 1; +#ifdef RADIO_FEM_TXGAIN + _prefs.radio_fem_txgain = RADIO_FEM_TXGAIN; // board-specific default for PA +#else _prefs.radio_fem_txgain = 0; +#endif //_prefs.rx_delay_base = 10.0f; enable once new algo fixed _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) @@ -898,7 +942,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe #endif } -void MyMesh::begin(bool has_display) { +void MyMesh::begin() { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { @@ -939,6 +983,8 @@ void MyMesh::begin(bool has_display) { // sanitise bad pref values _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.freq = constrain(_prefs.freq, 150.0f, 2500.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_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours -#ifdef BLE_PIN_CODE // 123456 by default - 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 + _active_ble_pin = _prefs.ble_pin; resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure public channel _store->loadChannels(this); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getCustom()); + MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -991,6 +1021,9 @@ NodePrefs *MyMesh::getNodePrefs() { uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +void MyMesh::setBLEPin(uint32_t active_pin) { + _active_ble_pin = active_pin; +} struct FreqRange { uint32_t lower_freq, upper_freq; @@ -1082,6 +1115,20 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], _prefs.node_name, tlen); i += tlen; _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) { int i = 1; 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]; i += 6; ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); - if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) { char *text = (char *)&cmd_frame[i]; int tlen = len - i; uint32_t est_timeout; text[tlen] = 0; // ensure null int result; uint32_t expected_ack; - if (txt_type == TXT_TYPE_CLI_DATA) { + if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) { msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); + result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout); expected_ack = 0; // no Ack expected } else { result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); @@ -1180,8 +1227,8 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } else if (data_type == DATA_TYPE_RESERVED) { writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { - MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { writeOKFrame(); @@ -1369,9 +1416,7 @@ void MyMesh::handleCmdFrame(size_t len) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); -#endif + if (_listener) _listener->onQueueSizeChanged(offline_queue_len); } else { out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; _serial->writeFrame(out_frame, 1); @@ -1511,16 +1556,20 @@ void MyMesh::handleCmdFrame(size_t len) { #endif } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; - int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); + uint8_t path_len = cmd_frame[i++]; + if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { + uint8_t path[MAX_PATH_SIZE]; + i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); + if (i + 4 > len) { // min payload 4 bytes + writeErrFrame(ERR_CODE_ILLEGAL_ARG); } 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 { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) @@ -2027,8 +2076,149 @@ void MyMesh::enterCLIRescue() { 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() { 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) { char c = Serial.read(); if (c != '\n') { @@ -2037,22 +2227,18 @@ void MyMesh::checkCLIRescueCmd() { } Serial.print(c); // echo } - if (len == sizeof(cli_command)-1) { // command buffer full - cli_command[sizeof(cli_command)-1] = '\r'; + if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line + 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 cli_command[len - 1] = 0; // replace newline with C string null terminator - if (memcmp(cli_command, "set ", 4) == 0) { - const char* config = &cli_command[4]; - if (memcmp(config, "pin ", 4) == 0) { - _prefs.ble_pin = atoi(&config[4]); - savePrefs(); - Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); - } else { - Serial.printf(" Error: unknown config: %s\n", config); - } + reply_buf[0] = 0; + if (handleCommand(cli_command, 0, reply_buf)) { + // command was handled, print reply output + Serial.print(" "); Serial.print(reply_buf); Serial.println(); } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { @@ -2233,6 +2419,11 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); +#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) + // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a + // display + long-press), so serve config commands on the otherwise unused USB serial + checkCLIRescueCmd(); +#endif } // is there are pending dirty contacts write needed? @@ -2240,10 +2431,6 @@ void MyMesh::loop() { saveContacts(); dirty_contacts_expiry = 0; } - -#ifdef DISPLAY_CLASS - if (_ui) _ui->setHasConnection(_serial->isConnected()); -#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f73fe0e06..52a22cb41 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -2,17 +2,16 @@ #include #include -#include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 13 +#define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "9 Aug 2026" +#define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.17.0" +#define FIRMWARE_VERSION "v1.17.1" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -76,6 +75,10 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +// Copied from simple_repeater +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + struct AdvertPath { uint8_t pubkey_prefix[7]; uint8_t path_len; @@ -86,14 +89,35 @@ struct AdvertPath { class MyMesh : public BaseChatMesh, public DataStoreHost { 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 setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); + void setBLEPin(uint32_t active_pin); void loop(); void handleCmdFrame(size_t len); @@ -106,6 +130,9 @@ protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() 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; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; @@ -135,6 +162,8 @@ protected: const char *text) override; void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; + void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text, char* reply) override; void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, @@ -169,6 +198,7 @@ public: _prefs.node_lat = sensors.node_lat; _prefs.node_lon = sensors.node_lon; _store->savePrefs(_prefs); + _prefs.clearDirty(); } #if ENV_INCLUDE_GPS == 1 @@ -201,6 +231,7 @@ private: } void checkCLIRescueCmd(); + bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; @@ -215,7 +246,7 @@ private: uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; - AbstractUITask* _ui; + Listener* _listener; ContactsIterator _iter; uint32_t _iter_filter_since; @@ -225,6 +256,7 @@ private: bool _cli_rescue; bool send_unscoped; // force un-scoped flood (instead of using send_scope) char cli_command[80]; + char reply_buf[166]; uint8_t app_target_ver; uint8_t *sign_data; uint32_t sign_data_len; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 453f28d5d..46a3dbb7c 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,8 @@ #pragma once #include // For uint8_t, uint32_t #include +#include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -25,6 +27,8 @@ public: uint8_t telemetry_mode_loc = 0; uint8_t telemetry_mode_env = 0; float rx_delay_base = 0; + float tx_delay_factor = 0; + float direct_tx_delay_factor = 0; uint32_t ble_pin = 0; uint8_t advert_loc_policy = 0; uint8_t buzzer_quiet = 0; @@ -38,11 +42,25 @@ public: uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + uint8_t cad_enabled = 0; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // secs / 4 char default_scope_name[31]; 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: - class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -50,22 +68,58 @@ private: def("bw", _parent->bw); def("sf", _parent->sf); def("cr", _parent->cr); - //def("cad", _parent->cad_enabled); - //def("int_thr", _parent->interference_threshold); + def("cad", _parent->cad_enabled); + def("int_thr", _parent->interference_threshold); 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("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); - //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded - //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded - //def("agc_int", _parent->agc_reset_interval); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); def("hash_mode", _parent->path_hash_mode); def("multi_ack", _parent->multi_acks); } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + + // 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; @@ -111,12 +165,30 @@ private: def("tel_base", _parent->telemetry_mode_base); def("tel_loc", _parent->telemetry_mode_loc); def("tel_env", _parent->telemetry_mode_env); + def("tz_offset", _parent->tz_offset); } public: CompanionPrefs(NodePrefs* parent) : _parent(parent) { } }; 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: void structure() override { def("name", node_name, sizeof(node_name)); @@ -128,9 +200,17 @@ protected: def("gps", gps); def("repeat", repeat); def("comp", companion); + def("custom", custom); +#ifdef ENABLE_WIFI_INTERFACE + def("wifi", wifi); +#endif } 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; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); @@ -138,4 +218,10 @@ public: // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } 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(); } }; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 89f0e6cb9..839d49583 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -26,19 +26,38 @@ MultiSerialInterface interface_manager; // include nrf52 bluetooth interface #include SerialBLEInterface bluetooth_interface; + #elif defined(RP2040_PLATFORM) + // include rp2040 (Pico W / CYW43) bluetooth interface + #include + SerialBLEInterface bluetooth_interface; #else #error "SerialBLEInterface is not defined for this platform" #endif #endif // 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 #define TCP_PORT 5000 #endif - #ifdef ESP32 - // include esp32 wifi interface - #include + #ifndef WIFI_RETRY_INTERVAL + #if defined(RP2040_PLATFORM) + #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 SerialWifiInterface wifi_interface; #else #error "SerialWifiInterface is not defined for this platform" @@ -95,11 +114,7 @@ MultiSerialInterface interface_manager; StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif -); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); /* END GLOBAL OBJECTS */ @@ -108,9 +123,13 @@ void halt() { } /* WIFI RECONNECT TRACKERS */ -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef ENABLE_WIFI_INTERFACE bool wifi_needs_reconnect = false; 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 void setup() { @@ -153,37 +172,38 @@ void setup() { #endif #endif store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #else #error "need to define filesystem" #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 #if defined(BLE_PIN_CODE) bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); @@ -191,23 +211,47 @@ void setup() { #endif // add wifi interface -#ifdef WIFI_SSID - board.setInhibitSleep(true); // prevent sleep when WiFi is active - WiFi.setAutoReconnect(true); +#ifdef ENABLE_WIFI_INTERFACE + // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults + 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){ - if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); - wifi_needs_reconnect = true; - } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { - WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); - wifi_needs_reconnect = false; - } - }); + WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...", + WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason)); + wifi_needs_reconnect = true; + } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { + WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + wifi_needs_reconnect = false; + } + }); +#endif - WiFi.begin(WIFI_SSID, WIFI_PWD); - wifi_interface.begin(TCP_PORT); - interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + +#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 // add usb interface @@ -239,6 +283,7 @@ void setup() { #ifdef DISPLAY_CLASS 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 board.onBootComplete(); @@ -252,6 +297,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif @@ -262,13 +308,34 @@ void loop() { #endif } -#if defined(ESP32) && defined(WIFI_SSID) - // Safely attempt to reconnect every 10 seconds if flagged - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); - WiFi.disconnect(); - WiFi.reconnect(); - last_wifi_reconnect_attempt = millis(); +#ifdef ENABLE_WIFI_INTERFACE + if (wifi_enabled) { + // RP2040 has no WiFi event callbacks, so poll the link state instead + #if defined(RP2040_PLATFORM) + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + 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 } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31e..181bc8328 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,7 +2,8 @@ #include #include "../MyMesh.h" #include "target.h" -#ifdef WIFI_SSID +#include +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -19,11 +20,12 @@ #define LONG_PRESS_MILLIS 1200 +// Used both for recent adverts and discovered nodes #ifndef UI_RECENT_LIST_SIZE #define UI_RECENT_LIST_SIZE 4 #endif -#if UI_HAS_JOYSTICK +#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT #define PRESS_LABEL "press Enter" #else #define PRESS_LABEL "long press" @@ -97,7 +99,12 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#if UI_DISCOVER_SCREEN + DISCOVERY, +#endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -108,7 +115,13 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; 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) { // Convert millivolts to percentage @@ -141,11 +154,24 @@ class HomeScreen : public UIScreen { int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; 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 if (_task->isBuzzerQuiet()) { 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 } @@ -182,6 +208,40 @@ public: : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _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 { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); @@ -224,8 +284,19 @@ public: display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); 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(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); @@ -234,13 +305,21 @@ public: if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); + #ifdef UI_SHOW_CLOCK + display.drawTextCentered(display.width() / 2, 110, "< Connected >"); + #else display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - + #endif } else if (the_mesh.getBLEPin() != 0) { // BT pin display.setColor(UIColor::warning_txt); - display.setTextSize(2); 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); + #endif } } else if (_page == HomePage::RECENT) { 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; else sensors_scroll_offset = 0; #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) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -432,6 +546,7 @@ public: display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } +#endif } return 5000; // next render after 5000 ms } @@ -446,6 +561,11 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if UI_DISCOVER_SCREEN + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } +#endif return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -478,14 +598,32 @@ public: return true; } #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) { _shutdown_init = true; // need to wait for button to be released return true; } +#endif return false; } }; +#ifndef UI_MSG_PREVIEW_SIZE + #define UI_MSG_PREVIEW_SIZE 78 +#endif + class MsgPreviewScreen : public UIScreen { UITask* _task; mesh::RTCClock* _rtc; @@ -493,7 +631,7 @@ class MsgPreviewScreen : public UIScreen { struct MsgEntry { uint32_t timestamp; char origin[62]; - char msg[78]; + char msg[UI_MSG_PREVIEW_SIZE]; }; #define MAX_UNREAD_MSGS 32 int num_unread; @@ -648,18 +786,12 @@ switch(t){ #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) { - _msgcount = msgcount; - 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); + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); 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 } } + + 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() { @@ -720,6 +897,9 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + display.forceFullRefresh(); + display.clear(); + display.endFrame(); // Power off board including radio, display, GPS and components _board->powerOff(); } @@ -760,6 +940,17 @@ void UITask::loop() { } #elif defined(PIN_USER_BTN) 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) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { @@ -769,6 +960,7 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } + #endif #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52d3ffa13..361b04d01 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,30 @@ #include "../AbstractUITask.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 { DisplayDriver* _display; SensorManager* _sensors; @@ -90,10 +114,14 @@ public: bool getGPSState(); 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 - 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 loop() override; diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe237902..8d9ab175f 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 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 }; \ No newline at end of file diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b49b05dd1..db8772f46 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -126,7 +126,7 @@ switch(t){ // Serial.println((int) t); } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { clearMsgPreview(); @@ -139,17 +139,19 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; +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; #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", from_name); + sprintf(_origin, "(F) %s", from.name); } 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)); @@ -158,10 +160,46 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _display->turnOn(); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _need_refresh = true; + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _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) { @@ -262,7 +300,7 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && the_mesh.getBLEPin() != 0) { + if (!hasConnection() && the_mesh.getBLEPin() != 0) { _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); @@ -372,7 +410,7 @@ void UITask::userLedHandler() { statusLedWrite(255, 0, 0); // red: battery low } else if (_msgcount > 0) { statusLedWrite(255, 90, 0); // amber: unread messages - } else if (_connected) { + } else if (hasConnection()) { statusLedWrite(0, 0, 255); // blue: app connected } else { statusLedWrite(0, 255, 0); // green: heartbeat diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 02d126e8f..d5f798986 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -70,9 +70,13 @@ public: bool hasDisplay() const { return _display != NULL; } 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 - 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 loop() override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index b6bdbcf4b..7b70f8368 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -4,7 +4,7 @@ #include "target.h" #include "u8g2_icons.h" -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -173,7 +173,7 @@ public: display.setCursor(0, 19); display.print(tmp); - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); @@ -503,15 +503,16 @@ switch(t){ } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { gotoHomeScreen(); } } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; +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; if (_display != NULL) { 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 } } + 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() { diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index dc689478e..084919aa6 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -98,10 +98,13 @@ public: bool getGPSState(); 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 - 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 loop() override; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 5836a6941..44b2a3df0 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,7 +113,7 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) && ARDUINO_USB_MODE +#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); #endif @@ -149,6 +149,8 @@ void loop() { } } + board.loop(); + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { radio_driver.triggerNoiseFloorCalibrate(0); next_noise_floor_calib_ms = millis(); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3a..ca6a3e607 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t int results_offset = 0; uint8_t results_buffer[130]; for(int index = 0; index < count && index + offset < neighbours_count; index++){ - + // stop if we can't fit another entry in results int entry_size = pubkey_prefix_length + 4 + 1; if(results_offset + entry_size > sizeof(results_buffer)){ @@ -704,7 +704,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks bool is_retry = (sender_timestamp == client->last_timestamp); @@ -907,6 +907,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.loop_detect = LOOP_DETECT_MINIMAL; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -981,8 +982,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1170,7 +1171,7 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6d9cf459c..cac6c4a28 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; @@ -71,11 +67,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557d..e0b6a41df 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -140,8 +140,10 @@ void UITask::loop() { } 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 + delay(1000); + digitalWrite(LED_PIN, !LED_STATE_ON); // off LED #endif if (millis() > _powering_off_at) { _board->powerOff(); // should not return diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68e..d475b13d8 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -125,6 +125,13 @@ void setup() { void loop() { // Handle Serial CLI 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) { char c = Serial.read(); if (c != '\n') { @@ -134,8 +141,9 @@ void loop() { } if (c == '\r') break; } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + 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 @@ -157,6 +165,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + #ifdef ETHERNET_ENABLED ethernet_loop_maintain(); if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094fc..71b8d32a3 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -443,7 +443,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries bool is_retry = (sender_timestamp == client->last_timestamp); @@ -463,7 +463,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint8_t temp[166]; bool send_ack; - if (flags == TXT_TYPE_CLI_DATA) { + if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { if (client->isAdmin()) { if (is_retry) { temp[5] = 0; // no reply @@ -725,8 +725,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index ba9f7eeca..5cf949c6b 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -28,11 +28,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #ifndef LORA_FREQ diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff39..65fc55ef7 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -105,6 +105,13 @@ void setup() { void loop() { 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) { char c = Serial.read(); if (c != '\n') { @@ -113,8 +120,9 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + 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 @@ -154,6 +162,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da42ddcbb..2e6ba7126 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -242,6 +242,10 @@ protected: void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { } + + void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { + } + void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { } @@ -526,6 +530,13 @@ public: BaseChatMesh::loop(); 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) { char c = Serial.read(); if (c != '\n') { @@ -534,8 +545,9 @@ public: } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + 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 diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 9bfa5ec6a..b18606681 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,4 +1,6 @@ #include "SensorMesh.h" +#include +#include /* ------------------------------ Config -------------------------------- */ @@ -54,6 +56,10 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present) + +#define REQ_TYPE_SUBSCRIBE 0x08 +#define REQ_TYPE_UNSUBSCRIBE 0x09 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -73,104 +79,70 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { #endif } -static uint8_t getDataSize(uint8_t type) { - switch (type) { - case LPP_GPS: - return 9; - case LPP_POLYLINE: - return 8; // TODO: this is MINIMIUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - return 6; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - return 4; - case LPP_COLOUR: - return 3; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - return 2; +/* --------------------- Cayenne LPP helpers ----------------------------*/ + +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) { + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = LPPData::getDataSize(t); + + if (ch == channel && t == type) { + return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); } - return 1; -} - -static uint32_t getMultiplier(uint8_t type) { - switch (type) { - case LPP_CURRENT: - case LPP_DISTANCE: - case LPP_ENERGY: - return 1000; - case LPP_VOLTAGE: - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - return 100; - case LPP_TEMPERATURE: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - return 10; - } - return 1; -} - -static bool isSigned(uint8_t type) { - return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || - type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; -} - -static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { - uint32_t value = 0; - for (uint8_t i = 0; i < size; i++) { - value = (value << 8) + buffer[i]; + i += sz; // skip } + return def_value; // not found +} - int sign = 1; - if (is_signed) { - uint32_t bit = 1ul << ((size * 8) - 1); - if ((value & bit) == bit) { - value = (bit << 1) - value; - sign = -1; +/* ------------------ end Cayenne LPP helpers ----------------------*/ + +bool SensorMesh::telemHasChanged(ClientInfo* c) { + auto buf = telemetry.getBuffer(); + uint8_t size = telemetry.getSize(); + uint8_t i = 0; + bool changed = false; + + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type + uint8_t sz = LPPData::getDataSize(t); + + float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + float v = findTelemValue(buf, size, ch, t, 0.0f); + if (abs(v - pv) > min_delta) changed = true; // Yes, has changed + + i += 2 + sz; // skip + } + if (changed) { + // take snapshot of all _monitored_ telem values, for next cycle + i = 0; + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type + uint8_t sz = LPPData::getDataSize(t); + + c->extra.sensor.prev_telem[i] = ch; + c->extra.sensor.prev_telem[i + 1] = t; + + float v = findTelemValue(buf, size, ch, t, 0.0f); + LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + i += 2 + sz; // skip } } - return sign * ((float) value / multiplier); + return changed; } -static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { - // check sign - bool sign = value < 0; - if (sign) value = -value; +uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; - // get value to store - uint32_t v = value * multiplier; - - // format an uint32_t as if it was an int32_t - if (is_signed & sign) { - uint32_t mask = (1 << (size * 8)) - 1; - v = v & mask; - if (sign) v = mask - v + 1; - } - - // add bytes (MSB first) - for (uint8_t i=1; i<=size; i++) { - dest[size - i] = (v & 0xFF); - v >>= 8; - } - return size; -} - -uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all @@ -181,6 +153,10 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint // query other sensors -- target specific sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); @@ -211,12 +187,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint auto d = &data[i]; reply_data[ofs++] = d->_channel; reply_data[ofs++] = d->_lpp_type; - uint8_t sz = getDataSize(d->_lpp_type); - uint32_t mult = getMultiplier(d->_lpp_type); - bool is_signed = isSigned(d->_lpp_type); - ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); + uint8_t sz = LPPData::getDataSize(d->_lpp_type); + uint32_t mult = LPPData::getMultiplier(d->_lpp_type); + bool is_signed = LPPData::isSigned(d->_lpp_type); + ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); } return ofs; } @@ -234,6 +210,45 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint return ofs; } } + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + memcpy(&from->extra.sensor.push_tag, &payload[0], 4); + uint16_t timeout_secs; + memcpy(&timeout_secs, &payload[4], 2); + uint8_t reserved = payload[6]; + uint8_t min_deltas_len = payload[7]; + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + uint8_t reply_len; + if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) { + from->extra.sensor.scope_region_id = r->id; + from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; + from->extra.sensor.min_deltas_len = min_deltas_len; + memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); + // reply with actual expiry seconds (we could modify/impose restriction) + memcpy(&reply_data[4], &timeout_secs, 2); + memset(&reply_data[6], 0, 2); // reserved + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + reply_len = 8 + strlen((char *)&reply_data[8]); + } else { + memset(&reply_data[4], 0, 4); // expiry secs (0 for error) + reply_len = 8; + } + return reply_len; + } + if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + from->extra.sensor.scope_region_id = 0; + from->extra.sensor.push_tag = 0; + from->extra.sensor.expiry_timestamp = 0; + from->extra.sensor.min_deltas_len = 0; + // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? + memset(&reply_data[4], 0, 8); // success + getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness + return 12 + 2; + } return 0; // unknown command } @@ -262,7 +277,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { sendDirect(pkt, c->out_path, c->out_path_len); } else { unsigned long delay_millis = 0; - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } } t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); @@ -330,6 +345,25 @@ int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } +void SensorMesh::startRegionsLoad() { + temp_map.resetFrom(region_map); // rebuild regions in a temp instance + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; +} + +bool SensorMesh::saveRegions() { + return region_map.save(_fs); +} + +void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) { + if (r) { + region_map.getTransportKeysFor(*r, &default_scope, 1); + } else { + memset(default_scope.key, 0, sizeof(default_scope.key)); + } +} + uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { ClientInfo* client; if (data[0] == 0) { // blank password, just check if sender is in ACL @@ -343,7 +377,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } else { if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password #if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]); #endif return 0; } @@ -379,7 +413,41 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* return 13; // reply length } -void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { +void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { + if (region_load_active) { + if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation + region_map = temp_map; // copy over the temp instance as new current map + region_load_active = false; + + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; + + char *ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name + + while (*ep && *ep != 'F') ep++; // look for (optional) flags + + if (indent > 0 && indent < 8 && strlen(np) > 0) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr + + load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's + } + } + } + reply[0] = 0; + } + return; + } + while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -400,10 +468,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r if (sp == NULL) { strcpy(reply, "Err - bad params"); } else { + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + uint8_t pubkey[PUB_KEY_SIZE]; + *sp++ = 0; // replace space with null terminator - uint8_t pubkey[PUB_KEY_SIZE]; - int hex_len = min(sp - hex, PUB_KEY_SIZE*2); if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { uint8_t perms = atoi(sp); if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { @@ -451,6 +520,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r } } +mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) { + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + recv_pkt_region = NULL; + } else { + recv_pkt_region = ®ion_map.getWildcard(); + } + } else { + recv_pkt_region = NULL; + } + return Mesh::onRecvPacket(pkt); +} + void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; @@ -472,10 +556,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -503,7 +587,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); + if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -531,7 +615,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(×tamp, data, 4); if (timestamp > from->last_timestamp) { // prevent replay attacks - uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5); + uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from->last_timestamp = timestamp; @@ -541,14 +625,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); if (reply) { if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -571,12 +655,12 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); } } - } else if (flags == TXT_TYPE_CLI_DATA) { + } else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { from->last_timestamp = sender_timestamp; from->last_activity = getRTCClock()->getCurrentTime(); @@ -586,7 +670,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i uint8_t temp[166]; char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; - handleCommand(sender_timestamp, command, reply); + handleCommand(from, sender_timestamp, command, reply); int text_len = strlen(reply); if (text_len > 0) { @@ -601,7 +685,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); if (reply) { if (from->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -699,7 +783,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - region_map(key_store), + region_map(key_store), temp_map(key_store), _cli(board, rtc, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { @@ -708,6 +792,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise last_read_time = 0; num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; + recv_pkt_region = NULL; + region_load_active = false; // defaults _prefs.airtime_factor = 1.0; @@ -771,8 +857,8 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -820,11 +906,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params } +void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) { + if (scope.isNull()) { + sendFlood(pkt, delay_millis, path_hash_size); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, path_hash_size); + } +} + +void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + TransportKey req_scope; + bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard(); + bool req_scope_known = recv_pkt_region != NULL && !is_wildcard + && region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0; + + switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) { + case mesh::REPLY_SCOPE_REQUEST: + sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request + break; + case mesh::REPLY_SCOPE_DEFAULT: + // requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region. + // un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0 + sendFloodScoped(default_scope, packet, delay_millis, path_hash_size); + break; + case mesh::REPLY_SCOPE_NONE: + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + break; + } +} + void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -866,23 +984,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - auto buf = telemetry.getBuffer(); - uint8_t size = telemetry.getSize(); - uint8_t i = 0; - - while (i + 2 < size) { - // Get channel # - uint8_t ch = buf[i++]; - // Get data type - uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); - - if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - } - i += sz; // skip - } - return 0.0f; // not found + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { @@ -902,7 +1004,7 @@ void SensorMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); unsigned long delay_millis = 0; - if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); // schedule next flood advert updateAdvertTimer(); // also schedule local advert (so they don't overlap) @@ -931,6 +1033,39 @@ void SensorMesh::loop() { telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // This MCU temperature will be overridden by external sensors (if any) + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } + + // compare with previous telemetry, check if any deltas are greater than subscriber minimums + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas + RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); + if (r == NULL) continue; // unknown region scope + if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired + if (telemHasChanged(c)) { + TransportKey scope; + if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { + uint8_t tlen = telemetry.getSize(); + memcpy(reply_data, &c->extra.sensor.push_tag, 4); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(&reply_data[4], ×tamp, 4); + memcpy(&reply_data[8], telemetry.getBuffer(), tlen); + + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen); + if (reply) { + if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + sendDirect(reply, c->out_path, c->out_path_len, 0); + } else { + sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1); + } + } + } + } + } onSensorDataRead(); @@ -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)) { acl.save(_fs); dirty_contacts_expiry = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 3a33b638c..6b7a32fef 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -34,11 +34,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #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); void begin(FILESYSTEM* fs); void loop(); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply); // CommonCLI callbacks const char* getFirmwareVer() override { return FIRMWARE_VERSION; } @@ -78,9 +78,15 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override { } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + void startRegionsLoad() override; + bool saveRegions() override; + void onDefaultRegionChanged(const RegionEntry* r) override; float getTelemValue(uint8_t channel, uint8_t type); + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); + void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + protected: // current telemetry data queries float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } @@ -93,12 +99,12 @@ protected: bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts - enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT }; struct Trigger { uint32_t timestamp; - AlertPriority pri; uint32_t expected_acks[4]; + AlertPriority pri; int8_t curr_contact_idx; uint8_t attempt; unsigned long send_expiry; @@ -122,6 +128,7 @@ protected: int getInterferenceThreshold() const override; bool getCADEnabled() const override; int getAGCResetInterval() const override; + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; @@ -129,6 +136,7 @@ protected: bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; void onControlDataRecv(mesh::Packet* packet) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; + virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); private: @@ -141,7 +149,9 @@ private: unsigned long dirty_contacts_expiry; CayenneLPP telemetry; TransportKeyStore key_store; - RegionMap region_map; + RegionMap region_map, temp_map; + RegionEntry* recv_pkt_region; + RegionEntry* load_stack[8]; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; @@ -152,9 +162,11 @@ private: float pending_bw; uint8_t pending_sf; uint8_t pending_cr; + bool region_load_active; + bool telemHasChanged(ClientInfo* c); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); - uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); void sendAlert(const ClientInfo* c, Trigger* t); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a7..66991daad 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -122,6 +122,13 @@ void setup() { void loop() { 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) { char c = Serial.read(); if (c != '\n') { @@ -130,14 +137,15 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + 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 command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -145,6 +153,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/platformio.ini b/platformio.ini index e78124a40..f296f8382 100644 --- a/platformio.ini +++ b/platformio.ini @@ -85,7 +85,8 @@ platform_packages = ; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 - framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301665b40959682252911e57b11df3ee651a + platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM @@ -109,7 +110,9 @@ build_flags = ${arduino_base.build_flags} [stm32_base] extends = arduino_base -platform = ststm32 +; Pinned pending upstream fix for upstream conversions. +; https://github.com/jgromes/RadioLib/issues/1873 +platform = ststm32@19.7.1 extra_scripts = post:arch/stm32/build_hex.py build_flags = ${arduino_base.build_flags} -D STM32_PLATFORM @@ -170,6 +173,7 @@ build_src_filter = +<../src/Utils.cpp> +<../src/Packet.cpp> +<../src/helpers/ConfigSerializer.cpp> + +<../src/helpers/DynamicConfigSerializer.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef1..2a3582de0 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,21 +64,22 @@ public: virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported - virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemLna() const { return false; } - virtual bool isLoRaFemLnaEnabled() const { return false; } - // Software-selectable external FEM transmit gain. This is not a PA power switch. - virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemPaGain() const { return false; } - virtual bool isLoRaFemPaGainEnabled() const { return false; } // Power management interface (boards with power management override these) + virtual bool isPwrMgtInitialised() const { return false; } virtual bool isExternalPowered() { return false; } virtual uint16_t getBootVoltage() { return 0; } + virtual bool getWakeLpcompSupported() const { return false; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } + + // Called from the example main loops. Lets a board feed its watchdog and + // run periodic housekeeping. Default no-op. + virtual void loop() { /* no op */ } }; /** diff --git a/src/Utils.cpp b/src/Utils.cpp index 5ae7f0e27..7a3fb78b3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -203,6 +203,15 @@ bool Utils::isHexChar(char c) { return c == '0' || hexVal(c) > 0; } +bool Utils::isZeroes(const uint8_t* buf, size_t len) { + while (len > 0) { + if (*buf != 0) return false; + buf++; + len--; + } + return true; +} + bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) { int len = strlen(src_hex); if (len != dest_size*2) return false; // incorrect length diff --git a/src/Utils.h b/src/Utils.h index 5736b8747..7a0f7b6ee 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -82,6 +82,8 @@ public: static int parseTextParts(char* text, const char* parts[], int max_num, char separator=','); static bool isHexChar(char c); + + static bool isZeroes(const uint8_t* buf, size_t len); }; } diff --git a/src/armstubs.cpp b/src/armstubs.cpp new file mode 100644 index 000000000..30da40ecf --- /dev/null +++ b/src/armstubs.cpp @@ -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; } diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 998733ae0..25e5cd3fe 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -28,6 +28,14 @@ return i; } +bool AdvertDataParser::isValidName(const char *n) { + while (*n) { + if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; + n++; + } + return true; +} + AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) { _name[0] = 0; _lat = _lon = 0; diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index abe14cbd0..f4e109e9f 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -50,6 +50,8 @@ class AdvertDataParser { public: AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len); + static bool isValidName(const char* name); + bool isValid() const { return _valid; } uint8_t getType() const { return _flags & 0x0F; } uint16_t getFeat1() const { return _extra1; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9e..28592415c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,6 +9,8 @@ #define TXT_ACK_DELAY 200 #endif +#define CLI_REPLY_DELAY_MILLIS 600 + void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { sendFlood(pkt, delay_millis); } @@ -227,8 +229,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender ContactInfo& from = contacts[i]; if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { - uint32_t timestamp; - memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = data[4] >> 2; // message attempt number, and other flags // len can be > original length, but 'text' will be padded with zeroes @@ -236,7 +238,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (flags == TXT_TYPE_PLAIN) { from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know int text_len = strlen((char *)&data[5]); uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it @@ -254,20 +256,43 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { - onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + char *text = (char *)&data[5]; + onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know // NOTE: no ack expected for CLI_DATA replies + } else if (flags == TXT_TYPE_CLI_COMMAND) { + uint8_t temp[166]; + char *command = (char *)&data[5]; + char *reply = (char *)&temp[5]; + *reply = 0; - if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) - mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0); - if (path) sendFloodScoped(from, path); + onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know + // NOTE: no ack expected for CLI_COMMAND replies + + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + timestamp++; + } + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len); + if (reply_pkt) { + if (from.out_path_len == OUT_PATH_UNKNOWN) { + sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { - if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date - from.sync_since = timestamp; + if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date + from.sync_since = sender_timestamp; } from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know + onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE); @@ -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 n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Skip empty/unconfigured slots. An empty slot has an all-zero secret and + // therefore matches null-key group traffic (a node transmitting with an + // unset PSK): the zero-key MAC validates against the empty slot and the + // foreign message is delivered as if it belonged to that channel. Any node + // with a free channel slot would otherwise act as a null-key sink. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } @@ -458,13 +489,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, return rc; } -int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { +int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); + temp[4] = (attempt & 3) | (txt_type << 2); memcpy(&temp[5], text, text_len + 1); auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d98785470..0a8fdef4f 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -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 void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0; virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; @@ -156,7 +157,7 @@ public: mesh::Packet* createSelfAdvert(const char* name); mesh::Packet* createSelfAdvert(const char* name, double lat, double lon); int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout); - int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); + int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068..9938bc1e1 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -28,13 +28,21 @@ struct ClientInfo { unsigned long ack_timeout; uint8_t push_failures; } room; + struct { + uint32_t expiry_timestamp; // epoch seconds + uint32_t push_tag; + uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber + uint8_t min_deltas_len; + uint8_t min_deltas[14]; // LPP encoded + uint8_t prev_telem[14]; // LPP encoded + } sensor; } extra; bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e..63ab29a28 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -164,6 +164,7 @@ void CommonCLI::savePrefs() { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } _callbacks->savePrefs(); + _prefs->clearDirty(); } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -180,6 +181,16 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { } void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + if (_prefs->getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is a radio CLI command? + if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); } + return; + } + // hook for variant-specific CLI processing + if (_board->handleCommand(command, sender_timestamp, reply)) { + if (_prefs->isDirty()) { savePrefs(); } + return; + } + if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { @@ -344,6 +355,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "gps provider not found"); } + } else if (strcmp(command, "gps diag") == 0) { + LocationProvider * l = _sensors->getLocationProvider(); + if (l != NULL) { + const char* setting = _sensors->getSettingByKey("gps"); + bool requested = setting != NULL && strcmp(setting, "1") == 0; + int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U); + if (prefix_len < 0 || prefix_len >= 160) { + reply[159] = 0; + } else { + l->formatDiagnostics(reply + prefix_len, 160 - prefix_len); + } + } else { + strcpy(reply, "gps provider not found"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; @@ -447,39 +472,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "dutycycle ", 10) == 0) { - float dc = atof(&config[10]); - if (dc < 1 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 1-100"); - } else { - _prefs->airtime_factor = (100.0f / dc) - 1.0f; - savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%%", a_int, a_frac); - } - } else if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "cad ", 4) == 0) { - _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { - _prefs->agc_reset_interval = atoi(&config[19]) / 4; - savePrefs(); - sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { + + if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); @@ -532,77 +526,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); - } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - bool enabled = memcmp(&config[13], "on", 2) == 0; - _prefs->rx_boosted_gain = enabled; - savePrefs(); - if (_callbacks->setRxBoostedGain(enabled)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: unsupported"); - } - } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemLnaEnabled(true)) { - _prefs->radio_fem_rxgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemLnaEnabled(false)) { - _prefs->radio_fem_rxgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } - } else if (memcmp(config, "radio.fem.txgain ", 17) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemPaGainEnabled(true)) { - _prefs->radio_fem_txgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemPaGainEnabled(false)) { - _prefs->radio_fem_txgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } - } else if (memcmp(config, "radio ", 6) == 0) { - strcpy(tmp, &config[6]); - const char *parts[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); - float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); @@ -611,24 +534,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->node_lon = atof(&config[4]); savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0 && db <= 20.0f) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-20"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0 && f <= 2.0f) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { uint8_t m = atoi(&config[19]); if (m <= 64) { @@ -656,15 +561,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0 && f <= 2.0f) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -675,16 +571,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep *dp = 0; savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } } else if (memcmp(config, "loop.detect ", 12) == 0) { config += 12; uint8_t mode; @@ -705,11 +591,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); 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) { _prefs->freq = atof(&config[5]); 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) { const char* config = &command[4]; - if (memcmp(config, "dutycycle", 9) == 0) { - float dc = 100.0f / (_prefs->airtime_factor + 1.0f); - int dc_int = (int)dc; - int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); - sprintf(reply, "> %d.%d%%", dc_int, dc_frac); - } else if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); - } else if (memcmp(config, "int.thresh", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); - } else if (memcmp(config, "cad", 3) == 0) { - sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); - } else if (memcmp(config, "agc.reset.interval", 18) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); - } else if (memcmp(config, "allow.read.only", 15) == 0) { + if (memcmp(config, "allow.read.only", 15) == 0) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); } else if (memcmp(config, "flood.advert.interval", 21) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); @@ -842,37 +708,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.rxgain", 12) == 0) { - sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); - } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); - } - } else if (memcmp(config, "radio.fem.txgain", 16) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); - } - } else if (memcmp(config, "radio", 5) == 0) { - char freq[16], bw[16]; - strcpy(freq, StrHelper::ftoa(_prefs->freq)); - strcpy(bw, StrHelper::ftoa3(_prefs->bw)); - sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); - } else if (memcmp(config, "rxdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); - } else if (memcmp(config, "txdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "direct.txdelay", 14) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; @@ -883,8 +724,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sp++; } *reply = 0; // set null terminator - } else if (memcmp(config, "path.hash.mode", 14) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); @@ -895,10 +734,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } - } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { - sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); - } else if (memcmp(config, "freq", 4) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e9..2759b0c4f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -72,7 +74,7 @@ public: uint8_t extra_sf[4]; private: - class RadioPrefs : public ConfigSerializer { + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -96,6 +98,41 @@ private: } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + // CommonRadioPrefs interface + float getFreq() const override { return _parent->freq; } + void setFreq(float f) override { _parent->freq = f; markDirty(); } + float getBandwidth() const override { return _parent->bw; } + void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } + uint8_t getSpreadFactor() const override { return _parent->sf; } + void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } + uint8_t getCodingRate() const override { return _parent->cr; } + void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } + float getAirtimeFactor() const override { return _parent->airtime_factor; } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } + uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } + void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } + uint8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + float getRxDelay() const override { return _parent->rx_delay_base; } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint8_t getHashMode() const override { return _parent->path_hash_mode; } + void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } + uint8_t getMultiAcks() const override { return _parent->multi_acks; } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } + uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } + void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } + uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } + void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; @@ -166,6 +203,8 @@ private: }; RoomPrefs room; + DynamicConfigSerializer custom; + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -182,16 +221,23 @@ protected: def("repeat", repeat); def("room", room); def("power", power); + def("custom", custom); } public: - NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this), custom(&radio) { node_name[0] = 0; password[0] = 0; guest_password[0] = 0; bridge_secret[0] = 0; owner_info[0] = 0; } + + CommonRadioPrefs* getRadioPrefs() { return &radio; } + KeyValueStore* getCustom() { return &custom; } + + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp new file mode 100644 index 000000000..41b179932 --- /dev/null +++ b/src/helpers/CommonRadioPrefs.cpp @@ -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 +} diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h new file mode 100644 index 000000000..0e83ca9df --- /dev/null +++ b/src/helpers/CommonRadioPrefs.h @@ -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; +}; diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index adff147f4..daf834b2b 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,4 +1,5 @@ #include "ConfigSerializer.h" +#include // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not) bool ConfigSerializer::saveSerial(Stream& s) { Context context(&s, OP::WRITE); @@ -23,7 +24,9 @@ static bool is_whitespace(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static bool is_key_char(char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; + // digits are allowed: keys like gr_1hop contain one, otherwise loadSerial + // fails at that key and prefs stop reloading after a reboot + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } static bool is_value_char(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; @@ -62,6 +65,7 @@ int ConfigSerializer::Context::readNext() { case EXPECT_COMMA_OR_KEY: if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } case EXPECT_KEY: + if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}' if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 7e6d6f2a6..e55b1b120 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -17,7 +17,9 @@ class ConfigSerializer { bool _first; int8_t _depth; + bool _dirty = false; +protected: enum OP { READ, WRITE }; class Context { @@ -38,13 +40,14 @@ class ConfigSerializer { const char* getToken() const { return rd_buf; } bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } + const char* getKey(uint8_t depth) { return _keys[depth]; } }; Context* _context = NULL; + int8_t getDepth() const { return _depth; } void writeComma(); -protected: ConfigSerializer() { } void def(const char* key, char* value, size_t max_len); // max_len inclusive of null @@ -62,7 +65,12 @@ protected: virtual void structure() = 0; + void markDirty() { _dirty = true; } + public: bool loadSerial(Stream& s); bool saveSerial(Stream& s); + + virtual bool isDirty() const { return _dirty; } + virtual void clearDirty() { _dirty = false; } }; diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index ede977cac..5a156c829 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -26,6 +26,12 @@ struct ContactInfo { return shared_secret; } + bool isFav() const { return flags & 0x01; } + bool isTelemBaseAllowed() const { return flags & 0x02; } + bool isTelemLocAllowed() const { return flags & 0x04; } + bool isTelemEnvAllowed() const { return flags & 0x08; } + bool isRemoteCLIAllowed() const { return flags & 0x10; } + private: mutable uint8_t shared_secret[PUB_KEY_SIZE]; }; diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp new file mode 100644 index 000000000..b9c6c267e --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -0,0 +1,97 @@ +#include "DynamicConfigSerializer.h" +#include + +#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()); + } +} diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h new file mode 100644 index 000000000..e5dc4c7c9 --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.h @@ -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; +}; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index d7eb5fee2..75428bc69 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -15,6 +15,7 @@ #include "soc/rtc.h" #include "esp_system.h" #include +#include class ESP32Board : public mesh::MainBoard { protected: @@ -51,6 +52,8 @@ public: #endif } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + // Temperature from ESP32 MCU float getMCUTemperature() override { uint32_t raw = 0; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h new file mode 100644 index 000000000..cc4ac455b --- /dev/null +++ b/src/helpers/KeyValueStore.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +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; } +}; diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index eb88c8974..f2fa7743a 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -47,7 +47,8 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() { g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; } -void NRF52Board::initPowerMgr() { +void NRF52Board::pwrmgtInit() { + if (pwrmgt_initialised) return; // Copy early-captured register values reset_reason = g_nrf52_reset_reason; shutdown_reason = g_nrf52_shutdown_reason; @@ -74,6 +75,7 @@ void NRF52Board::initPowerMgr() { MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)", getResetReasonString(reset_reason), (unsigned long)reset_reason); } + pwrmgt_initialised = true; } const char* NRF52Board::getResetReasonString(uint32_t reason) { @@ -98,6 +100,7 @@ const char* NRF52Board::getResetReasonString(uint32_t reason) { const char* NRF52Board::getShutdownReasonString(uint8_t reason) { switch (reason) { + case SHUTDOWN_REASON_NONE: return "None"; case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage"; case SHUTDOWN_REASON_USER: return "User Request"; case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection"; @@ -106,7 +109,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { } bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { - initPowerMgr(); + pwrmgtInit(); // Read boot voltage boot_voltage_mv = getBattMilliVolts(); @@ -173,24 +176,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) { NVIC_SystemReset(); } -void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { +void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t lpcomp_refsel) { + pwrmgtWakeArmVbus(); + if (!isPwrMgtInitialised()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: Power Management not initialised"); + return; + } + if (!getWakeLpcompSupported()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: LPCOMP unsupported on variant"); + return; + } // LPCOMP is not managed by SoftDevice - direct register access required // Halt and disable before reconfiguration NRF_LPCOMP->TASKS_STOP = 1; NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled; // Select analog input (AIN0-7 maps to PSEL 0-7) - NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; + NRF_LPCOMP->PSEL = ((uint32_t)PWRMGT_LPCOMP_AIN << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; + NRF_LPCOMP->REFSEL = ((uint32_t)lpcomp_refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; // Detect UP events (voltage rises above threshold for battery recovery) NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up; - // Enable 50mV hysteresis for noise immunity - NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV; - // Clear stale events/interrupts before enabling wake NRF_LPCOMP->EVENTS_READY = 0; NRF_LPCOMP->EVENTS_DOWN = 0; @@ -204,23 +213,22 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled; NRF_LPCOMP->TASKS_START = 1; - // Wait for comparator to settle before entering SYSTEMOFF + // Wait for comparator to settle for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) { delayMicroseconds(50); } - if (refsel == 7) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel); - } else if (refsel <= 6) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)", - ain_channel, refsel + 1); + if (lpcomp_refsel == 7) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN); + } else if (lpcomp_refsel <= 6) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1); } else { - uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1); - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)", - ain_channel, ref_num); + uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num); } +} - // Configure VBUS (USB power) wake alongside LPCOMP +void NRF52Board::pwrmgtWakeArmVbus() { uint8_t sd_enabled = 0; sd_softdevice_is_enabled(&sd_enabled); if (sd_enabled) { @@ -229,8 +237,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_POWER->EVENTS_USBDETECTED = 0; NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk; } - - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed"); } #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f974..43944b5f7 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(NRF52_PLATFORM) @@ -25,15 +26,15 @@ struct PowerMgtConfig { #endif class NRF52Board : public mesh::MainBoard { -#ifdef NRF52_POWER_MANAGEMENT - void initPowerMgr(); -#endif +private: + bool pwrmgt_initialised = false; protected: uint8_t startup_reason; char *ota_name; #ifdef NRF52_POWER_MANAGEMENT + void pwrmgtInit(); uint32_t reset_reason; // RESETREAS register value uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) @@ -41,6 +42,7 @@ protected: bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void pwrmgtWakeArmVbus(); virtual void initiateShutdown(uint8_t reason); #endif @@ -57,12 +59,18 @@ public: virtual void sleep(uint32_t secs) override; bool isExternalPowered() override; + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } const char* getResetReasonString(uint32_t reason) override; const char* getShutdownReasonString(uint8_t reason) override; + bool isPwrMgtInitialised() const override { return pwrmgt_initialised; } + #ifdef PWRMGT_LPCOMP_AIN + bool getWakeLpcompSupported() const override { return true; } + #endif #endif }; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c22..85b9a1ee0 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -23,6 +23,7 @@ public: virtual const char* getSettingValue(int i) const { return NULL; } virtual bool setSettingValue(const char* name, const char* value) { return false; } virtual LocationProvider* getLocationProvider() { return NULL; } + virtual bool isGPSDetected() const { return false; } // Helper functions to manage setting by keys (useful in many places ...) const char* getSettingByKey(const char* key) { diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0c..8a8eefe8c 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -44,7 +44,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) { } 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]; _num--; @@ -80,10 +80,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() { } 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) { + if (packet == NULL) return; if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); free(packet); @@ -115,6 +119,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { } void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { + if (packet == NULL) return; if (!rx_queue.add(packet, 0, scheduled_for)) { MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet"); free(packet); diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e0..f63700e51 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -14,7 +14,10 @@ public: bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); int count() const { return _num; } 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); }; diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index d327931fd..4358c8b83 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -27,8 +27,13 @@ bool StrHelper::isBlank(const char* str) { } #include +#include +#if __has_include() +// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084 +#include +#endif -union int32_Float_t +union int32_Float_t { int32_t Long; float Float; @@ -100,7 +105,7 @@ static void _ftoa(float f, char *p, int *status) *p++ = '-'; if (int_part == 0) *p++ = '0'; - else + else { ltoa(int_part, p, 10); while (*p) diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index ece494f29..693a47cad 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -4,8 +4,10 @@ #include #define TXT_TYPE_PLAIN 0 // a plain text message -#define TXT_TYPE_CLI_DATA 1 // a CLI command +#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply #define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender +#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly) + #define DATA_TYPE_RESERVED 0x0000 // reserved for future use #define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1df..f0fde4354 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1110::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h new file mode 100644 index 000000000..67e3b52a9 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#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; } +}; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h new file mode 100644 index 000000000..5a754cee8 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -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(); + } +}; diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index a89ae9433..0255b93cf 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -72,7 +72,7 @@ class CustomLR2021 : public LR2021 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags - return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } bool isReceiving() { diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b9c095ac4..e4d2ba1c2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -48,6 +48,9 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { +#if defined(USE_LR2021) + idle(); +#endif _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; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +} diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 5db1e41f0..77dd93116 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -92,8 +92,10 @@ public: void random(uint8_t* dest, size_t sz) override { #ifdef USE_CC310_HW_CRYPTO - // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. 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 for (int i = 0; i < sz; i++) { dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF); diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp new file mode 100644 index 000000000..a1fa487e7 --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -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 +#include +#include + +// 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 diff --git a/src/helpers/rp2040/SerialBLEInterface.h b/src/helpers/rp2040/SerialBLEInterface.h new file mode 100644 index 000000000..2e085a6cc --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.h @@ -0,0 +1,75 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include +#include + +// 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 + #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 diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index e2f0d33e7..6f4607751 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -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 // 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: // the first detected+initialized sensor gets channel 2, the // next gets channel 3, and so on. @@ -551,21 +553,27 @@ struct SensorDef { 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[] = { #if ENV_INCLUDE_AHTX0 { TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 }, #endif #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 #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 #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 #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 #if ENV_INCLUDE_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 }; +#undef TELEM_BOSCH_ALT_ADDR + 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; for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; 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]) { MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address); continue; @@ -650,6 +666,7 @@ bool EnvironmentSensorManager::begin() { continue; } 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++) { _active_sensors[_active_sensor_count++] = { def.query, sub }; } @@ -814,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } #endif + #ifndef RAK_3401 //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); delay(500); digitalWrite(ioPin,HIGH); + #endif + + // give gps time to power up delay(500); //Try to init RAK12500 on I2C @@ -854,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ return true; } + #ifndef RAK_3401 pinMode(ioPin, INPUT); + #endif MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } @@ -863,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, HIGH); + #endif return; #endif @@ -879,8 +904,10 @@ void EnvironmentSensorManager::start_gps() { void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, LOW); + #endif return; #endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c896..17706cbc4 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -38,6 +38,7 @@ public: #if ENV_INCLUDE_GPS EnvironmentSensorManager(LocationProvider &location): _location(&location){}; LocationProvider* getLocationProvider() { return _location; } + bool isGPSDetected() const override { return gps_detected; } #else EnvironmentSensorManager(){}; #endif diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 70a036c49..db49cade4 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -63,12 +63,66 @@ #define LPP_ERROR_OVERFLOW 1 #define LPP_ERROR_UNKOWN_TYPE 2 -class LPPReader { - const uint8_t* _buf; - uint8_t _len; - uint8_t _pos; +class LPPData { +public: + static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; + } - float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + return 10; + } + return 1; + } + + static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; + } + + static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { uint32_t value = 0; for (uint8_t i = 0; i < size; i++) { value = (value << 8) + buffer[i]; @@ -85,6 +139,36 @@ class LPPReader { return sign * ((float) value / multiplier); } + static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { + // check sign + bool sign = value < 0; + if (sign) value = -value; + + // get value to store + uint32_t v = value * multiplier; + + // format an uint32_t as if it was an int32_t + if (is_signed & sign) { + uint32_t mask = (1 << (size * 8)) - 1; + v = v & mask; + if (sign) v = mask - v + 1; + } + + // add bytes (MSB first) + for (uint8_t i=1; i<=size; i++) { + dest[size - i] = (v & 0xFF); + v >>= 8; + } + return size; + } + +}; + +class LPPReader { + const uint8_t* _buf; + uint8_t _len; + uint8_t _pos; + public: LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { } @@ -103,72 +187,42 @@ public: } bool readGPS(float& lat, float& lon, float& alt) { - lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3; + lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3; return _pos <= _len; } bool readVoltage(float& voltage) { - voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2; + voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2; return _pos <= _len; } bool readCurrent(float& amps) { - amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; + amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; return _pos <= _len; } bool readPower(float& watts) { - watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2; + watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2; return _pos <= _len; } bool readTemperature(float& degrees_c) { - degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2; + degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2; return _pos <= _len; } bool readPressure(float& pa) { - pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2; + pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2; return _pos <= _len; } bool readRelativeHumidity(float& pct) { - pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1; + pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1; return _pos <= _len; } bool readAltitude(float& m) { - m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2; + m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2; return _pos <= _len; } void skipData(uint8_t type) { - switch (type) { - case LPP_GPS: - _pos += 9; break; - case LPP_POLYLINE: - _pos += 8; break; // TODO: this is MINIMUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - _pos += 6; break; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - _pos += 4; break; - case LPP_COLOUR: - _pos += 3; break; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - _pos += 2; break; - default: - _pos++; - } + _pos += LPPData::getDataSize(type); } }; @@ -185,6 +239,19 @@ class LPPWriter { public: LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { } + bool writeData(uint8_t channel, uint8_t type, float v) { + uint8_t sz = LPPData::getDataSize(type); + bool s = LPPData::isSigned(type); + uint32_t mul = LPPData::getMultiplier(type); + if (_len + 2 + sz <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = type; + _len += LPPData::putFloat(&_buf[_len], v, sz, mul, s); + return true; + } + return false; + } + bool writeVoltage(uint8_t channel, float voltage) { if (_len + 4 <= _max_len) { _buf[_len++] = channel; diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652e..a70ae8ca6 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -1,6 +1,8 @@ #pragma once #include "Mesh.h" +#include +#include class LocationProvider { @@ -22,4 +24,14 @@ public: virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + + // Format compact diagnostics in the caller-provided buffer. Providers that + // have more information (for example UART counters) can override this. + virtual void formatDiagnostics(char* out, size_t out_size) { + if (out_size == 0) return; + snprintf(out, out_size, "en:%u sat:%ld fix:%u", + isEnabled() ? 1U : 0U, + satellitesCount(), + isValid() ? 1U : 0U); + } }; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index a8bdbe0bf..ca7fd9aa0 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef GPS_EN #ifdef PIN_GPS_EN @@ -61,6 +62,31 @@ class MicroNMEALocationProvider : public LocationProvider { delay(250); } #endif + // Observation-only diagnostics. These fields do not change GPS power, + // timing, reset behaviour, or parser input. + uint32_t _uart_bytes = 0; + uint32_t _nmea_checksum_ok = 0; + uint32_t _nmea_checksum_bad = 0; + uint32_t _begin_calls = 0; + uint32_t _stop_calls = 0; + unsigned long _last_uart_ms = 0; + unsigned long _last_nmea_ms = 0; + unsigned long _last_fix_ms = 0; + bool _uart_seen = false; + bool _nmea_seen = false; + bool _fix_seen = false; + + static uint32_t ageMs(unsigned long timestamp) { + return (uint32_t)(millis() - timestamp); + } + + static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) { + if (!seen) { + snprintf(out, out_size, "never"); + } else { + snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp)); + } + } public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : @@ -87,6 +113,7 @@ public : } void begin() override { + _begin_calls++; claim(); if (_pin_en != -1) { digitalWrite(_pin_en, GPS_EN_ACTIVE); @@ -110,6 +137,7 @@ public : } void stop() override { + _stop_calls++; if (_pin_en != -1) { digitalWrite(_pin_en, !GPS_EN_ACTIVE); } @@ -153,10 +181,36 @@ public : while (_gps_serial->available()) { char c = _gps_serial->read(); + _uart_bytes++; + _last_uart_ms = millis(); + _uart_seen = true; #ifdef GPS_NMEA_DEBUG Serial.print(c); #endif - nmea.process(c); + bool parsed = nmea.process(c); + + // 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; @@ -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); + } }; diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h index 06bc768f8..016d1fb15 100644 --- a/src/helpers/stm32/STM32Board.h +++ b/src/helpers/stm32/STM32Board.h @@ -2,6 +2,7 @@ #include #include +#include class STM32Board : public mesh::MainBoard { protected: @@ -12,6 +13,8 @@ public: startup_reason = BD_STARTUP_NORMAL; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + uint8_t getStartupReason() const override { return startup_reason; } uint16_t getBattMilliVolts() override { diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index b76a1b6ca..3e9e2dde8 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -23,6 +23,7 @@ public: virtual bool isOn() = 0; virtual bool isEink() { return false; } // default to non-eink, override in eink drivers + virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 13dafa345..332157eb8 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,10 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +#ifndef EPD_WASHING_MACHINE_CYCLES + #define EPD_WASHING_MACHINE_CYCLES 0 +#endif + // Color scheme ColorVal UIColor::window_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::corp_blue = GxEPD_BLACK; - bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() { display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); setTextSize(1); // Default to size 1 - display.setPartialWindow(0, 0, display.width(), display.height()); - display.fillScreen(GxEPD_WHITE); - display.display(true); + display.setFullWindow(); + + 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 digitalWrite(DISP_BACKLIGHT, LOW); pinMode(DISP_BACKLIGHT, OUTPUT); #endif _init = true; + _isOn = true; return true; } @@ -55,7 +70,9 @@ void GxEPDDisplay::turnOn() { #elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif - _isOn = true; + if (!_isOn) { + _isOn = true; + } } void GxEPDDisplay::turnOff() { @@ -65,6 +82,13 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _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() { @@ -77,6 +101,16 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); 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) { @@ -178,9 +212,13 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) { } void GxEPDDisplay::endFrame() { + if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { display.display(true); - last_display_crc_value = crc; + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; + } } + last_display_crc_value = crc; } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c653eac4b..8a37b72cc 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,6 +16,12 @@ #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 { #if defined(EINK_DISPLAY_MODEL) @@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver { uint16_t _curr_color; CRC32 display_crc; 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: #if defined(EINK_DISPLAY_MODEL) @@ -48,6 +56,8 @@ public: bool isOn() override { return _isOn; } 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 turnOff() override; void clear() override; diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h index 358a343b0..351545cef 100644 --- a/src/helpers/ui/MomentaryButton.h +++ b/src/helpers/ui/MomentaryButton.h @@ -25,8 +25,8 @@ class MomentaryButton { public: MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true); MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold); - void begin(); - int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* + virtual void begin(); + virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state) uint8_t getPin() { return _pin; } bool isPressed() const; diff --git a/src/helpers/ui/RotaryInputGPIO.cpp b/src/helpers/ui/RotaryInputGPIO.cpp new file mode 100644 index 000000000..b094fe498 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.cpp @@ -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; +} \ No newline at end of file diff --git a/src/helpers/ui/RotaryInputGPIO.h b/src/helpers/ui/RotaryInputGPIO.h new file mode 100644 index 000000000..16b7e46ab --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#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;} +}; diff --git a/src/helpers/ui/SH1107Display.cpp b/src/helpers/ui/SH1107Display.cpp new file mode 100644 index 000000000..09bf424f8 --- /dev/null +++ b/src/helpers/ui/SH1107Display.cpp @@ -0,0 +1,113 @@ +#include "SH1107Display.h" +#include +#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(); +} diff --git a/src/helpers/ui/SH1107Display.h b/src/helpers/ui/SH1107Display.h new file mode 100644 index 000000000..417184c29 --- /dev/null +++ b/src/helpers/ui/SH1107Display.h @@ -0,0 +1,43 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#define SH110X_NO_SPLASH +#include + +#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; +}; diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/wifi/SerialWifiInterface.cpp similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.cpp rename to src/helpers/wifi/SerialWifiInterface.cpp diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/wifi/SerialWifiInterface.h similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.h rename to src/helpers/wifi/SerialWifiInterface.h diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index 6d3cebdfe..433c97106 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -50,6 +50,8 @@ public: const std::string& text() const { return _text; } }; +#if 0 +// Re-enable test once we can SET fem_ values in companion TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { NodePrefs saved; saved.rx_boosted_gain = 0; @@ -73,6 +75,7 @@ TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { EXPECT_EQ(0, loaded.radio_fem_rxgain); EXPECT_EQ(1, loaded.radio_fem_txgain); } +#endif int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index 27c3c8119..80d5e7808 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -1,13 +1,6 @@ #include #include "helpers/ConfigSerializer.h" - -class NativeFileSystem { -public: - void mkdir(const char*) { } -}; -#define FILESYSTEM NativeFileSystem -#include "helpers/CommonCLI.h" -#undef FILESYSTEM +#include "helpers/DynamicConfigSerializer.h" #define TEST_INT_S "56" #define TEST_INT 56 @@ -192,28 +185,131 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } -TEST(NodePrefs, FemGainSettingsRoundTrip) { - NodePrefs saved; - saved.radio_fem_rxgain = 0; - saved.radio_fem_txgain = 1; +class TestNested : public ConfigSerializer { + class Inner : public ConfigSerializer { + protected: + void structure() override { } // no properties, so it writes as '{}' + }; + Inner inner; + protected: + void structure() override { + def("age", age); + def("inner", inner); + def("name", name, sizeof(name)); // comes *after* the empty sub-object + } + public: + int32_t age; + char name[16]; +}; - MockPrintStream output; - ASSERT_TRUE(saved.saveSerial(output)); +TEST(ConfigSerializer, LoadSerial_EmptyObject) { + MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; - std::string serialised(reinterpret_cast(output.getBytes()), output.getLength()); - EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0")); - EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1")); + bool success = data.loadSerial(s); + EXPECT_TRUE(success); - MockInputStream input(serialised.c_str()); - NodePrefs loaded; - loaded.radio_fem_rxgain = 1; - loaded.radio_fem_txgain = 0; - - ASSERT_TRUE(loaded.loadSerial(input)); - EXPECT_EQ(0, loaded.radio_fem_rxgain); - EXPECT_EQ(1, loaded.radio_fem_txgain); + EXPECT_EQ(TEST_INT, data.age); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); // properties after an empty object must still load } +TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) { + MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + +TEST(DynamicConfigSerializer, GetSet_Basic) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("11", tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} + +TEST(DynamicConfigSerializer, Set_Replaces) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool s3 = data.setByKey("age", "333"); + EXPECT_TRUE(s3); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("333", tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} + +TEST(DynamicConfigSerializer, GetUnknown_Fail) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + EXPECT_TRUE(s1); + + char tmp[32]; + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_FALSE(g2); +} + +TEST(DynamicConfigSerializer, SaveCustom_Basic) { + MockPrintStream s; + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + char tmp[128]; + memcpy(tmp, s.getBytes(), l); + tmp[l] = 0; + + const char* expect = "{age:\"11\",name:\"Scott\"}"; + EXPECT_STREQ(expect, tmp); +} + +TEST(DynamicConfigSerializer, LoadCustom_Basic) { + MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}"); + DynamicConfigSerializer data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ(TEST_INT_S, tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} // ── main ─────────────────────────────────────────────────────── diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 401a30d23..f98f8d4a5 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -1,6 +1,7 @@ [Heltec_ct62] extends = esp32_base board = esp32-c3-devkitm-1 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32_base.build_flags} -I variants/heltec_ct62 diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 354004f07..2f07c021b 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -173,6 +173,7 @@ extends = Heltec_RC32 build_flags = ${Heltec_RC32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -185,6 +186,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -314,6 +316,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -324,6 +327,7 @@ build_flags = build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 78af529d6..ea26d2cc1 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool T096Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); -} - bool T096Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void T096Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 15c7e68b5..e926c8a6a 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -4,28 +4,33 @@ #include #include #include +#include #include "LoRaFEMControl.h" class T096Board : public NRF52BoardDCDC { + KeyValueStore* _prefs = NULL; + protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif void variant_shutdown(); + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + public: RefCountedDigitalPin periph_power; LoRaFEMControl loRaFEMControl; T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {} void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 99b1cdfe0..753824a05 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -72,10 +72,50 @@ void HeltecTrackerV2Board::begin() { return true; } - bool HeltecTrackerV2Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a0254..5fed2b655 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -6,6 +6,10 @@ #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; public: RefCountedDigitalPin periph_power; @@ -14,13 +18,13 @@ public: HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d040b72f9..5fd9edabe 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -182,6 +182,7 @@ extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display @@ -195,6 +196,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 78561a14a..6435936eb 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -178,6 +178,7 @@ extends = Heltec_lora32_v2 build_flags = ${Heltec_lora32_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 @@ -189,6 +190,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 65e636eb4..a2b159ecc 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -185,6 +185,7 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display @@ -198,6 +199,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -340,6 +342,7 @@ lib_deps = extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -350,6 +353,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3f13f41f6..794257d76 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -73,10 +73,50 @@ void HeltecV4Board::begin() { return true; } - bool HeltecV4Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecV4Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb37..79a690e84 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -10,6 +10,10 @@ #endif class HeltecV4Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -20,12 +24,12 @@ public: HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { if (multiplier == 0.0f) { diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index d718c006c..96b3eec04 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -228,6 +228,7 @@ extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -241,6 +242,7 @@ build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -393,6 +395,7 @@ extends = heltec_v4_tft build_flags = ${heltec_v4_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -406,6 +409,7 @@ build_src_filter = ${heltec_v4_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1fb123b2f..ea2cfbedb 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -84,3 +84,61 @@ const char* HeltecV4R8Board::getManufacturerName() const { return "Heltec V4 R8 OLED"; #endif } + +bool HeltecV4R8Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool HeltecV4R8Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} + +void HeltecV4R8Board::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 HeltecV4R8Board::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 +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 20811abb1..7ab29f962 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -11,6 +11,10 @@ #endif class HeltecV4R8Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -21,6 +25,9 @@ public: HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h index 961cfd070..4b74c2e26 100644 --- a/variants/heltec_v4_r8/LoRaFEMControl.h +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -17,6 +17,7 @@ public: void setLNAEnable(bool enabled); bool isLnaCanControl(void) { return true; } void setLnaCanControl(bool can_control) { } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } private: diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index f523ebf9b..7611cc1cb 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -175,6 +175,7 @@ extends = heltec_v4_r8_oled build_flags = ${heltec_v4_r8_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -186,6 +187,7 @@ build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -300,6 +302,7 @@ extends = heltec_v4_r8_tft build_flags = ${heltec_v4_r8_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -311,6 +314,7 @@ build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index 1719d7333..e04199554 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -1,5 +1,10 @@ #include "TBeam1WBoard.h" +#include +#include +#include +#include + void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -15,31 +20,61 @@ void TBeam1WBoard::begin() { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); - // Initialize fan control (on by default - 1W PA can overheat) + // NTC ADC (PA-adjacent thermistor) + pinMode(NTC_PIN, INPUT); + analogSetPinAttenuation(NTC_PIN, ADC_11db); + analogReadResolution(12); + + // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); digitalWrite(FAN_CTRL_PIN, HIGH); + _fan_on = true; + _temp_c = readNtcTempC(); + startFanTask(); +} + +void TBeam1WBoard::startFanTask() { + if (_fan_task) return; + xTaskCreate(fanTaskThunk, "tbeam1w_fan", 4096, this, 1, &_fan_task); +} + +void TBeam1WBoard::fanTaskThunk(void* arg) { + auto* self = static_cast(arg); + for (;;) { + self->updateFan(); + vTaskDelay(pdMS_TO_TICKS(1000)); + } } void TBeam1WBoard::onBeforeTransmit() { - // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() digitalWrite(LED_PIN, HIGH); // TX LED on + portENTER_CRITICAL(&_fan_mux); + _tx_active = true; + if (_mode == FAN_AUTO && !_stopped) { + setFanOutputLocked(true); + } + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off + portENTER_CRITICAL(&_fan_mux); + if (!_stopped) { + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + } + _tx_active = false; + portEXIT_CRITICAL(&_fan_mux); } uint16_t TBeam1WBoard::getBattMilliVolts() { // T-Beam 1W uses 7.4V battery with voltage divider - // ADC reads through divider - adjust multiplier based on actual divider ratio analogReadResolution(12); uint32_t raw = 0; for (int i = 0; i < 8; i++) { raw += analogRead(BATTERY_PIN); } raw = raw / 8; - // Assuming voltage divider ratio from ADC_MULTIPLIER - // 3.3V reference, 12-bit ADC (4095 max) return static_cast((raw * 3300 * ADC_MULTIPLIER) / 4095); } @@ -48,6 +83,13 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + portENTER_CRITICAL(&_fan_mux); + _stopped = true; + _tx_active = false; + _tx_cooldown_active = false; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); + // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -55,17 +97,254 @@ void TBeam1WBoard::powerOff() { digitalWrite(SX126X_POWER_EN, LOW); radio_powered = false; - // Turn off LED and fan digitalWrite(LED_PIN, LOW); - digitalWrite(FAN_CTRL_PIN, LOW); ESP32Board::powerOff(); } void TBeam1WBoard::setFanEnabled(bool enabled) { - digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); + portENTER_CRITICAL(&_fan_mux); + setFanOutputLocked(enabled && !_stopped); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::isFanEnabled() const { - return digitalRead(FAN_CTRL_PIN) == HIGH; + portENTER_CRITICAL(&_fan_mux); + bool enabled = _fan_on; + portEXIT_CRITICAL(&_fan_mux); + return enabled; +} + +float TBeam1WBoard::readNtcTempC() { + analogReadMilliVolts(NTC_PIN); // settle + uint32_t sum = 0; + for (int i = 0; i < 8; i++) { + uint32_t sample_mv = analogReadMilliVolts(NTC_PIN); + // GPIO14 is ADC2 on ESP32-S3. Wi-Fi/ESP-NOW arbitration failures are + // reported by Arduino as 0 mV; never average a failed sample into a + // plausible-but-low temperature. + if (sample_mv == 0 || sample_mv >= NTC_VCC_MV) return NAN; + sum += sample_mv; + } + float mv = sum / 8.0f; + + // R_ntc = R_fixed * (Vcc - V) / V for 3.3V-NTC-ADC-10k-GND + float r_ntc = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv; + if (r_ntc <= 0.0f) return NAN; + + float temp_k = 1.0f / (1.0f / 298.15f + (1.0f / NTC_B) * logf(r_ntc / NTC_R25)); + return temp_k - 273.15f; +} + +bool TBeam1WBoard::ntcImplausible(float temp_c) const { + return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; +} + +bool TBeam1WBoard::isTxCoolingLocked(uint32_t now) { + if (_tx_active) return true; + if (!_tx_cooldown_active) return false; + if ((int32_t)(now - _tx_until_ms) >= 0) { + _tx_cooldown_active = false; + return false; + } + return true; +} + +int TBeam1WBoard::cooldownSecsLocked() { + if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; + if (!_tx_cooldown_active) return 0; + int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); + if (remain_ms <= 0) { + _tx_cooldown_active = false; + return 0; + } + return (remain_ms + 999) / 1000; +} + +void TBeam1WBoard::setFanOutputLocked(bool enabled) { + _fan_on = enabled; + digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); +} + +void TBeam1WBoard::updateFan() { + float t = readNtcTempC(); + uint32_t now = millis(); + + portENTER_CRITICAL(&_fan_mux); + if (_stopped) { + portEXIT_CRITICAL(&_fan_mux); + return; + } + + _temp_c = t; + bool tx_cooling = isTxCoolingLocked(now); + + bool enabled; + if (_mode == FAN_ON) { + enabled = true; + } else if (_mode == FAN_OFF) { + enabled = false; + } else if (ntcImplausible(t)) { + enabled = true; // fail-safe: treat bad NTC as hot + } else { + // TX cooldown must not latch _thermal_on, or a TX at 30C keeps the fan + // running until the temperature dips under lo. + if (t >= (float)_hi_c) _thermal_on = true; + else if (t < (float)_lo_c) _thermal_on = false; + enabled = _thermal_on || tx_cooling; + } + + setFanOutputLocked(enabled); + portEXIT_CRITICAL(&_fan_mux); +} + +bool TBeam1WBoard::persistKey(const char* key, const char* value) { + return _prefs && _prefs->setByKey(key, value); +} + +bool TBeam1WBoard::parseIntArg(const char* text, int& value) { + if (!text || !*text) return false; + errno = 0; + char* end = nullptr; + long parsed = strtol(text, &end, 10); + if (errno == ERANGE || end == text || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + value = (int)parsed; + return true; +} + +void TBeam1WBoard::loadFanPrefs() { + if (!_prefs) return; + + FanMode mode = FAN_AUTO; + char buf[12]; + buf[0] = 0; + if (_prefs->getByKey("fan", buf, 11)) { + if (strcmp(buf, "off") == 0) mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) mode = FAN_ON; + } + + int lo = FAN_DEFAULT_LO_C; + int hi = FAN_DEFAULT_HI_C; + buf[0] = 0; + if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); + buf[0] = 0; + if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + + portENTER_CRITICAL(&_fan_mux); + _mode = mode; + if (lo >= 0 && hi <= 120 && lo < hi) { + _lo_c = lo; + _hi_c = hi; + } + portEXIT_CRITICAL(&_fan_mux); +} + +void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + loadFanPrefs(); + updateFan(); +} + +const char* TBeam1WBoard::modeNameLocked() const { + if (_mode == FAN_AUTO) return "auto"; + if (_mode == FAN_OFF) return "off"; + return "on"; +} + +bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + (void)sender_timestamp; + + if (strcmp(command, "get fan") == 0) { + portENTER_CRITICAL(&_fan_mux); + int cd = cooldownSecsLocked(); + float temp_c = _temp_c; + bool enabled = _fan_on; + const char* mode = modeNameLocked(); + portEXIT_CRITICAL(&_fan_mux); + if (ntcImplausible(temp_c)) { + sprintf(reply, "> %s n/a fan=%s cd=%ds", mode, enabled ? "on" : "off", cd); + } else { + sprintf(reply, "> %s %.1fC fan=%s cd=%ds", + mode, (double)temp_c, enabled ? "on" : "off", cd); + } + return true; + } + + if (strncmp(command, "set fan.lo ", 11) == 0) { + int lo; + portENTER_CRITICAL(&_fan_mux); + int hi_limit = _hi_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= hi_limit || lo > 100) { + strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); + } else if (!persistKey("fan_lo", &command[11])) { + strcpy(reply, "Error: failed to save fan.lo"); + } else { + portENTER_CRITICAL(&_fan_mux); + _lo_c = lo; + portEXIT_CRITICAL(&_fan_mux); + sprintf(reply, "OK - fan.lo %d", lo); + } + return true; + } + + if (strncmp(command, "set fan.hi ", 11) == 0) { + int hi; + portENTER_CRITICAL(&_fan_mux); + int lo_limit = _lo_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], hi) || hi <= lo_limit || hi > 120) { + strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); + } else if (!persistKey("fan_hi", &command[11])) { + strcpy(reply, "Error: failed to save fan.hi"); + } else { + portENTER_CRITICAL(&_fan_mux); + _hi_c = hi; + portEXIT_CRITICAL(&_fan_mux); + sprintf(reply, "OK - fan.hi %d", hi); + } + return true; + } + + if (strncmp(command, "set fan ", 8) == 0) { + const char* arg = &command[8]; + if (strcmp(arg, "on") == 0) { + if (!persistKey("fan", "on")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_ON; + if (!_stopped) setFanOutputLocked(true); + portEXIT_CRITICAL(&_fan_mux); + strcpy(reply, "OK - fan on"); + } + } else if (strcmp(arg, "off") == 0) { + if (!persistKey("fan", "off")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_OFF; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); + strcpy(reply, "OK - fan off"); + } + } else if (strcmp(arg, "auto") == 0) { + if (!persistKey("fan", "auto")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_AUTO; + portEXIT_CRITICAL(&_fan_mux); + updateFan(); + strcpy(reply, "OK - fan auto"); + } + } else { + strcpy(reply, "Error: fan must be on, off, or auto"); + } + return true; + } + + return false; } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index d999dfd4c..429b7bd1c 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "variant.h" @@ -28,18 +30,48 @@ // - Battery must support 2A+ discharge for high-power TX class TBeam1WBoard : public ESP32Board { +public: + enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; + private: bool radio_powered = false; + bool _stopped = false; + KeyValueStore* _prefs = nullptr; + FanMode _mode = FAN_AUTO; + int _lo_c = FAN_DEFAULT_LO_C; + int _hi_c = FAN_DEFAULT_HI_C; + bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this + bool _fan_on = true; + float _temp_c = NAN; + bool _tx_active = false; + bool _tx_cooldown_active = false; + uint32_t _tx_until_ms = 0; + TaskHandle_t _fan_task = nullptr; + mutable portMUX_TYPE _fan_mux = portMUX_INITIALIZER_UNLOCKED; + + void startFanTask(); + void updateFan(); + void setFanOutputLocked(bool enabled); + float readNtcTempC(); + int cooldownSecsLocked(); + bool isTxCoolingLocked(uint32_t now); + bool ntcImplausible(float temp_c) const; + bool persistKey(const char* key, const char* value); + static bool parseIntArg(const char* text, int& value); + void loadFanPrefs(); + const char* modeNameLocked() const; + static void fanTaskThunk(void* arg); public: void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; - // Fan control methods void setFanEnabled(bool enabled); bool isFanEnabled() const; }; diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 0f604fac4..ab9c9e67c 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -153,6 +153,7 @@ extends = LilyGo_TBeam_1W build_flags = ${LilyGo_TBeam_1W.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -165,6 +166,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index f6807e56b..528459a4c 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -77,11 +77,21 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 3.0 -// NTC temperature sensor +// NTC thermistor (Murata NCP18XH103F03RB, 10k, B25/50=3380K) +// Divider: 3.3V -> NTC -> GPIO14 -> 10k pull-down -> GND (Vadc rises with temp) #define NTC_PIN 14 +#define NTC_B 3380.0f +#define NTC_R25 10000.0f +#define NTC_R_FIXED 10000.0f +#define NTC_VCC_MV 3300.0f -// Fan control +// Fan control (GPIO41). NTC is PA-adjacent PCB temp, not die temp, so trip +// well below the SX1262/ESP32 85C operating limit. Hardware testing confirmed +// that this fan/MOSFET path is on/off; PWM below 100% does not spin the fan. #define FAN_CTRL_PIN 41 +#define FAN_TX_COOLDOWN_MS 15000 +#define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) +#define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 8bfc4093a..af4a8eafa 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -148,6 +148,7 @@ extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -160,6 +161,7 @@ build_flags = ; -D CORE_DEBUG_LEVEL=4 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 1aea74d28..dc60b6f75 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -132,6 +132,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D WIFI_SSID='"ssid"' @@ -140,6 +141,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=128 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/src/helpers/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h similarity index 97% rename from src/helpers/MeshadventurerBoard.h rename to variants/meshadventurer/MeshadventurerBoard.h index 0325161d5..d9e88e1bc 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -13,7 +13,7 @@ #define PIN_VBAT_READ 35 -#include "ESP32Board.h" +#include "helpers/ESP32Board.h" class MeshadventurerBoard : public ESP32Board { diff --git a/variants/meshadventurer/target.h b/variants/meshadventurer/target.h index d59b3aee8..0749565d3 100644 --- a/variants/meshadventurer/target.h +++ b/variants/meshadventurer/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -44,4 +44,3 @@ extern MASensorManager sensors; bool radio_init(); mesh::LocalIdentity radio_new_identity(); - diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 1168f10a0..ffab9d57c 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -27,8 +27,8 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_VEXT_EN=45 -D PIN_VEXT_EN_ACTIVE=HIGH - -D LORA_TX_POWER=4 - -D MAX_LORA_TX_POWER=4 ; GC1109 datasheet says max input at TX port is +5dbm, confirmed full saturation seems to occur at around 3-4dbm + -D LORA_TX_POWER=5 ; default to 5, which gives ~22dbm + -D MAX_LORA_TX_POWER=13 ; tested with tinySA, 13 gives ~28dbm -D PIN_GPS_RX=38 -D PIN_GPS_TX=39 -D PIN_GPS_RESET=42 @@ -171,6 +171,7 @@ extends = meshnology_w12 build_flags = ${meshnology_w12.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -184,6 +185,7 @@ build_src_filter = ${meshnology_w12.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/muzi_base/MuziBaseBoard.cpp b/variants/muzi_base/MuziBaseBoard.cpp new file mode 100644 index 000000000..a104c550b --- /dev/null +++ b/variants/muzi_base/MuziBaseBoard.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include "MuziBaseBoard.h" + +#ifdef NRF52_POWER_MANAGEMENT +const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +void MuziBaseBoard::initiateShutdown(uint8_t reason) { + // Disable LoRa module power before shutdown + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + enterSystemOff(reason); +} +#endif // NRF52_POWER_MANAGEMENT + +void MuziBaseBoard::begin() { + NRF52BoardDCDC::begin(); + pinMode(PIN_VBAT_READ, INPUT); + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + // output latches default to HIGH, so pull these low right after enabling + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + // 12V rail is only needed for the superIO display + pinMode(SCREEN_12V_ENABLE, OUTPUT); +#ifdef MUZI_BASE_SUPERIO + digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display + delay(250); +#else + digitalWrite(SCREEN_12V_ENABLE, LOW); +#endif + Wire.begin(); + // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. + +#ifdef PIN_USER_BTN + pinMode(PIN_USER_BTN, INPUT_PULLUP); +#endif + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + +// #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) +// Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +// #endif +#ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); +#endif +} diff --git a/variants/muzi_base/MuziBaseBoard.h b/variants/muzi_base/MuziBaseBoard.h new file mode 100644 index 000000000..a5a4abbe2 --- /dev/null +++ b/variants/muzi_base/MuziBaseBoard.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +// The Muzi Base PCB ships in two radio flavors. Keep the user-facing identity +// (OTA/DFU name and reported manufacturer) distinct per radio even though the +// board logic is shared. +#if defined(USE_SX1262) + #define MUZI_BASE_OTA_NAME "MUZI_BASE_UNO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Uno" +#else + #define MUZI_BASE_OTA_NAME "MUZI_BASE_DUO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Duo" +#endif + +class MuziBaseBoard : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + MuziBaseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + void begin(); + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(1); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + // ADC_MULTIPLIER is the voltage divider ratio + return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return MUZI_BASE_MFR_NAME; + } +}; diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini new file mode 100644 index 000000000..72a3ecac2 --- /dev/null +++ b/variants/muzi_base/platformio.ini @@ -0,0 +1,322 @@ +; ============================================================================ +; Muzi Base (MuziWorks) +; +; One nRF52840 board, shipped in two radio flavors. The radio is selected per +; environment; all board logic lives in variants/muzi_base and is shared: +; muzi_base_duo_* -> LR1121 (USE_LR1121) +; muzi_base_uno_* -> SX1262 (USE_SX1262) +; ============================================================================ + +[muzi_base_common] +extends = nrf52_base +board = muzi_base +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/muzi_base + -I src/helpers/ui + -D MUZI_BASE + -D NRF52_POWER_MANAGEMENT + -D PIN_USER_BTN=PIN_BUTTON1 + -D USER_BTN_PRESSED=LOW + -D PIN_STATUS_LED=35 + -D LORA_TX_POWER=22 + -D P_LORA_BUSY=LORA_BUSY + -D P_LORA_SCLK=LORA_SCLK + -D P_LORA_NSS=LORA_NSS + -D P_LORA_DIO_1=LORA_DIO_1 + -D P_LORA_MISO=LORA_MISO + -D P_LORA_MOSI=LORA_MOSI + -D P_LORA_RESET=LORA_RESET + -D QSPIFLASH=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/muzi_base> +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = + ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + +; --------------------------------------------------------------------------- +; Radio selection (Duo = LR1121, Uno = SX1262). The matching radio pins / TCXO +; / RF-switch defines are picked up in variant.h via USE_LR1121 / USE_SX1262. +; --------------------------------------------------------------------------- +[muzi_base_duo] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_LR1121 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=3.0 + +[muzi_base_uno] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + +; =========================================================================== +; Duo (LR1121) environments +; =========================================================================== +[env:muzi_base_duo_repeater] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_room_server] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_companion_radio_usb] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_duo.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_duo_companion_radio_ble] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_duo_superIO] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -D MUZI_BASE_SUPERIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_duo_superIO_companion_radio_ble] +extends = muzi_base_duo_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo_superIO.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_duo_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_duo_superIO.lib_deps} + +; =========================================================================== +; Uno (SX1262) environments +; =========================================================================== +[env:muzi_base_uno_repeater] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_room_server] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_companion_radio_usb] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_uno.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_uno_companion_radio_ble] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_uno_superIO] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -D MUZI_BASE_SUPERIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_uno_superIO_companion_radio_ble] +extends = muzi_base_uno_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno_superIO.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_uno_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_uno_superIO.lib_deps} diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp new file mode 100644 index 000000000..5bd846c98 --- /dev/null +++ b/variants/muzi_base/target.cpp @@ -0,0 +1,162 @@ +#include +#include +#include "target.h" +#include "variant.h" + +MuziBaseBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + MuziBaseSensorManager sensors = MuziBaseSensorManager(nmea); +#else + MuziBaseSensorManager sensors; +#endif + +// user button for the N-click power-off. single-click mode (multiclick off) so +// each press is its own CLICK; we count them ourselves. reliable across the +// idle-sleep loop because it's the same MomentaryButton the UI navigates with. +static MomentaryButton pwr_btn(PIN_USER_BTN, 0, true, true, false); + +// power off, arming the user button as a wake source first so a later press +// turns it back on. wait for the button to release (the 5th click already fires +// on release, so this returns quickly), then set nRF52 GPIO SENSE (press = LOW); +// it survives into SYSTEMOFF because shutdownPeripherals() doesn't touch this pin. +static void muziPowerOff() { + uint32_t t0 = millis(); + while (digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED && (millis() - t0) < 3000) { delay(5); } + delay(50); // settle + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + board.powerOff(); // shutdownPeripherals() + SYSTEMOFF +} + +bool MuziBaseSensorManager::begin() { + pwr_btn.begin(); + bool ok = EnvironmentSensorManager::begin(); +#if ENV_INCLUDE_GPS + pinMode(PIN_GPS_SWITCH, INPUT); + _last_gps_sw = digitalRead(PIN_GPS_SWITCH); // initial gps state from the switch + if (_last_gps_sw == HIGH) start_gps(); else stop_gps(); +#endif + return ok; +} + +void MuziBaseSensorManager::loop() { + // user button: USER_BTN_POWER_OFF_CLICKS quick clicks -> power off + if (pwr_btn.check() == BUTTON_EVENT_CLICK) { + unsigned long t = millis(); + if (t - _pwr_last_click > 2000) _pwr_clicks = 0; // restart if too slow + _pwr_last_click = t; + if (++_pwr_clicks >= USER_BTN_POWER_OFF_CLICKS) muziPowerOff(); + } +#if ENV_INCLUDE_GPS + unsigned long now = millis(); + if (now > _next_sw_check) { // check the mode switch ~once a sec + _next_sw_check = now + 1000; + int sw = digitalRead(PIN_GPS_SWITCH); + if (sw != _last_gps_sw) { + _last_gps_sw = sw; + if (sw == HIGH) start_gps(); else stop_gps(); + } + } +#endif + EnvironmentSensorManager::loop(); +} + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +#if defined(USE_LR1121) + #ifndef LORA_CR + #define LORA_CR 5 + #endif + + #ifdef RF_SWITCH_TABLE + static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC + }; + + static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, + }; + #endif +#endif // USE_LR1121 + +bool radio_init() { + //rtc_clock.begin(Wire); + +#if defined(USE_LR1121) + #ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; + #else + float tcxo = 1.6f; + #endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + + #ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); + #endif + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + return true; // success +#else // USE_SX1262 + // CustomSX1262::std_init() configures the SPI pins, runs begin() with the + // TCXO-voltage fallback, sets CRC, current limit, the DIO2 RF switch, and + // RX boosted gain from the SX126X_* build flags. + return radio.std_init(&SPI); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h new file mode 100644 index 000000000..45cd595fd --- /dev/null +++ b/variants/muzi_base/target.h @@ -0,0 +1,64 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "MuziBaseBoard.h" +#if defined(USE_LR1121) + #include +#elif defined(USE_SX1262) + #include +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif +#include +#include +#include +#include +#include // Added: Include for EnvironmentSensorManager +#include + + +#ifdef MUZI_BASE_SUPERIO + #include + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#elif defined(DISPLAY_CLASS) + #include "helpers/ui/NullDisplayDriver.h" + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern MuziBaseBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +// muzi sensor manager, polled from main loop via sensors.loop(): +// - user button: USER_BTN_POWER_OFF_CLICKS quick presses -> power off (all builds) +// - gps mode switch on/off (superIO only, see thinknode_m1) +class MuziBaseSensorManager : public EnvironmentSensorManager { + unsigned long _pwr_last_click = 0; + uint8_t _pwr_clicks = 0; +#if ENV_INCLUDE_GPS + int _last_gps_sw = -1; + unsigned long _next_sw_check = 0; +#endif +public: +#if ENV_INCLUDE_GPS + MuziBaseSensorManager(LocationProvider& location) : EnvironmentSensorManager(location) {} +#else + MuziBaseSensorManager() {} +#endif + bool begin() override; + void loop() override; +}; +extern MuziBaseSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp new file mode 100644 index 000000000..cbde3355b --- /dev/null +++ b/variants/muzi_base/variant.cpp @@ -0,0 +1,62 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, +// P1 pins. + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, +}; diff --git a/variants/muzi_base/variant.h b/variants/muzi_base/variant.h new file mode 100644 index 000000000..b007c1f8b --- /dev/null +++ b/variants/muzi_base/variant.h @@ -0,0 +1,169 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_VBAT_READ (31) // P0.31 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.537 +#define ADC_RESOLUTION 14 +#define PIN_BATTERY_CHARGING (32+2) // P1.02 STAT2 +#define PIN_CHARGER_FAULT (27) // P0.27 STAT1 this pin is disabled on meshtastic. +// BQ25185 has 2 status pins: STAT1 and STAT2. Both are high when not charging. STAT1 high, STAT2 low: charging. Recoverable fault: STAT1 low, STAT2 high. Unrecoverable fault: both low. +// We only need to detect charging vs not charging, but someone else can use the fault pin to log when the battery gets too hot or cold. + +// Power management boot protection threshold (millivolts) +#define PWRMGT_VOLTAGE_BOOTLOCK 3100 // Won't boot below this voltage (mV). BB15 battery min voltage is 3v, 3100mV is minimum batt in meshtastic code. + +// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) +#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT_READ +#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V) was the default on RAK4631. should still apply here. + +// Other pins +#define PIN_AREF (-1) +#define SCREEN_12V_ENABLE (23) // SH1107 OLED controller has a pin that needs to be enabled to turn on the screen. + +static const uint8_t AREF = (PIN_AREF); // not used + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (19) // P0.19 used for GPS RX +#define PIN_SERIAL1_TX (20) // P0.20 used for GPS TX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE1_SDA (4) // P0.4 +#define PIN_WIRE1_SCL (6) // P0.6 +#define PIN_WIRE_SDA (24) // P0.24 OLED I2C +#define PIN_WIRE_SCL (25) // P0.25 OLED I2C +#define I2C_NO_RESCAN +// #define I2C_NO_RESCAN +// #define HAS_QMA6100P +// #define QMA_6100P_INT_PIN (34) // P1.2 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (32+15) // internally connected to p1.15 +#define PIN_SPI_MOSI (32+14) // internally connected to p1.14 +#define PIN_SPI_SCK (32+13) // internally connected to p1.13 +#define PIN_SPI_NSS (32+12) // internally connected to p1.12 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (35) +#define LED_BLUE (-1) // P1.04 turned off, because the blue LED was annoying. +// #define LED_GREEN (35) // P1.03 +#define LED_PIN LED_BUILTIN + +#define LED_STATE_ON LOW + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons +#define PIN_BUTTON1 (10) // P0.10 Menu / User Button | on superIO, this is in the center of the "D-Pad", but it's also the button on the Uno/Duo. +#define PIN_BUTTON2 (21) // Joystick Up +#define PIN_BUTTON3 (17) // Joystick Down +#define PIN_BUTTON4 (37) // Joystick Left +#define PIN_BUTTON5 (16) // Joystick Right +#define PIN_BUTTON6 (15) // Back / Cancel Button. +#define JOYSTICK_PRESS PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define PIN_BACK_BTN PIN_BUTTON6 + +// quick user-button presses that power the device off (per Muzi's docs) +#ifndef USER_BTN_POWER_OFF_CLICKS +#define USER_BTN_POWER_OFF_CLICKS 5 +#endif + +//////////////////////////////////////////////////////////////////////////////// +// LoRa radio +// +// The Muzi Base PCB is populated with one of two radios: +// * Base Uno -> SX1262 (USE_SX1262) +// * Base Duo -> LR1121 (USE_LR1121) +// Both share NSS/SCLK/MISO/MOSI/RESET/BUSY. The only pin difference is the +// radio IRQ (DIO1): the SX1262 routes it to P1.06, the LR1121 to P1.08. +// (see Meshtastic muzi_base variant, which defines both radios on this board) + +#define LORA_NSS (PIN_SPI_NSS) // P1.12 +#define LORA_RESET (32+10) // P1.10 +#define LORA_BUSY (32+11) // P1.11 +#define LORA_SCLK (PIN_SPI_SCK) // P1.13 +#define LORA_MISO (PIN_SPI_MISO) // P1.15 +#define LORA_MOSI (PIN_SPI_MOSI) // P1.14 + +// only the IRQ pin differs per radio. rf-switch/tcxo defines are in platformio.ini +#if defined(USE_LR1121) + #define LORA_DIO_1 (32+8) // P1.08 LR1121 IRQ/DIO1 +#elif defined(USE_SX1262) + #define LORA_DIO_1 (32+6) // P1.06 SX1262 IRQ/DIO1 +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif + +//////////////////////////////////////////////////////////////////////////////// +// QSPI Flash +#define PIN_QSPI_SCK (0 + 3) +#define PIN_QSPI_CS (0 + 26) +#define PIN_QSPI_IO0 (0 + 30) +#define PIN_QSPI_IO1 (0 + 29) +#define PIN_QSPI_IO2 (0 + 28) +#define PIN_QSPI_IO3 (0 + 2) + +#define EXTERNAL_FLASH_DEVICES W25Q128JVPQ +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// GPS +#define HAS_GPS 1 +#define PIN_GPS_RX PIN_SERIAL1_RX +#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_EN (32+1) // P1.01 PWR_IO2 on schematic. gps power, driven by the sensor manager. + +// superIO 3-position mode switch (pins from the meshtastic muzi_base variant). +// "Mode 2" reads HIGH on PIN_GPS_SWITCH and turns the gps on. +#define SWITCH_MODE1 (32+9) // P1.09 +#define SWITCH_MODE2 (12) // P0.12 +#define PIN_GPS_SWITCH SWITCH_MODE2 + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_PIN (22) // P0.22 same load switch design as GPS_EN. diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 112181df2..0f8d31de8 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -143,6 +143,7 @@ extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 @@ -154,6 +155,7 @@ build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 1161743ea..c7e7b0765 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -140,6 +140,7 @@ extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 @@ -150,6 +151,7 @@ build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 8bd3c5977..5f3b905bd 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -172,6 +172,7 @@ extends = rak3112 build_flags = ${rak3112.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -182,6 +183,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e719226..c285e3ec5 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 + -D RAK_BOARD -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 988278860..e0c24759b 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 diff --git a/variants/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index 708e96558..e64590f27 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -2,6 +2,7 @@ #include #include +#include // built-ins #define PIN_VBAT_READ 26 @@ -16,6 +17,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + void onBeforeTransmit() override { digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on } diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 0fe8c4369..9814f3971 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -67,34 +67,60 @@ lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 lib_ignore = BLE -; [env:PicoW_companion_radio_ble] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_ble] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 -; [env:PicoW_companion_radio_wifi] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D WIFI_DEBUG_LOGGING=1 -; -D WIFI_SSID='"myssid"' -; -D WIFI_PWD='"mypwd"' -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +; USB + WiFi + BLE together; the interface manager fans frames out to all of them +[env:PicoW_companion_radio] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE + -D ENABLE_WIFI_INTERFACE + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 +; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) +; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:PicoW_companion_radio_wifi] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D ENABLE_WIFI_INTERFACE + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 +lib_ignore = BLE [env:PicoW_terminal_chat] extends = rpi_picow diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index bdb7ee0c3..c1ae765fe 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -224,6 +224,7 @@ extends = Station_G2 build_flags = ${Station_G2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -234,6 +235,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp index dd863aca6..277c2b8b7 100644 --- a/variants/station_g3_esp32/StationG3Board.cpp +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -21,10 +21,6 @@ bool StationG3Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemLna() const { - return loRaFEMControl.canControlLNA(); -} - bool StationG3Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } @@ -37,10 +33,86 @@ bool StationG3Board::setLoRaFemPaGainEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemPaGain() const { - return loRaFEMControl.canControlPAGain(); -} - bool StationG3Board::isLoRaFemPaGainEnabled() const { return loRaFEMControl.isPAGainEnabled(); } + +void StationG3Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_rxgain", gain, 7); // get initial values + setLoRaFemLnaEnabled(strcmp(gain, "1") == 0); + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool StationG3Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.canControlLNA()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.canControlLNA()) { + 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; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + 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"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index 52628eb6c..b4c5838c8 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -6,6 +6,12 @@ #include "LoRaFEMControl.h" class StationG3Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const; public: LoRaFEMControl loRaFEMControl; @@ -25,6 +31,10 @@ public: } } + void attachDynamicPrefs(KeyValueStore* prefs); + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void setPrimaryLNAEnable(bool enabled) { loRaFEMControl.setLNAEnable(enabled); } @@ -43,13 +53,6 @@ public: loRaFEMControl.setRxModeEnable(); } - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; - bool setLoRaFemPaGainEnabled(bool enable) override; - bool canControlLoRaFemPaGain() const override; - bool isLoRaFemPaGainEnabled() const override; - void powerOff() override; uint16_t getBattMilliVolts() override { diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 074d6a2ed..7006d939e 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -139,6 +139,7 @@ extends = Station_G3_ESP32 build_flags = ${Station_G3_ESP32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -149,6 +150,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index 583f913c9..862c71543 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -176,6 +176,7 @@ extends = ThinkNode_M2 build_flags = ${ThinkNode_M2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -184,6 +185,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m4/ThinkNodeM4Board.cpp b/variants/thinknode_m4/ThinkNodeM4Board.cpp new file mode 100644 index 000000000..4a144a3a4 --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.cpp @@ -0,0 +1,46 @@ +#include +#include "ThinkNodeM4Board.h" +#include + +#include + +void ThinkNodeM4Board::begin() { + NRF52Board::begin(); + btn_prev_state = HIGH; + + Wire.begin(); + battery_serial->begin(4800); + + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM4Board::getBattMilliVolts() { + int tentatives = 10; + uint8_t data[5]; + uint8_t b; + + if (battery_serial->available() < 10) + return bat_level_mv; + + // discard old data + while (battery_serial->available() > 10) + battery_serial->read(); + + // synchronize + while((b = battery_serial->read()) != 0xFE) { + if (tentatives-- == 0) { + MESH_DEBUG_PRINTLN("Could not find battery frame start %x", b); + return bat_level_mv; + } + } + battery_serial->readBytes(data, 5); + + if (data[4] != 0xFD) { + MESH_DEBUG_PRINTLN("Invalid battery frame end %x", data[4]); + return bat_level_mv; + } + bat_level_percent = data[0]; + //MESH_DEBUG_PRINTLN("Battery level %d\%", bat_level_percent); + bat_level_mv = 2 * (data[1]*1000. + data[2]*10. + data[3]/10.); + return bat_level_mv; +} diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h new file mode 100644 index 000000000..c2cae54b6 --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +class ThinkNodeM4Board : public NRF52BoardDCDC { +protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + uint8_t btn_prev_state; + HardwareSerial * battery_serial; + uint16_t bat_level_mv = 0; + uint8_t bat_level_percent =0; + +public: + ThinkNodeM4Board() : NRF52Board("THINKNODE_M4_OTA"), battery_serial(&Serial2) {} + void begin(); + uint16_t getBattMilliVolts() override; + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off + } +#endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode M4"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == USER_BTN_PRESSED) ? 1 : -1; + } + #endif + return 0; + } + + void shutdownPeripherals() override { + // shutdown common peripherals + NRF52Board::shutdownPeripherals(); + + #ifdef LED_STATUS + digitalWrite(LED_STATUS, HIGH); + #endif + #ifdef BUTTON_PIN + while(!digitalRead(BUTTON_PIN)); + #endif + #ifdef LED_STATUS + digitalWrite(LED_STATUS, LOW); + #endif + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); + digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + #endif + } +}; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini new file mode 100644 index 000000000..06762f88d --- /dev/null +++ b/variants/thinknode_m4/platformio.ini @@ -0,0 +1,130 @@ +[ThinkNode_M4] +extends = nrf52_base +board = thinknode_m4 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m4 + -I src/helpers/ui + -D THINKNODE_M4 + -D PIN_USER_BTN=4 + -D PIN_STATUS_LED=LED_STATUS + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D P_LORA_BUSY=26 + -D P_LORA_SCLK=6 + -D P_LORA_NSS=27 + -D P_LORA_DIO_1=12 + -D P_LORA_MISO=8 + -D P_LORA_MOSI=7 + -D P_LORA_RESET=40 + -D P_LORA_TX_LED=LED_TX + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 + -D ENV_INCLUDE_GPS=1 +; -D MESH_DEBUG=1 +; -D GPS_NMEA_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/thinknode_m4> + + +debug_tool = stlink +upload_protocol = nrfutil +lib_deps= ${nrf52_base.lib_deps} + +[env:ThinkNode_M4_repeater] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_room_server] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RF_SWITCH_TABLE +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_companion_radio_usb] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_companion_radio_ble] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_kiss_modem] +extends = ThinkNode_M4 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 diff --git a/variants/thinknode_m4/target.cpp b/variants/thinknode_m4/target.cpp new file mode 100644 index 000000000..7da3f400d --- /dev/null +++ b/variants/thinknode_m4/target.cpp @@ -0,0 +1,84 @@ +#include +#include "target.h" +#include + +ThinkNodeM4Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +#ifdef RF_SWITCH_TABLE +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW , LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW , HIGH }}, + { LR11x0::MODE_TX_HF, {LOW , LOW }}, + { LR11x0::MODE_GNSS, {LOW , LOW }}, + { LR11x0::MODE_WIFI, {LOW , LOW }}, + END_OF_MODE_TABLE, +}; +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/thinknode_m4/target.h b/variants/thinknode_m4/target.h new file mode 100644 index 000000000..1135f5e89 --- /dev/null +++ b/variants/thinknode_m4/target.h @@ -0,0 +1,26 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "ThinkNodeM4Board.h" +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern ThinkNodeM4Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp new file mode 100644 index 000000000..fbdb08e2a --- /dev/null +++ b/variants/thinknode_m4/variant.cpp @@ -0,0 +1,94 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = +{ + 0xff, // P0.00 - LFXO (do not use) + 0xff, // P0.01 - LFXO (do not use) + 2, // P0.02 + 3, // P0.03 + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 + 12, // P0.12 + 13, // P0.13 + 14, // P0.14 + 15, // P0.15 + 16, // P0.16 + 17, // P0.17 + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24 + 25, // P0.25 + 26, // P0.26 + 27, // P0.27 + 28, // P0.28 + 29, // P0.29 + 30, // P0.30 + 31, // P0.31 + 32, // P1.00 + 33, // P1.01 + 34, // P1.02 + 35, // P1.03 + 36, // P1.04 + 37, // P1.05 + 38, // P1.06 + 39, // P1.07 + 40, // P1.08 + 41, // P1.09 + 42, // P1.10 + 43, // P1.11 + 44, // P1.12 + 45, // P1.13 + 46, // P1.14 + 47, // P1.15 +}; + +void initVariant() +{ +/* TODO */ + pinMode(PIN_PWR_EN, OUTPUT); + pinMode(I2C_POWER, OUTPUT); + + digitalWrite(PIN_PWR_EN, HIGH); + digitalWrite(I2C_POWER, I2C_POWER_ACTIVE); + + pinMode(LED_STATUS, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BAT1, OUTPUT); + pinMode(LED_BAT2, OUTPUT); + pinMode(LED_BAT3, OUTPUT); + pinMode(LED_BAT4, OUTPUT); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + pinMode(BUTTON_PIN, INPUT_PULLUP); + + pinMode(PIN_GPS_POWER, OUTPUT); + pinMode(PIN_GPS_EN, OUTPUT); + + // Power on gps but in standby + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); +} diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h new file mode 100644 index 000000000..6e355d7f8 --- /dev/null +++ b/variants/thinknode_m4/variant.h @@ -0,0 +1,106 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power + +#define EXT_CHRG_DETECT (38) +// Power to radio +#define PIN_PWR_EN (11) + +// I2C bus power +#define I2C_POWER (32) +#define I2C_POWER_ACTIVE LOW + +#define PIN_BAT_RX (5) +#define PIN_BAT_TX (30) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX PIN_BAT_TX +#define PIN_SERIAL2_TX PIN_BAT_RX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) +#define I2C_NO_RESCAN + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) +#define PIN_SPI_NSS (27) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BLUE (-1) // disable blue led +#define LED_STATUS (13) // blue +#define LED_PIN (41) // red +#define LED_TX LED_PIN +#define LED_BUILTIN LED_BLUE +#define LED_STATE_ON HIGH + +#define LED_BAT1 (15) +#define LED_BAT2 (17) +#define LED_BAT3 (34) +#define LED_BAT4 (36) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (4) +#define BUTTON_PIN PIN_BUTTON1 +#define USER_BTN_PRESSED LOW + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define PIN_GPS_RX (44) +#define PIN_GPS_TX (46) + +#define PIN_GPS_POWER (14) +#define GPS_POWER_ACTIVE LOW +#define PIN_GPS_EN (43) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_ACTIVE HIGH diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 5e85b6498..a3f348b33 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -190,6 +190,7 @@ extends = ThinkNode_M5 build_flags = ${ThinkNode_M5.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -198,6 +199,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M5.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 7d9892e5f..128b12441 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -98,11 +98,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> @@ -135,6 +137,7 @@ extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=NullDisplayDriver @@ -145,6 +148,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> @@ -167,6 +171,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp new file mode 100644 index 000000000..2ca393d98 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -0,0 +1,38 @@ +#include +#include + +#include "ThinkNodeM8Board.h" + +#ifdef THINKNODE_M8 + +void ThinkNodeM8Board::begin() { + NRF52Board::begin(); + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM8Board::getBattMilliVolts() { + int adcvalue = 0; + + digitalWrite(ADC_EN, HIGH); + analogReference(AR_INTERNAL_2_4); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + digitalWrite(ADC_EN, LOW); + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} +#endif diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h new file mode 100644 index 000000000..73aeb1f5d --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +// built-ins +#define VBAT_MV_PER_LSB (0.5859375F) // 2.4V ADC range and 12-bit ADC resolution = 2400mV/4096 + +#define VBAT_DIVIDER (0.57F) // 150K + 150K voltage divider on VBAT +#define VBAT_DIVIDER_COMP (1.75F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (4) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class ThinkNodeM8Board : public NRF52Board { +public: + ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {} + void begin(); + uint16_t getBattMilliVolts() override; + + #if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } + #endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode-M8"; + } + + void shutdownPeripherals() override { + // power off board + NRF52Board::shutdownPeripherals(); + + // make sure every gate is closed + digitalWrite(DISP_EN, LOW); + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(SX126X_ANT_SW, LOW); + digitalWrite(ADC_EN, LOW); + + #ifdef PIN_BUTTON1 // Use BTN to go out of sleep + nrf_gpio_cfg_sense_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + #endif + } +}; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini new file mode 100644 index 000000000..db19e4466 --- /dev/null +++ b/variants/thinknode_m8/platformio.ini @@ -0,0 +1,160 @@ +[ThinkNode_M8] +extends = nrf52_base +board = thinknode_m8 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m8 + -D THINKNODE_M8=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=25 + -D P_LORA_DIO_2=32 + -D P_LORA_NSS=21 + -D P_LORA_RESET=24 + -D P_LORA_BUSY=32 # DIO2 + -D P_LORA_SCLK=19 + -D P_LORA_MISO=22 + -D P_LORA_MOSI=20 + -D UI_SHOW_CLOCK=1 + -D UI_HAS_ROTARY_INPUT=1 + -D UI_HAS_NAV_INPUT=1 + -D UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 + -D DISABLE_DIAGNOSTIC_OUTPUT + -D SX126X_POWER_EN=37 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/thinknode_m8> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 +debug_tool = jlink +upload_protocol = nrfutil + +[env:ThinkNode_M8_repeater] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_room_server] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_room_server/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_companion_radio_ble] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 +# -D BLE_DEBUG_LOGGING=1 + -D DISPLAY_ROTATION=4 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=33 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D QSPIFLASH=1 + -D ENV_INCLUDE_GPS=1 +; -D GPS_NMEA_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.9 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_companion_radio_usb] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_ROTATION=4 + -D QSPIFLASH=1 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=6 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.2 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_kiss_modem] +extends = ThinkNode_M8 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp new file mode 100644 index 000000000..ac4ac100e --- /dev/null +++ b/variants/thinknode_m8/target.cpp @@ -0,0 +1,36 @@ +#include +#include "target.h" +#include +#include +#include + +ThinkNodeM8Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h new file mode 100644 index 000000000..c3402a01a --- /dev/null +++ b/variants/thinknode_m8/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include + #include +#endif + +extern ThinkNodeM8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern RotaryInputGPIO rotary_input; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m8/variant.cpp b/variants/thinknode_m8/variant.cpp new file mode 100644 index 000000000..e15c08420 --- /dev/null +++ b/variants/thinknode_m8/variant.cpp @@ -0,0 +1,39 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const int MISO = PIN_SPI1_MISO; +const int MOSI = PIN_SPI1_MOSI; +const int SCK = PIN_SPI1_SCK; + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + pinMode(DISP_EN, OUTPUT); + digitalWrite(DISP_EN, HIGH); + + pinMode(PIN_BUTTON1, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_A, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_B, INPUT_PULLDOWN); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + + // shutdown gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // disable at startup + + pinMode(SX126X_ANT_SW, OUTPUT); // ANT_SW + digitalWrite(SX126X_ANT_SW, HIGH); + + pinMode(ADC_EN, OUTPUT); + digitalWrite(ADC_EN, LOW); + +} diff --git a/variants/thinknode_m8/variant.h b/variants/thinknode_m8/variant.h new file mode 100644 index 000000000..9c36c772b --- /dev/null +++ b/variants/thinknode_m8/variant.h @@ -0,0 +1,141 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_TXCO (21) +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_PWR_EN (13) // I2C + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (1.75F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (2.4) + +#define ADC_EN (40) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (22) +#define PIN_SPI_MOSI (20) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (21) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs +#define LED_BLUE (-1) + +#define LED_BUILTIN LED_BLUE +#define LED_PIN LED_BUILTIN +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) +#define PIN_BUTTON1_A (8) // Rotary button/Encoder +#define PIN_BUTTON1_B (41) +#define BUTTON_PIN PIN_BUTTON1 + +#define PIN_BUTTON2 (12) +#define BUTTON_PIN2 PIN_BUTTON2 + +#define PIN_USER_BTN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_ANT_SW (23) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +//////////////////////////////////////////////////////////////////////////////// +// SPI1 +#define PIN_SPI1_NSS (30) +#define PIN_SPI1_SCK (31) +#define PIN_SPI1_MOSI (29) +#define PIN_SPI1_MISO (-1) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +//////////////////////////////////////////////////////////////////////////////// +// QSPI +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI +#define PIN_QSPI_SCK (46) +#define PIN_QSPI_CS (47) +#define PIN_QSPI_IO0 (44) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (45) // MISO if using two bit interface +#define PIN_QSPI_IO2 (7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (5) // HOLD if using two bit interface (i.e. not used) + +//////////////////////////////////////////////////////////////////////////////// +// Display + +#define DISP_MISO PIN_SPI1_MISO +#define DISP_MOSI PIN_SPI1_MOSI +#define DISP_SCLK PIN_SPI1_SCK +#define PIN_DISPLAY_CS PIN_SPI1_NSS +#define PIN_DISPLAY_DC (28) +#define PIN_DISPLAY_RST (2) +#define PIN_DISPLAY_BUSY (3) +#define DISP_BACKLIGHT (43) +#define DISP_EN (42) + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define PIN_GPS_RX (36) +#define PIN_GPS_TX (34) +#define PIN_GPS_EN (16) +#define PIN_GPS_RESET (-1) +#define PIN_GPS_PPS (14) +#define PIN_GPS_STANDBY (15) // Low = sleep +#define GPS_BAUD_RATE 115200 diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 09b1391d6..d4983491d 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -137,6 +137,7 @@ extends = ThinkNode_M9 build_flags = ${ThinkNode_M9.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -146,6 +147,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index 694b8bd12..e7c70a5de 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -2,6 +2,7 @@ #include #include +#include // LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF // https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf @@ -32,6 +33,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp new file mode 100644 index 000000000..c6dbdbf58 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -0,0 +1,87 @@ +#include +#include + +#include "WioTrackerL1Board.h" + +void WioTrackerL1Board::begin() { + NRF52BoardDCDC::begin(); + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input + // Set all button pins to INPUT_PULLUP + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { + _is_pa_enabled = enable; + digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA + return true; +} + +void WioTrackerL1Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool WioTrackerL1Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + 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"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h new file mode 100644 index 000000000..798145db0 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +class WioTrackerL1Board : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + bool _is_pa_enabled = false; + KeyValueStore* _prefs = NULL; + + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const { return _is_pa_enabled; } + +public: + WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + void powerOff() override { + setLoRaFemPaGainEnabled(false); // turn Off PA + NRF52Board::powerOff(); + } +}; diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini new file mode 100644 index 000000000..7d3c2dc39 --- /dev/null +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -0,0 +1,123 @@ +[WioTrackerL1-1W] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1-1w + -D WIO_TRACKER_L1 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_OLED_RESET=-1 + -D GPS_BAUD_RATE=9600 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1-1w> + + + + +lib_deps= ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + +[env:WioTrackerL1-1W_repeater] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_room_server] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_companion_radio_usb] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ENABLE_USB_INTERFACE +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + + + +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_companion_radio_ble] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ADVERT_NAME='"@@MAC"' + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_kiss_modem] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/wio-tracker-l1-1w/target.cpp b/variants/wio-tracker-l1-1w/target.cpp new file mode 100644 index 000000000..7a573258e --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.cpp @@ -0,0 +1,40 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/wio-tracker-l1-1w/target.h b/variants/wio-tracker-l1-1w/target.h new file mode 100644 index 000000000..05bc7ff1c --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.h @@ -0,0 +1,35 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #if defined(WIO_TRACKER_L1_EINK) + #include + #else + #include + #endif + #include +#endif +#include + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/wio-tracker-l1-1w/variant.cpp b/variants/wio-tracker-l1-1w/variant.cpp new file mode 100644 index 000000000..18efe7eaf --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.cpp @@ -0,0 +1,78 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 29, // D5 P0.29 (AIN5) LORA_VDET 鈥?Pro 1W: P0.29 (not P1.08) + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.01 Mesh_LED (orange) 鈥?Pro 1W: P1.01 (not P1.15) + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL + + // D31-D33 - Pro 1W only + 13, // D31 P0.13 BOOST_EN (Grove 5V Boost) + 47, // D32 P1.15 nRF_Sig_Charge_State (BQ25616 STAT) + 14, // D33 P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO) +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1-1w/variant.h b/variants/wio-tracker-l1-1w/variant.h new file mode 100644 index 000000000..7520460ab --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.h @@ -0,0 +1,130 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (34) +#define NUM_DIGITAL_PINS (34) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (29) // Joystick Press +#define PIN_BACK_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 +#define PIN_USER_BTN PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// BQ25616 single-wire charge status (Pro 1W) +#define PIN_BOOST_EN (31) // D31 / P0.13 鈥?Grove 5V Boost enable +#define EXT_CHRG_DETECT (32) // D32 / P1.15 鈥?BQ25616 STAT +#define EXT_CHRG_DETECT_VALUE LOW // 0 = charging, 1 = full / charger sleep +#define BOOST_EN_ACTIVE HIGH // HIGH enables Grove 5V Boost + +// Lora Pins +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_DIO_1 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) + +// LORA_PWR_EN (P0.14): external LDO enable for the SX1262 + 1 W PA on the Pro 1W board. +#define LORA_PWR_EN (33) +#define SX126X_POWER_EN LORA_PWR_EN + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (18) +#define PIN_WIRE1_SCL (17) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (PIN_GPS_STANDBY) + +// QSPI Pins +#define PIN_QSPI_SCK (19) +#define PIN_QSPI_CS (20) +#define PIN_QSPI_IO0 (21) +#define PIN_QSPI_IO1 (22) +#define PIN_QSPI_IO2 (23) +#define PIN_QSPI_IO3 (24) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// // EInk on SPI1 +// #define PIN_DISPLAY_CS (36) +// #define PIN_DISPLAY_BUSY (35) +// #define PIN_DISPLAY_DC (34) +// #define PIN_DISPLAY_RST (32) + +#define PIN_SPI1_MISO (37) +#define PIN_SPI1_MOSI (33) +#define PIN_SPI1_SCK (31) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +#endif \ No newline at end of file diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2d..19139c289 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -95,6 +96,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"' diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 587c5c273..62e6fd216 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -73,6 +73,7 @@ lib_deps = [env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + @@ -114,8 +115,10 @@ extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + + + build_flags = ${Xiao_esp32_C3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index d2951c755..cad399dec 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -2,6 +2,7 @@ #include #include +#include /* * This board has no built-in way to read battery voltage. @@ -30,6 +31,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 293a13c15..3bb956070 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -202,11 +202,13 @@ extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> build_flags = ${Xiao_S3_WIO.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256