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/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..f4a43fad3 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. @@ -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..fb9cbaf99 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -171,8 +171,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/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a6739ac09..01a7473a0 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) @@ -134,6 +139,10 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 +// Copied from simple_repeater (could probably be shared) +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -259,10 +268,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 +280,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); } @@ -403,6 +412,53 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { + if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; + if (max_num > disc_nodes_count) max_num = disc_nodes_count; + + for (int i = 0; i < max_num; i++) { + nodes[i] = discovered_nodes[i]; + } + return max_num; +} + +bool MyMesh::requestRepeatersDiscovery() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + getRNG()->random(&cmd_bytes[2], 4); // tag + disc_nodes_count = 0; + disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); + mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + sendZeroHop(req); + return true; + } + return false; +} + +void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { + if ((p_len < 12) + || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) + || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) + || (memcmp(&payload[2], &disc_node_req_tag, 4))) { + return; + } + memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); + discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; + discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; + discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); + ContactInfo* c = lookupContactByPubKey(&payload[6], 8); + if (c != NULL) { + strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); + } else { + discovered_nodes[disc_nodes_count].name[0] = 0; + } + disc_nodes_count ++; +} +#endif + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -528,12 +584,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); @@ -783,6 +850,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); +#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); @@ -865,6 +935,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { _iter_started = false; _cli_rescue = false; + cli_command[0] = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -877,6 +948,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; @@ -943,6 +1016,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); @@ -980,8 +1055,9 @@ void MyMesh::begin(bool has_display) { 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"); } @@ -1086,6 +1162,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++]; @@ -1096,16 +1186,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); @@ -1184,8 +1274,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(); @@ -1515,7 +1605,7 @@ 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++]; + 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); @@ -2035,8 +2125,135 @@ 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 (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, "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) { // no 'get wifi.pwd', by design + 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') { @@ -2045,22 +2262,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) { @@ -2241,6 +2454,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? diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada9..3b98a4f67 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 13 +#define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "14 Aug 2026" @@ -84,6 +84,16 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +struct DiscoveredNode { + uint8_t pubkey_prefix[9]; + float snr_in; + float snr_out; + char name[32]; + uint8_t type; +}; +#endif + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); @@ -102,10 +112,18 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + bool requestRepeatersDiscovery(); + int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); +#endif + 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 +153,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 +189,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 +222,7 @@ private: } void checkCLIRescueCmd(); + bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; @@ -225,6 +247,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; @@ -256,6 +279,19 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif + DiscoveredNode discovered_nodes[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; + + void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); +#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 21766de82..4b169d73f 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,26 +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); - #if 0 - // NOTE: these cannot be set (yet) so don't load/save until we can. - // also, fem_rxgain WAS mapped to wrong JSON property previously - 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); - #endif 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(); } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_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; @@ -115,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)); @@ -132,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)); @@ -142,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..6dc9acdc3 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" @@ -108,9 +127,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() { @@ -191,23 +214,46 @@ 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. 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; + } + }); +#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 @@ -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 59b58461e..53ae480cf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -3,14 +3,10 @@ #include "../MyMesh.h" #include "target.h" #include -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif -#ifndef UI_TZ_OFFSET - #define UI_TZ_OFFSET 0 -#endif - #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -24,6 +20,7 @@ #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 @@ -102,7 +99,12 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + DISCOVERY, +#endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -113,7 +115,11 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; + 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 @@ -146,11 +152,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 } @@ -233,8 +252,7 @@ public: #ifdef UI_SHOW_CLOCK display.setTextSize(3); uint32_t now = _rtc->getCurrentTime(); - int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... - now += (int32_t)tz * 3600; + 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); @@ -242,7 +260,7 @@ public: sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); display.drawTextCentered(display.width() / 2, 80, tmp); #endif - #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); @@ -446,6 +464,41 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + } else if (_page == HomePage::DISCOVERY) { + int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + 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 < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * UI_RECENT_LIST_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); @@ -457,6 +510,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 } @@ -471,6 +525,11 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } +#endif return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -503,10 +562,25 @@ public: return true; } #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { + if (millis() > discovery_req_time + 5000) { // rate limiter + the_mesh.requestRepeatersDiscovery(); + discovery_req_time = millis(); + } + 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; } }; 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-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index b6bdbcf4b..9ecd66f12 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); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a5..ca6a3e607 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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); @@ -982,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(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb92..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; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68e..1f71da74d 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 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/main.cpp b/examples/simple_room_server/main.cpp index d833fff39..227ee2cbd 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 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..23d0cdc35 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -576,7 +576,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i 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(); @@ -771,8 +771,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(); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a7..749ff6ef1 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,8 +137,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 diff --git a/platformio.ini b/platformio.ini index ee502473c..de4d6c29c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -85,7 +85,7 @@ 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} @@ -171,6 +171,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..4bc9d3dbb 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,21 +64,18 @@ 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; } }; /** 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/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..e06544647 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #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..4930e81e9 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) { @@ -447,39 +458,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 +512,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 +520,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 +547,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 +557,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 +577,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 +673,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 +694,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 +710,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 +720,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..8591cdc14 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(); } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_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..9d92cad9a --- /dev/null +++ b/src/helpers/CommonRadioPrefs.cpp @@ -0,0 +1,230 @@ +#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) { + setAgcResetInt(atoi(&command[23])); + 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..96895bb2e --- /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 uint8_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint8_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..36aff5ccf 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); @@ -62,6 +63,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/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/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/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/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/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_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_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/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/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_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..5606d4822 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -135,6 +135,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 +146,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index b8c709739..db19e4466 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -22,7 +22,6 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_ROTARY_INPUT=1 -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 - -D UI_TZ_OFFSET=-4 # GMT-4 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 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/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