diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8057bc70a7..5c5735c63b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,6 @@ { + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format "recommendations": [ "pioarduino.pioarduino-ide", "platformio.platformio-ide" diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 7cca7bc9a2..a31bd42d56 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -428,6 +428,29 @@ Byte 0: 0x14 --- +### 9. Set Other Params + +**Purpose**: Update miscellaneous node preferences. All bytes after byte 1 are optional; omit trailing bytes to leave the corresponding preference unchanged. + +**Command Format**: +``` +Byte 0: 0x26 +Byte 1: Manual Add Contacts (bool) +Byte 2: Telemetry Mode (bitfield: bits [5:4] env, bits [3:2] loc, bits [1:0] base) [optional] +Byte 3: Advertisement Location Policy [optional] +Byte 4: Multi ACKs (bool) [optional] +Byte 5: ACK Timeout Multiplier (1–10) [optional] +``` + +**Example** — set ACK timeout multiplier to 3, leaving other fields at current values (hex): +``` +26 00 00 00 00 03 +``` + +**Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) / `ERR_CODE_ILLEGAL_ARG` (0x06) if the multiplier is outside the range 1–10. + +--- + ## Channel Management ### Channel Types @@ -744,6 +767,7 @@ Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0) Byte 56: Radio Spreading Factor Byte 57: Radio Coding Rate Bytes 58+: Device Name (UTF-8, variable length, no null terminator required) +Byte 58+len(name): ACK Timeout Multiplier (1–10; multiplied against the computed DM ACK wait window) ``` **Parsing Pseudocode**: @@ -785,8 +809,13 @@ def parse_self_info(data): offset += 10 if offset < len(data): - name_bytes = data[offset:] - info['name'] = name_bytes.decode('utf-8').rstrip('\x00').strip() + # name is variable-length; ack_timeout_mult is the single byte appended after it + payload = data[offset:] + if len(payload) > 1: + info['name'] = payload[:-1].decode('utf-8').rstrip('\x00').strip() + info['ack_timeout_mult'] = payload[-1] + else: + info['name'] = payload.decode('utf-8').rstrip('\x00').strip() return info ``` diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index bf2f36c3d9..c114fb11d1 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.read((uint8_t *)&_prefs.ack_timeout_mult, sizeof(_prefs.ack_timeout_mult)); // 153 file.close(); } @@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.write((uint8_t *)&_prefs.ack_timeout_mult, sizeof(_prefs.ack_timeout_mult)); // 153 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5fb9bf9d37..2877f52d28 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -844,13 +844,13 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { - return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); + return (SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis)) * _prefs.ack_timeout_mult; } uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { uint8_t path_hash_count = path_len & 63; - return SEND_TIMEOUT_BASE_MILLIS + - ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * - (path_hash_count + 1)); + return (SEND_TIMEOUT_BASE_MILLIS + + ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * + (path_hash_count + 1))) * _prefs.ack_timeout_mult; } void MyMesh::onSendTimeout() {} @@ -881,6 +881,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default + _prefs.ack_timeout_mult = 1; //_prefs.rx_delay_base = 10.0f; enable once new algo fixed #if defined(USE_SX1262) || defined(USE_SX1268) #ifdef SX126X_RX_BOOSTED_GAIN @@ -1070,6 +1071,7 @@ void MyMesh::handleCmdFrame(size_t len) { int tlen = strlen(_prefs.node_name); // revisit: UTF_8 ?? memcpy(&out_frame[i], _prefs.node_name, tlen); i += tlen; + out_frame[i++] = _prefs.ack_timeout_mult; _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { int i = 1; @@ -1440,6 +1442,14 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.advert_loc_policy = cmd_frame[3]; if (len >= 5) { _prefs.multi_acks = cmd_frame[4]; + if (len >= 6) { + uint8_t mult = cmd_frame[5]; + if (mult < 1 || mult > 10) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + return; + } + _prefs.ack_timeout_mult = mult; + } } } } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381ceaf..649a409614 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -34,4 +34,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; + uint8_t ack_timeout_mult; // ACK wait window multiplier (1 = default, max 10) }; \ No newline at end of file diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 10cb00c776..4cff3615e1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,7 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t ack_timeout_mult; // ACK wait window multiplier (1 = default, max 10) }; class CommonCLICallbacks { diff --git a/test/test_ack_timeout_mult/test_ack_timeout_mult.cpp b/test/test_ack_timeout_mult/test_ack_timeout_mult.cpp new file mode 100644 index 0000000000..462291789b --- /dev/null +++ b/test/test_ack_timeout_mult/test_ack_timeout_mult.cpp @@ -0,0 +1,53 @@ +#include +#include + +// Mirror constants from examples/companion_radio/MyMesh.cpp to guard against +// accidental changes that break the backwards-compatibility guarantee. +#define SEND_TIMEOUT_BASE_MILLIS 500 +#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f +#define DIRECT_SEND_PERHOP_FACTOR 6.0f +#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 + +static uint32_t calcFloodTimeout(uint32_t airtime_ms, uint8_t mult) { + return (uint32_t)((SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * airtime_ms)) * mult); +} + +static uint32_t calcDirectTimeout(uint32_t airtime_ms, uint8_t path_len, uint8_t mult) { + uint8_t hops = path_len & 63; + return (uint32_t)((SEND_TIMEOUT_BASE_MILLIS + + ((airtime_ms * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * + (hops + 1))) * mult); +} + +// At mult=1, results must be identical to the pre-feature formula. +TEST(AckTimeoutMult, FloodDefaultMultiplierIsUnchanged) { + EXPECT_EQ(calcFloodTimeout(500, 1), 500 + (uint32_t)(16.0f * 500)); + EXPECT_EQ(calcFloodTimeout(1000, 1), 500 + (uint32_t)(16.0f * 1000)); + EXPECT_EQ(calcFloodTimeout(2600, 1), 500 + (uint32_t)(16.0f * 2600)); +} + +TEST(AckTimeoutMult, DirectDefaultMultiplierIsUnchanged) { + // 1-hop path (path_len=1, hops=1, so (hops+1)=2) + uint32_t expected = 500 + (uint32_t)((1000 * 6.0f + 250) * 2); + EXPECT_EQ(calcDirectTimeout(1000, 1, 1), expected); +} + +// Scaling: mult=N must produce exactly N times the default result. +TEST(AckTimeoutMult, FloodScalesLinearly) { + for (uint8_t mult = 1; mult <= 10; mult++) { + uint32_t base = calcFloodTimeout(1000, 1); + EXPECT_EQ(calcFloodTimeout(1000, mult), base * mult); + } +} + +TEST(AckTimeoutMult, DirectScalesLinearly) { + for (uint8_t mult = 1; mult <= 10; mult++) { + uint32_t base = calcDirectTimeout(1000, 2, 1); + EXPECT_EQ(calcDirectTimeout(1000, 2, mult), base * mult); + } +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index cb25903ce9..370492b21b 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -38,6 +38,26 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 boschsensortec/BSEC Software Library @ ^1.8.1492 +[env:Tbeam_SX1276_companion_radio_usb] +extends = LilyGo_TBeam_SX1276 +board_build.upload.maximum_ram_size=2000000 +build_flags = + ${LilyGo_TBeam_SX1276.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=160 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=128 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1276.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_SX1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Tbeam_SX1276_companion_radio_ble] extends = LilyGo_TBeam_SX1276 board_build.upload.maximum_ram_size=2000000