Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
33 changes: 31 additions & 2 deletions docs/companion_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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**:
Expand Down Expand Up @@ -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
```
Expand Down
2 changes: 2 additions & 0 deletions examples/companion_radio/DataStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
18 changes: 14 additions & 4 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
1 change: 1 addition & 0 deletions src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions test/test_ack_timeout_mult/test_ack_timeout_mult.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include <gtest/gtest.h>
#include <cstdint>

// 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();
}
20 changes: 20 additions & 0 deletions variants/lilygo_tbeam_SX1276/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../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
Expand Down