diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8c9115954..ef454087c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1491,6 +1491,7 @@ Station G2/G3 targets default to `off`. **Notes:** - This controls a software-selectable external LoRa FEM transmit gain where the board supports it. +- Heltec T096 routes TX through its KCT8103L PA with no populated TX bypass. `get radio.fem.txgain` reports `on (fixed TX PA)`; `set radio.fem.txgain` cannot toggle it. Use `set tx ` to adjust SX1262 drive. The PA's `CSD` control shuts down the FEM and is not a lower-gain TX mode. See the [T096 schematic](https://resource.heltec.cn/download/Mesh_Node_T096/Schematic/Mesh_Node_T096_V0.2.pdf) and [KCT8103L product description](https://kxcomtech.com/en/product/info/1774). - On Station G3, remove the PA PL1 jumper to allow software control. `on` selects PA PL1 high/short and `off` selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. - Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. - The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. `get` reports the configured state, which may lead the hardware until the node next transmits. @@ -3809,9 +3810,10 @@ set retry.preset mobile Flood retry resends flood-routed packets when the same packet is not heard from another qualifying repeater. -The count, path, group-data path, and advert controls work on repeater, room-server, and sensor -firmware. Flood forwarding must also be enabled for retries to run. Prefix, -ignore, bridge, and bucket controls are repeater-only. +The count, path, group-data path, and advert controls work on repeater, +room-server, sensor, and Companion firmware. Infrastructure forwarding must +also be enabled for retries to run. Prefix, ignore, bridge, and bucket controls +are repeater-only. #### Hop-zero flood retry coding rate @@ -3858,6 +3860,20 @@ setting or physical preamble. #### View or change flood retry count +Companion firmware supports `flood.retry.count`, `flood.retry.path`, +`flood.retry.group.path`, and `flood.retry.advert` through its text terminal and +framed CLI. These settings persist across reboot. Companion retains its +pre-existing defaults when upgrading an older preferences file: count `15`, +general path gate `1`, group path gate `off`, and self-origin advert retries +`on`. Companion uses the configured count directly, without the infrastructure +role's hop-zero and one-hop multipliers. The shared payload caps below still +apply. Companion only retries its **own** adverts; neighbor adverts remain +ineligible even with `flood.retry.advert on`. + +The `infra`, `rooftop`, and `mobile` defaults and `retry.preset` command in this +section apply to infrastructure roles. Companion does not expose repeater +bridge or prefix retry filters. + **Search terms:** flood tx retries, flood retry attempts, flood retransmissions, broadcast retries. **Usage:** @@ -3867,7 +3883,7 @@ setting or physical preamble. **Parameters:** - `count`: Base retry attempts after the original send, from `0` to `15`. `0` disables flood retry. -**Note:** The role first calculates its retry count: path count 0 uses `count * 2`, path count 1 uses `count * 1.5` rounded up, and path count 2 and higher uses the configured base count, with a hard cap of `15`. A shared payload policy then applies to every build: `REQ` never retries; `GRP_TXT` keeps the role-calculated count; remote-login-critical `RESPONSE`, `TXT_MSG`, `ANON_REQ`, and `PATH` packets keep up to `15` at the originating node (path count 0) and cap at `2` after entering the path; all other flood payload types cap at `1`. These caps never raise a lower role-calculated count. Setting `count` to `0` immediately removes queued and future flood retries; a packet already transmitting is allowed to finish. +**Note:** Infrastructure roles first calculate their retry count: path count 0 uses `count * 2`, path count 1 uses `count * 1.5` rounded up, and path count 2 and higher uses the configured base count, with a hard cap of `15`. Companion uses `count` directly. A shared payload policy then applies to every build: `REQ` never retries; `GRP_TXT` keeps the role-calculated count; remote-login-critical `RESPONSE`, `TXT_MSG`, `ANON_REQ`, and `PATH` packets keep up to `15` at the originating node (path count 0) and cap at `2` after entering the path; all other flood payload types cap at `1`. These caps never raise a lower role-calculated count. Setting `count` to `0` immediately removes queued and future flood retries; a packet already transmitting is allowed to finish. Forwarded neighbor adverts have an additional loop guard independent of the advert retry setting. After this node completes an advert transmission and hears a downstream copy with a longer path, it does not forward that same advert again while the advert's signed timestamp is less than six hours old. Self-originated adverts, adverts without a heard echo, and adverts six hours old or older are unaffected. @@ -3923,7 +3939,7 @@ set flood.retry.path off **Note:** The stricter of `flood.retry.path` and `flood.retry.group.path` is used. A value of `1` allows retry sequences at path counts `0` and `1`; group data at path count `2` or higher is still forwarded normally but does not start a flood retry sequence. A value of `0` allows retries only at the originating sender. -Setting `flood.retry.path` to `0` also sets `flood.retry.group.path` to `off` because the general zero-hop gate is already stricter. While the general gate remains `0`, attempts to set the group-data gate keep it `off`. Applying a named retry preset restores the group-data default of `1`. +On infrastructure roles, setting `flood.retry.path` to `0` also sets `flood.retry.group.path` to `off` because the general zero-hop gate is already stricter. While the general gate remains `0`, attempts to set the group-data gate keep it `off`. Applying a named retry preset restores the group-data default of `1`. Companion retains the saved group gate, but the stricter general gate still wins. **Examples:** ``` diff --git a/examples/companion_radio/CompanionRetry.h b/examples/companion_radio/CompanionRetry.h new file mode 100644 index 000000000..168fe3d4c --- /dev/null +++ b/examples/companion_radio/CompanionRetry.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +#include +#include "NodePrefs.h" + +namespace mesh { namespace companion { + +// Companion keeps the historical core retry budget by default. These are +// local Companion controls; repeater presets and bridge prefix filters have +// different forwarding semantics and are intentionally not exposed here. +template +bool handleRetryCommand(CompanionNodePrefs& prefs, const char* command, + char* reply, size_t reply_size, + SavePrefs save_prefs, + CancelFloodRetries cancel_flood_retries) { + if (command == nullptr || reply == nullptr || reply_size == 0) return false; + + const bool get = strncmp(command, "get ", 4) == 0; + const bool set = strncmp(command, "set ", 4) == 0; + if (!get && !set) return false; + const char* key = command + 4; + const char* value = set ? strchr(key, ' ') : nullptr; + const size_t key_len = value == nullptr ? strlen(key) : static_cast(value - key); + const auto matches = [key, key_len](const char* name) { + return strlen(name) == key_len && strncmp(key, name, key_len) == 0; + }; + + enum Field { Count, Path, GroupPath, Advert }; + Field field; + if (matches("flood.retry.count")) field = Count; + else if (matches("flood.retry.path")) field = Path; + else if (matches("flood.retry.group.path")) field = GroupPath; + else if (matches("flood.retry.advert")) field = Advert; + else return false; + + if (get) { + if (field == Count) { + snprintf(reply, reply_size, "> %u", (unsigned)prefs.flood_retry_attempts); + } else if (field == Advert) { + snprintf(reply, reply_size, "> %s", prefs.flood_retry_advert_enabled ? "on" : "off"); + } else { + const uint8_t gate = field == Path + ? prefs.flood_retry_max_path : prefs.flood_retry_group_max_path; + if (gate == 0xFF) snprintf(reply, reply_size, "> off"); + else snprintf(reply, reply_size, "> %u", (unsigned)gate); + } + return true; + } + + const char* input = value == nullptr ? "" : value + 1; + const uint8_t old_count = prefs.flood_retry_attempts; + const uint8_t old_path = prefs.flood_retry_max_path; + const uint8_t old_group_path = prefs.flood_retry_group_max_path; + const uint8_t old_advert = prefs.flood_retry_advert_enabled; + uint32_t parsed = 0; + bool valid = true; + + if (field == Advert) { + if (strcmp(input, "on") == 0) prefs.flood_retry_advert_enabled = 1; + else if (strcmp(input, "off") == 0) prefs.flood_retry_advert_enabled = 0; + else valid = false; + } else if (field == Count) { + valid = cli::parseUnsignedIntegerStrict(input, parsed) && parsed <= 15; + if (valid) prefs.flood_retry_attempts = static_cast(parsed); + } else { + if (strcmp(input, "off") == 0) parsed = 0xFF; + else valid = cli::parseUnsignedIntegerStrict(input, parsed) && parsed <= 63; + if (valid) { + if (field == Path) prefs.flood_retry_max_path = static_cast(parsed); + else prefs.flood_retry_group_max_path = static_cast(parsed); + } + } + + if (!valid) { + const char* range = field == Count ? "0-15" + : field == Advert ? "on|off" : "0-63|off"; + snprintf(reply, reply_size, "Error: use set %.*s <%s>", + (int)key_len, key, range); + return true; + } + if (!save_prefs()) { + prefs.flood_retry_attempts = old_count; + prefs.flood_retry_max_path = old_path; + prefs.flood_retry_group_max_path = old_group_path; + prefs.flood_retry_advert_enabled = old_advert; + snprintf(reply, reply_size, "Error: retry setting could not be saved"); + return true; + } + if (old_count != 0 && prefs.flood_retry_attempts == 0) cancel_flood_retries(); + snprintf(reply, reply_size, "OK"); + return true; +} + +} } // namespace mesh::companion diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 48a3611de..c999da944 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1032,6 +1032,13 @@ bool DataStore::loadPrefsInt(const char *filename, #endif #if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) 323, // Pico W additionally persists its SSID/password (ESP32 uses NVS) +#endif + 230, // Companion flood retry controls, appended after prior fields +#ifdef TBEAM_1W + 237, +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 327, #endif }; const uint32_t prefs_size = file.size(); @@ -1164,6 +1171,14 @@ bool DataStore::loadPrefsInt(const char *filename, loaded_prefs.wifi_ssid[sizeof(loaded_prefs.wifi_ssid) - 1] = 0; loaded_prefs.wifi_pwd[sizeof(loaded_prefs.wifi_pwd) - 1] = 0; #endif + readOptionalField(&loaded_prefs.flood_retry_attempts, + sizeof(loaded_prefs.flood_retry_attempts)); + readOptionalField(&loaded_prefs.flood_retry_max_path, + sizeof(loaded_prefs.flood_retry_max_path)); + readOptionalField(&loaded_prefs.flood_retry_group_max_path, + sizeof(loaded_prefs.flood_retry_group_max_path)); + readOptionalField(&loaded_prefs.flood_retry_advert_enabled, + sizeof(loaded_prefs.flood_retry_advert_enabled)); // Any bytes left over form only part of a historically appended field. // Preserve the file and defaults rather than treating that tail as EOF. @@ -1314,6 +1329,14 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou success = success && file.write((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd)) == sizeof(_prefs.wifi_pwd); #endif + success = success && file.write((uint8_t *)&_prefs.flood_retry_attempts, + sizeof(_prefs.flood_retry_attempts)) == sizeof(_prefs.flood_retry_attempts); + success = success && file.write((uint8_t *)&_prefs.flood_retry_max_path, + sizeof(_prefs.flood_retry_max_path)) == sizeof(_prefs.flood_retry_max_path); + success = success && file.write((uint8_t *)&_prefs.flood_retry_group_max_path, + sizeof(_prefs.flood_retry_group_max_path)) == sizeof(_prefs.flood_retry_group_max_path); + success = success && file.write((uint8_t *)&_prefs.flood_retry_advert_enabled, + sizeof(_prefs.flood_retry_advert_enabled)) == sizeof(_prefs.flood_retry_advert_enabled); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) success = file.commit(success); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5f518459e..dcc68fc4b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -3,6 +3,7 @@ #include #include "CompanionBluetooth.h" #include "CompanionWireless.h" +#include "CompanionRetry.h" #if defined(MESH_SOAK_DIAGNOSTICS) #include "../../tools/hil/S3SoakDiagnostics.h" #endif @@ -1110,12 +1111,23 @@ void MyMesh::onGroupPacketRecv(mesh::Packet* packet) { #endif bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const { - if (packet == NULL) return false; + if (packet == NULL || _prefs.flood_retry_attempts == 0) return false; // A companion may retry its own advert once, using the core's deliberately // slow origin-advert delay. Do not add retries while relaying a neighbour's // advert; the ordinary forwarding and recent-echo guard still apply. return packet->getPayloadType() != PAYLOAD_TYPE_ADVERT - || isSelfOriginAdvert(packet); + || (_prefs.flood_retry_advert_enabled && isSelfOriginAdvert(packet)); +} + +uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const { + return applyGroupDataFloodRetryPathGate( + packet, _prefs.flood_retry_max_path, + _prefs.flood_retry_group_max_path); +} + +uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return _prefs.flood_retry_attempts; } bool MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { @@ -1977,6 +1989,16 @@ void MyMesh::begin(bool has_display, bool radio_available) { _prefs.sf = constrain(_prefs.sf, 5, 12); _prefs.cr = constrain(_prefs.cr, 5, 8); _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); + _prefs.flood_retry_attempts = constrain(_prefs.flood_retry_attempts, 0, 15); + if (_prefs.flood_retry_max_path > 63 + && _prefs.flood_retry_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) { + _prefs.flood_retry_max_path = 1; + } + if (_prefs.flood_retry_group_max_path > 63 + && _prefs.flood_retry_group_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) { + _prefs.flood_retry_group_max_path = FLOOD_RETRY_PATH_GATE_DISABLED; + } + _prefs.flood_retry_advert_enabled = _prefs.flood_retry_advert_enabled ? 1 : 0; _prefs.multi_acks = constrain(_prefs.multi_acks, 0, 1); _prefs.manual_add_contacts = constrain(_prefs.manual_add_contacts, 0, 1); _prefs.vibe_quiet = constrain(_prefs.vibe_quiet, 0, 1); @@ -2834,7 +2856,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply, if (strcmp(command, "get radio.fem.txgain") == 0) { if (!board.canControlLoRaFemPaGain()) { - snprintf(reply, reply_size, "Error: unsupported"); + if (board.isLoRaFemPaGainEnabled()) { + snprintf(reply, reply_size, "> on (fixed TX PA)"); + } else { + snprintf(reply, reply_size, "Error: unsupported"); + } } else { snprintf(reply, reply_size, "> %s", board.isLoRaFemPaGainEnabled() ? "on" : "off"); @@ -2848,7 +2874,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply, snprintf(reply, reply_size, "Error: use set radio.fem.txgain on|off"); } else if (!board.canControlLoRaFemPaGain()) { - snprintf(reply, reply_size, "Error: unsupported"); + snprintf(reply, reply_size, "%s", board.isLoRaFemPaGainEnabled() + ? "Error: TX PA is fixed on; use set tx to adjust power" + : "Error: unsupported"); } else if (!applyAndSaveFemTxGain(strcmp(value, "on") == 0)) { snprintf(reply, reply_size, "Error: failed to apply or save FEM TX gain"); } else { @@ -8320,7 +8348,11 @@ void MyMesh::handleTerminalCommand(char* command) { } } else if (strcmp(command, "get radio.fem.txgain") == 0) { if (!board.canControlLoRaFemPaGain()) { - terminalOutput().print(" ERROR: FEM TX gain control is unsupported on this board\r\n"); + if (board.isLoRaFemPaGainEnabled()) { + terminalOutput().print(" FEM TX gain: on (fixed TX PA)\r\n"); + } else { + terminalOutput().print(" ERROR: FEM TX gain control is unsupported on this board\r\n"); + } } else { terminalOutput().printf(" FEM TX gain: %s\r\n", board.isLoRaFemPaGainEnabled() ? "on" : "off"); @@ -8417,7 +8449,9 @@ void MyMesh::handleTerminalCommand(char* command) { if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) { terminalOutput().print(" ERROR: use set radio.fem.txgain \r\n"); } else if (!board.canControlLoRaFemPaGain()) { - terminalOutput().print(" ERROR: FEM TX gain control is unsupported on this board\r\n"); + terminalOutput().print(board.isLoRaFemPaGainEnabled() + ? " ERROR: TX PA is fixed on; use set tx to adjust power\r\n" + : " ERROR: FEM TX gain control is unsupported on this board\r\n"); } else if (!applyAndSaveFemTxGain(strcmp(value, "on") == 0)) { terminalOutput().print(" ERROR: failed to apply FEM TX gain\r\n"); } else { @@ -8531,6 +8565,10 @@ void MyMesh::handleTerminalCommand(char* command) { terminalOutput().print(" set radio.fem.rxgain \r\n"); terminalOutput().print(" get radio.fem.txgain\r\n"); terminalOutput().print(" set radio.fem.txgain \r\n"); + terminalOutput().print(" get/set flood.retry.count <0-15>\r\n"); + terminalOutput().print(" get/set flood.retry.path <0-63|off>\r\n"); + terminalOutput().print(" get/set flood.retry.group.path <0-63|off>\r\n"); + terminalOutput().print(" get/set flood.retry.advert \r\n"); terminalOutput().print(" card\r\n"); terminalOutput().print(" import \r\n"); terminalOutput().print(" clock\r\n"); @@ -8739,6 +8777,11 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, } if (sender_timestamp == 0 && handleDirectCommand(command, reply, reply_capacity)) return true; + if (mesh::companion::handleRetryCommand( + _prefs, command, reply, reply_capacity, + [this]() { return savePrefs(); }, + [this]() { cancelAllFloodRetries(); })) return true; + #if COMPANION_FEATURE_USB_MOTA_SOURCE if (mesh::isUsbMotaOwnerTransitionCommand(command)) { snprintf(reply, reply_capacity, diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 8649c1697..906e032c6 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -285,6 +285,8 @@ protected: bool filterRecvFloodPacket(mesh::Packet* packet) override; bool allowPacketForward(const mesh::Packet* packet) override; bool allowFloodRetry(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override; #ifdef COMPANION_MESH_CLOCK_SYNC void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 49cd1e896..911329ada 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -99,6 +99,13 @@ public: char wifi_ssid[33] = {}; char wifi_pwd[64] = {}; #endif + // Keep these after every board-specific field: all existing prefs images + // must retain their exact offsets. Defaults reproduce the core Companion + // retry policy for older files that have no retry tail. + uint8_t flood_retry_attempts = 15; + uint8_t flood_retry_max_path = 1; + uint8_t flood_retry_group_max_path = 0xFF; + uint8_t flood_retry_advert_enabled = 1; private: class RadioPrefs : public CommonRadioPrefs { diff --git a/test/test_companion_preferences_transaction.py b/test/test_companion_preferences_transaction.py index 245966da4..374965349 100644 --- a/test/test_companion_preferences_transaction.py +++ b/test/test_companion_preferences_transaction.py @@ -51,6 +51,8 @@ int main(int argc,char** argv){ original.gps_interval=123;original.sf=7;original.cr=7;original.bw=62.5f; original.tx_delay_factor=1.25f;original.direct_tx_delay_factor=0.75f; original.interference_threshold=8;original.agc_reset_interval=15;original.tz_offset=-7; + original.flood_retry_attempts=4;original.flood_retry_max_path=3; + original.flood_retry_group_max_path=2;original.flood_retry_advert_enabled=0; #ifdef TBEAM_1W strcpy(original.fan_mode,"on");original.fan_lo=33;original.fan_hi=42; #endif @@ -84,6 +86,8 @@ int main(int argc,char** argv){ assert(loaded.freq==changed.freq&&loaded.ble_pin==original.ble_pin); assert(loaded.tx_delay_factor==1.25f&&loaded.direct_tx_delay_factor==0.75f); assert(loaded.interference_threshold==8&&loaded.agc_reset_interval==15&&loaded.tz_offset==-7); + assert(loaded.flood_retry_attempts==4&&loaded.flood_retry_max_path==3); + assert(loaded.flood_retry_group_max_path==2&&loaded.flood_retry_advert_enabled==0); #ifdef TBEAM_1W assert(!strcmp(loaded.fan_mode,"on")&&loaded.fan_lo==33&&loaded.fan_hi==42); #endif @@ -166,7 +170,22 @@ int main(int argc,char** argv){ assert(legacy.loadPrefs(loaded,lat,lon)); assert(loaded.tx_delay_factor==0.5f&&loaded.direct_tx_delay_factor==0.2f); assert(loaded.interference_threshold==0&&loaded.agc_reset_interval==0&&loaded.tz_offset==0); + assert(loaded.flood_retry_attempts==15&&loaded.flood_retry_max_path==1); + assert(loaded.flood_retry_group_max_path==0xff&&loaded.flood_retry_advert_enabled==1); assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin); + } else if(scenario==6){ + DataStore legacy;legacy.fs.files["/new_prefs"]=disk; +#if defined(TBEAM_1W) + legacy.fs.files["/new_prefs"].resize(233); +#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + legacy.fs.files["/new_prefs"].resize(323); +#else + legacy.fs.files["/new_prefs"].resize(226); +#endif + CompanionNodePrefs loaded;double lat=0,lon=0; + assert(legacy.loadPrefs(loaded,lat,lon)); + assert(loaded.flood_retry_attempts==15&&loaded.flood_retry_max_path==1); + assert(loaded.flood_retry_group_max_path==0xff&&loaded.flood_retry_advert_enabled==1); } else if(scenario==4){ DataStore legacy;legacy.fs.files["/node_prefs"]=disk; legacy.fs.fail_write_after=17; @@ -254,7 +273,7 @@ inline char* utoa(unsigned int value,char* output,int base){ str(ROOT / 'src/helpers/TxtDataHelpers.cpp'), '-o', str(binary)], capture_output=True, text=True) self.assertEqual(build.returncode, 0, build.stdout + build.stderr) - for scenario in range(6): + for scenario in range(7): with self.subTest(scenario=scenario): run = subprocess.run([str(binary), str(scenario)], capture_output=True, text=True) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) diff --git a/test/test_companion_retry_settings.py b/test/test_companion_retry_settings.py new file mode 100644 index 000000000..11a0116aa --- /dev/null +++ b/test/test_companion_retry_settings.py @@ -0,0 +1,113 @@ +"""Exercise Companion's production retry command parser and save semantics.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +#include "examples/companion_radio/CompanionRetry.h" + +int main() { + CompanionNodePrefs prefs; + char reply[160] = {}; + int saves = 0, cancels = 0; + bool save_ok = true; + auto save = [&]() { ++saves; return save_ok; }; + auto cancel = [&]() { ++cancels; }; + auto run = [&](const char* command) { + memset(reply, 0, sizeof(reply)); + return mesh::companion::handleRetryCommand( + prefs, command, reply, sizeof(reply), save, cancel); + }; + + assert(run("get flood.retry.count") && !strcmp(reply, "> 15")); + assert(run("get flood.retry.path") && !strcmp(reply, "> 1")); + assert(run("get flood.retry.group.path") && !strcmp(reply, "> off")); + assert(run("get flood.retry.advert") && !strcmp(reply, "> on")); + assert(saves == 0); + + assert(run("set flood.retry.count 8") && !strcmp(reply, "OK")); + assert(run("get flood.retry.count") && !strcmp(reply, "> 8")); + assert(run("set flood.retry.count 2") && !strcmp(reply, "OK")); + assert(run("get flood.retry.count") && !strcmp(reply, "> 2")); + assert(run("set flood.retry.path off") && !strcmp(reply, "OK")); + assert(run("get flood.retry.path") && !strcmp(reply, "> off")); + assert(run("set flood.retry.group.path 3") && !strcmp(reply, "OK")); + assert(run("get flood.retry.group.path") && !strcmp(reply, "> 3")); + assert(run("set flood.retry.advert off") && !strcmp(reply, "OK")); + assert(run("get flood.retry.advert") && !strcmp(reply, "> off")); + + const int saved_before_invalid = saves; + for (const char* command : {"set flood.retry.count 16", + "set flood.retry.count -1", + "set flood.retry.count 3junk", + "set flood.retry.path 64", + "set flood.retry.group.path", + "set flood.retry.advert maybe"}) { + assert(run(command) && !strncmp(reply, "Error:", 6)); + } + assert(saves == saved_before_invalid); + assert(prefs.flood_retry_attempts == 2); + assert(prefs.flood_retry_max_path == 0xff); + assert(prefs.flood_retry_group_max_path == 3); + assert(prefs.flood_retry_advert_enabled == 0); + + save_ok = false; + assert(run("set flood.retry.count 0")); + assert(!strcmp(reply, "Error: retry setting could not be saved")); + assert(prefs.flood_retry_attempts == 2 && cancels == 0); + assert(run("set flood.retry.advert on")); + assert(prefs.flood_retry_advert_enabled == 0); + save_ok = true; + assert(run("set flood.retry.count 0") && !strcmp(reply, "OK")); + assert(prefs.flood_retry_attempts == 0 && cancels == 1); + assert(run("set flood.retry.count 0") && cancels == 1); + assert(!run("set flood.retry.bridge on")); +} +''' + + +class CompanionRetrySettingsTests(unittest.TestCase): + def test_retry_commands(self): + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + source = work / "retry.cpp" + binary = work / "retry" + source.write_text(HARNESS) + (work / "Utils.h").write_text('''#pragma once +#include +#include +namespace mesh { struct Utils { + static void printHex(Stream&,const uint8_t*,size_t){assert(false);} + static void fromHex(uint8_t*,size_t,const char*){assert(false);} +}; } +''') + build = subprocess.run([ + "g++", "-std=c++17", "-DNRF52_PLATFORM=1", + "-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie", + "-I", str(work), + "-I", str(ROOT / "test/fixtures/radio_profiles/mocks"), + "-I", str(ROOT / "test/mocks"), + "-I", str(ROOT / "src"), + "-I", str(ROOT), + str(source), + str(ROOT / "src/helpers/ConfigSerializer.cpp"), + str(ROOT / "src/helpers/DynamicConfigSerializer.cpp"), + str(ROOT / "src/helpers/CommonRadioPrefs.cpp"), + str(ROOT / "src/helpers/TxtDataHelpers.cpp"), + "-o", str(binary), + ], capture_output=True, text=True) + self.assertEqual(build.returncode, 0, build.stdout + build.stderr) + run = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(run.returncode, 0, run.stdout + run.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 4a1ea1c2a..6980e527c 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -33,4 +33,7 @@ public: bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; + // KCT8103L TX has no populated bypass path on T096. CSD shuts down the + // whole FEM, so the PA is always used when this board transmits. + bool isLoRaFemPaGainEnabled() const override { return true; } };