Add direct retry controls

This commit is contained in:
mikecarper
2026-06-15 17:24:22 -07:00
parent 7d8bb1d319
commit 765ec1447b
12 changed files with 649 additions and 416 deletions
-68
View File
@@ -1128,71 +1128,3 @@ region save
**Note:** Returns an error on boards without power management support.
---
---
## Halo Direct Retry Commands
These commands are available on the Halo direct-message retry branch. See `docs/halo_settings.md` for operating guidance and examples.
### View or change the retry preset
**Usage:**
- `get retry.preset`
- `set retry.preset <value>`
**Parameters:**
- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2`
---
### View or change whether direct retries use the recent repeater SNR gate
**Usage:**
- `get direct.retry.heard`
- `set direct.retry.heard <state>`
**Parameters:**
- `state`: `on`|`off`
---
### View or change direct retry timing and count
**Usage:**
- `get direct.retry.margin`
- `set direct.retry.margin <value>`
- `get direct.retry.count`
- `set direct.retry.count <1-15>`
- `get direct.retry.base`
- `set direct.retry.base <10-5000>`
- `get direct.retry.step`
- `set direct.retry.step <0-5000>`
---
### View or change adaptive coding rate for direct retry packets
**Usage:**
- `get direct.retry.cr`
- `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`
- `set direct.retry.cr off`
---
### Get or set recent repeater prefix/SNR
**Usage:**
- `get recent.repeater`
- `get recent.repeater <page>`
- `get recent.repeater page <page>`
- `set recent.repeater <prefix_hex_6> <snr_db>`
- `clear recent.repeater`
---
### View or change direct reply path overrides
**Usage:**
- `get outpath`
- `set outpath <hops>`
- `set outpath direct`
- `set outpath clear`
- `set outpath flood`
- `get altpath`
- `set altpath <hops>`
- `set altpath clear`
-136
View File
@@ -1,136 +0,0 @@
# Halo Direct Message Retry Settings
This file covers only CLI settings and helper commands added for Halo direct-message retry behavior. Use `docs/cli_commands.md` for the general MeshCore CLI.
Halo retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard.
## Quick Start
```text
set retry.preset rooftop
set direct.retry.heard on
get retry.preset
get direct.retry.heard
get direct.retry.count
get direct.retry.base
get direct.retry.step
```
Use prefixes from the analyzer, neighbors list, or `get recent.repeater` after the repeater has been online for a few hours.
## Added Halo Settings
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry. | `get recent.repeater`, `get recent.repeater <page>`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` |
| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath <hops>`, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` |
| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath <hops>`, `set altpath clear` | `set altpath A1B2C3,D4E5F6` |
| `retry.preset` | Applies direct retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset <value>` | `set retry.preset rooftop` |
| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` |
| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` |
| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` |
| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` |
| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` |
| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` |
## Recent Repeater Table
Direct retry uses the recent repeater table when `direct.retry.heard` is `on`.
Show learned rows:
```text
get recent.repeater
get recent.repeater 2
get recent.repeater page 3
```
Seed or correct a prefix:
```text
set recent.repeater A1B2C3 8.5
```
Clear learned and manually seeded rows:
```text
clear recent.repeater
```
Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`.
Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to `7` rows.
## Direct Path Overrides
`outpath` and `altpath` apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI.
Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width.
```text
get outpath
set outpath A1B2C3,D4E5F6
set outpath direct
set outpath clear
set outpath flood
get altpath
set altpath A1B2C3,D4E5F6
set altpath clear
```
`set outpath direct` sets a zero-hop direct route for a client reachable without repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. `altpath` sends a duplicate reply over a second direct route; clearing it returns replies to a single route.
## Direct Retry Details
The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no recent repeater table entry is available, retry packets use `CR5`.
Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`.
Preset details:
| Preset | Base | Count | Step | SNR gate |
| --- | ---: | ---: | ---: | --- |
| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` |
| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` |
| `mobile` | `175 ms` | `15` | `50 ms` | SF floor |
Example for a quiet fixed repeater:
```text
set retry.preset rooftop
set direct.retry.heard on
set direct.retry.margin 5
```
Example for a moving or weak-link node:
```text
set retry.preset mobile
set direct.retry.margin 0
```
## Troubleshooting
If direct retries are too aggressive:
```text
set direct.retry.count 4
set direct.retry.margin 10
```
If direct retries are too sparse:
```text
set direct.retry.count 15
set direct.retry.margin 0
```
If direct retry is skipping a path you expect it to retry:
```text
get direct.retry.heard
get recent.repeater
```
Either disable the heard gate with `set direct.retry.heard off`, or seed the next-hop prefix with `set recent.repeater <prefix_hex_6> <snr_db>`.
+226
View File
@@ -549,6 +549,208 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const {
if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0) {
return false;
}
prefix_len = packet->getPathHashSize();
memcpy(prefix, packet->path, prefix_len);
return true;
}
int8_t MyMesh::getDirectRetryMinSNRX4() const {
switch (active_sf) {
case 7: return -30;
case 8: return -40;
case 9: return -50;
case 10: return -60;
case 11: return -70;
case 12: return -80;
default: return -60;
}
}
uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const {
if (!_prefs.direct_retry_cr_enabled) return 0;
if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) return 4;
if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) return 5;
if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) return 7;
return 8;
}
uint8_t MyMesh::getDirectRetryPreset() const {
return _prefs.retry_preset;
}
uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const {
return constrain(_prefs.direct_retry_attempts, 1, 15);
}
uint32_t MyMesh::getDirectRetryAttemptStepMillis() const {
return _prefs.direct_retry_step_ms;
}
bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const {
(void)packet;
if (next_hop_hash == NULL || next_hop_hash_len == 0) {
return true;
}
const SimpleMeshTables* tables = static_cast<const SimpleMeshTables*>(getTables());
const SimpleMeshTables::RecentRepeaterInfo* repeater = tables != NULL
? tables->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len)
: NULL;
if (repeater == NULL) {
// Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the
// recent-repeater table below the +2.00 dB starting point.
return true;
}
int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4;
return (int16_t)repeater->snr_x4 >= retry_floor_x4;
}
void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) {
(void)retry_attempt;
int8_t snr_x4 = 8; // unknown repeaters start at +2.00 dB
const SimpleMeshTables* tables = static_cast<const SimpleMeshTables*>(getTables());
if (tables != NULL) {
uint8_t prefix[MAX_HASH_SIZE];
uint8_t prefix_len = 0;
if (extractDirectRetryPrefix(original, prefix, prefix_len)) {
const SimpleMeshTables::RecentRepeaterInfo* repeater = tables->findRecentRepeaterByHash(prefix, prefix_len);
if (repeater != NULL) {
snr_x4 = repeater->snr_x4;
}
}
}
retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4);
}
bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) {
(void)packet;
return false;
}
uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
(void)packet;
return 200;
}
uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
(void)packet;
return getDirectRetryConfiguredMaxAttempts();
}
uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) {
(void)packet;
return _prefs.direct_retry_base_ms + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis());
}
void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("direct retry %s attempt=%u delay=%lu type=%u route=%s",
event ? event : "?",
(uint32_t)retry_attempt,
(unsigned long)delay_millis,
packet ? (uint32_t)packet->getPayloadType() : 0,
packet && packet->isRouteDirect() ? "D" : "F");
#endif
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": direct retry %s attempt=%u delay=%lu type=%u route=%s\n",
event ? event : "?",
(uint32_t)retry_attempt,
(unsigned long)delay_millis,
packet ? (uint32_t)packet->getPayloadType() : 0,
packet && packet->isRouteDirect() ? "D" : "F");
f.close();
}
}
}
void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) {
if (next_hop_hash == NULL || next_hop_hash_len == 0) {
return;
}
SimpleMeshTables* tables = static_cast<SimpleMeshTables*>(getTables());
if (tables != NULL) {
if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) {
tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 7, false, true);
}
}
}
void MyMesh::onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) {
if (next_hop_hash == NULL || next_hop_hash_len == 0) {
return;
}
SimpleMeshTables* tables = static_cast<SimpleMeshTables*>(getTables());
if (tables != NULL) {
tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4, false, true);
}
}
static void formatLocalSnrX4(char* dest, size_t dest_len, int16_t snr_x4) {
int16_t v = snr_x4;
const char* sign = "";
if (v < 0) {
sign = "-";
v = -v;
}
snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25);
}
void MyMesh::formatRecentRepeatersReply(char *reply, int page) {
const SimpleMeshTables* tables = static_cast<const SimpleMeshTables*>(getTables());
if (tables == NULL) {
strcpy(reply, "Error: unsupported");
return;
}
int count = tables->getRecentRepeaterCount();
if (count <= 0) {
strcpy(reply, "> -none-");
return;
}
const int page_size = 4;
int pages = (count + page_size - 1) / page_size;
if (page < 1) page = 1;
if (page > pages) page = pages;
int len = snprintf(reply, 160, "> %d/%d ", page, pages);
int start = (page - 1) * page_size;
for (int i = 0; i < page_size && len < 150; i++) {
const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterNewestByIdx(start + i);
if (info == NULL) break;
char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1];
char snr[12];
mesh::Utils::toHex(prefix, info->prefix, info->prefix_len);
prefix[info->prefix_len * 2] = 0;
formatLocalSnrX4(snr, sizeof(snr), info->snr_x4);
len += snprintf(&reply[len], 160 - len, "%s%s,%s",
i == 0 ? "" : " ",
prefix,
snr);
}
}
bool MyMesh::setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
SimpleMeshTables* tables = static_cast<SimpleMeshTables*>(getTables());
return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4, false, true);
}
void MyMesh::clearRecentRepeaters() {
SimpleMeshTables* tables = static_cast<SimpleMeshTables*>(getTables());
if (tables != NULL) {
tables->clearRecentRepeaters();
}
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
// just try to determine region for packet (apply later in allowPacketForward())
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
@@ -865,6 +1067,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
set_radio_at = revert_radio_at = 0;
active_bw = 0.0f;
active_sf = 0;
active_cr = 0;
_logging = false;
region_load_active = false;
@@ -894,6 +1099,18 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.retry_preset = RETRY_PRESET_ROOFTOP;
_prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT;
_prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
_prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
_prefs.direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
_prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT;
_prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT;
_prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
_prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
_prefs.direct_retry_cr_enabled = 1;
_prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
_prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
// bridge defaults
_prefs.bridge_enabled = 1; // enabled
@@ -962,6 +1179,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
#endif
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
active_bw = _prefs.bw;
active_sf = _prefs.sf;
active_cr = _prefs.cr;
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
@@ -1289,12 +1509,18 @@ void MyMesh::loop() {
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
active_bw = pending_bw;
active_sf = pending_sf;
active_cr = pending_cr;
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
active_bw = _prefs.bw;
active_sf = _prefs.sf;
active_cr = _prefs.cr;
MESH_DEBUG_PRINTLN("Radio params restored");
}
+6 -18
View File
@@ -163,6 +163,8 @@ protected:
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override;
void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
@@ -194,20 +196,6 @@ protected:
void onControlDataRecv(mesh::Packet* packet) override;
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
mesh::Packet* createPacketCopy(const mesh::Packet* packet, const char* caller);
mesh::Packet* createAltPathCopy(const mesh::Packet* packet,
const uint8_t* primary_path, uint8_t primary_path_len,
const uint8_t* alt_path, uint8_t alt_path_len);
void sendFloodReplyWithAltPath(mesh::Packet* packet,
const uint8_t* direct_path, uint8_t direct_path_len,
const uint8_t* alt_path, uint8_t alt_path_len,
unsigned long delay_millis, uint8_t path_hash_size);
void sendDirectWithAltPath(mesh::Packet* packet,
const uint8_t* path, uint8_t path_len,
const uint8_t* alt_path, uint8_t alt_path_len,
uint32_t delay_millis);
void sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt,
uint32_t delay_millis, uint8_t path_hash_size);
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
@@ -248,6 +236,9 @@ public:
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void formatRecentRepeatersReply(char *reply, int page) override;
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override;
void clearRecentRepeaters() override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
@@ -257,10 +248,7 @@ public:
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply);
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
handleCommand(sender_timestamp, NULL, command, reply);
}
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
#if defined(WITH_BRIDGE)
+4 -2
View File
@@ -53,7 +53,7 @@ void setup() {
halt();
}
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -99,6 +99,8 @@ void setup() {
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
board.onBootComplete();
}
void loop() {
@@ -120,7 +122,7 @@ void loop() {
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
+45 -87
View File
@@ -35,6 +35,7 @@ void Mesh::begin() {
_direct_retries[i].retry_at = 0;
_direct_retries[i].retry_delay = 0;
_direct_retries[i].retry_attempts_sent = 0;
_direct_retries[i].next_hop_hash_len = 0;
_direct_retries[i].priority = 0;
_direct_retries[i].progress_marker = 0;
_direct_retries[i].expect_path_growth = false;
@@ -63,6 +64,7 @@ void Mesh::loop() {
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
clearDirectRetrySlot(i);
continue;
}
@@ -183,12 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
}
if (canDecodeDirectPayloadForSelf(pkt)) {
// Some path sources include the final node hash, and some packets are
// heard before all planned hops are consumed. Only stop forwarding once
// this node proves it can decrypt the payload.
removePathPrefix(pkt, pkt->getPathHashCount());
} else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
if (allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
@@ -479,13 +476,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
if (a1) {
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
@@ -496,7 +490,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
extra--;
}
auto a2 = createAck(crc);
auto a2 = createAck(packet->payload, packet->payload_len);
if (a2) {
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
@@ -508,9 +502,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
}
void Mesh::clearDirectRetrySlot(int idx) {
if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) {
releasePacket(_direct_retries[idx].packet);
}
_direct_retries[idx].packet = NULL;
_direct_retries[idx].trigger_packet = NULL;
_direct_retries[idx].retry_started_at = 0;
@@ -518,6 +509,8 @@ void Mesh::clearDirectRetrySlot(int idx) {
_direct_retries[idx].retry_at = 0;
_direct_retries[idx].retry_delay = 0;
_direct_retries[idx].retry_attempts_sent = 0;
memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash));
_direct_retries[idx].next_hop_hash_len = 0;
_direct_retries[idx].priority = 0;
_direct_retries[idx].progress_marker = 0;
_direct_retries[idx].expect_path_growth = false;
@@ -558,6 +551,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
}
int8_t echo_snr_x4 = packet->_snr;
onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4);
if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) {
if (_direct_retries[i].packet != NULL) {
// Success quality comes from the received downstream echo, not the original upstream RX.
@@ -620,16 +614,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX;
}
if (_direct_retries[i].retry_attempts_sent >= max_attempts) {
Packet* final_wait = obtainNewPacket();
if (final_wait == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
clearDirectRetrySlot(i);
continue;
}
*final_wait = *packet;
_direct_retries[i].packet = final_wait;
// Dispatcher releases the retry packet after this hook. Keep only retry metadata
// for the final echo window so pool exhaustion cannot force a premature failure.
_direct_retries[i].packet = NULL;
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
_direct_retries[i].waiting_final_echo = true;
_direct_retries[i].queued = false;
@@ -783,62 +770,6 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h
}
}
bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) {
if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) {
return false;
}
switch (packet->getPayloadType()) {
case PAYLOAD_TYPE_PATH:
case PAYLOAD_TYPE_REQ:
case PAYLOAD_TYPE_RESPONSE:
case PAYLOAD_TYPE_TXT_MSG: {
if (packet->payload_len < 2) {
return false;
}
int i = 0;
uint8_t dest_hash = packet->payload[i++];
uint8_t src_hash = packet->payload[i++];
if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
return false;
}
int num = searchPeersByHash(&src_hash);
for (int j = 0; j < num; j++) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);
uint8_t data[MAX_PACKET_PAYLOAD];
if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) {
return true;
}
}
return false;
}
case PAYLOAD_TYPE_ANON_REQ: {
int i = 0;
uint8_t dest_hash = packet->payload[i++];
if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
return false;
}
Identity sender(&packet->payload[i]);
i += PUB_KEY_SIZE;
uint8_t secret[PUB_KEY_SIZE];
self_id.calcSharedSecret(secret, sender);
uint8_t data[MAX_PACKET_PAYLOAD];
return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0;
}
default:
return false;
}
}
void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
const uint8_t* next_hop_hash;
uint8_t next_hop_hash_len;
@@ -849,6 +780,18 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
return;
}
uint8_t retry_key[MAX_HASH_SIZE];
calculateDirectRetryKey(packet, retry_key);
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active
&& memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0
&& _direct_retries[i].progress_marker == progress_marker
&& _direct_retries[i].expect_path_growth == expect_path_growth) {
return;
}
}
int slot_idx = -1;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
@@ -864,7 +807,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
// Only store retry metadata here; allocate the retry packet after the initial TX really completes.
uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0);
calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key);
memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE);
_direct_retries[slot_idx].packet = NULL;
_direct_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
_direct_retries[slot_idx].retry_started_at = 0;
@@ -872,6 +815,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
_direct_retries[slot_idx].retry_at = 0;
_direct_retries[slot_idx].retry_delay = retry_delay;
_direct_retries[slot_idx].retry_attempts_sent = 0;
memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash));
memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len);
_direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len;
_direct_retries[slot_idx].priority = priority;
_direct_retries[slot_idx].progress_marker = progress_marker;
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
@@ -1036,7 +982,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con
return packet;
}
Packet* Mesh::createAck(uint32_t ack_crc) {
Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) {
if (ack_len > sizeof(Packet::payload)) return NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime());
@@ -1044,13 +992,19 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
}
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, &ack_crc, 4);
packet->payload_len = 4;
memcpy(packet->payload, ack_hash, ack_len);
packet->payload_len = ack_len;
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
Packet* Mesh::createAck(uint32_t ack_crc) {
return createAck((const uint8_t*)&ack_crc, 4);
}
Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) {
if (ack_len + 1 > sizeof(Packet::payload)) return NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
@@ -1059,12 +1013,16 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
memcpy(&packet->payload[1], ack_hash, ack_len);
packet->payload_len = ack_len + 1;
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
return createMultiAck((const uint8_t*)&ack_crc, 4, remaining);
}
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
+18 -1
View File
@@ -29,6 +29,10 @@ public:
* and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets.
*/
class Mesh : public Dispatcher {
RNG* _rng;
RTCClock* _rtc;
MeshTables* _tables;
struct DirectRetryEntry {
Packet* packet;
Packet* trigger_packet;
@@ -38,6 +42,8 @@ class Mesh : public Dispatcher {
uint32_t retry_delay;
uint8_t retry_attempts_sent;
uint8_t retry_key[MAX_HASH_SIZE];
uint8_t next_hop_hash[MAX_HASH_SIZE];
uint8_t next_hop_hash_len;
uint8_t priority;
uint8_t progress_marker;
bool expect_path_growth;
@@ -58,7 +64,6 @@ class Mesh : public Dispatcher {
void clearPendingDirectRetryOnSendFail(const Packet* packet);
bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len,
uint8_t& progress_marker, bool& expect_path_growth) const;
bool canDecodeDirectPayloadForSelf(const Packet* packet);
void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
@@ -133,6 +138,16 @@ protected:
*/
virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
/**
* \brief Optional hook for link-quality feedback when all direct-retry attempts fail.
*/
virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { }
/**
* \brief Optional hook for link-quality feedback when a direct-retry echo is heard.
*/
virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { }
/**
* \brief Optional hook to set local-only transmit options on a retry packet before it is queued.
*/
@@ -253,7 +268,9 @@ public:
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len);
Packet* createAck(uint32_t ack_crc);
Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining);
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
+1 -12
View File
@@ -1,7 +1,5 @@
#include "ClientACL.h"
static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1;
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
@@ -30,7 +28,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
uint8_t unused[2];
memset(&c, 0, sizeof(c));
c.alt_path_len = OUT_PATH_UNKNOWN;
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
@@ -39,10 +36,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) {
success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1);
success = success && (file.read(c.alt_path, 64) == 64);
}
if (!success) break; // EOF
@@ -64,8 +57,7 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
unused[0] = CONTACT_RECORD_VERSION_ALT_PATH;
unused[1] = 0;
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_clients; i++) {
auto c = &clients[i];
@@ -78,8 +70,6 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1);
success = success && (file.write(c->alt_path, 64) == 64);
if (!success) break; // write failed
}
@@ -125,7 +115,6 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
c->permissions = init_perms;
c->id = id;
c->out_path_len = OUT_PATH_UNKNOWN;
c->alt_path_len = OUT_PATH_UNKNOWN;
return c;
}
+1 -4
View File
@@ -10,16 +10,13 @@
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define OUT_PATH_FORCE_FLOOD 0xFE
#define OUT_PATH_UNKNOWN 0xFF
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
uint8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t alt_path_len;
uint8_t alt_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
uint32_t last_activity; // by OUR clock (transient)
+275 -2
View File
@@ -27,6 +27,113 @@ static bool isValidName(const char *n) {
return true;
}
static bool looksNumeric(const char* s) {
if (s == NULL) return false;
while (*s == ' ') s++;
if (*s == '-' || *s == '+') s++;
bool saw_digit = false;
while (*s) {
if (*s >= '0' && *s <= '9') {
saw_digit = true;
} else if (*s != '.') {
break;
}
s++;
}
return saw_digit;
}
static int16_t parseSnrDbX4(const char* s) {
float db = atof(s);
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
}
static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) {
int16_t v = snr_x4;
const char* sign = "";
if (v < 0) {
sign = "-";
v = -v;
}
snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25);
}
static const char* retryPresetName(uint8_t preset) {
switch (preset) {
case RETRY_PRESET_INFRA: return "infra";
case RETRY_PRESET_ROOFTOP: return "rooftop";
case RETRY_PRESET_MOBILE: return "mobile";
default: return "custom";
}
}
static void markDirectRetryPrefsValid(NodePrefs* prefs) {
prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
}
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
prefs->retry_preset = preset;
if (preset == RETRY_PRESET_INFRA) {
prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4;
} else if (preset == RETRY_PRESET_MOBILE) {
prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4;
} else {
prefs->retry_preset = RETRY_PRESET_ROOFTOP;
prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
}
markDirectRetryPrefsValid(prefs);
}
static void setDefaultDirectRetryPrefs(NodePrefs* prefs) {
applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP);
prefs->direct_retry_cr_enabled = 1;
prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
markDirectRetryPrefsValid(prefs);
}
static bool directRetryPrefsValid(const NodePrefs* prefs) {
return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0
&& prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1;
}
static bool parseRetryPreset(const char* s, uint8_t& preset) {
if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) {
preset = RETRY_PRESET_INFRA;
return true;
}
if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) {
preset = RETRY_PRESET_ROOFTOP;
return true;
}
if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) {
preset = RETRY_PRESET_MOBILE;
return true;
}
return false;
}
static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) {
size_t hex_len = strlen(text);
if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) {
return false;
}
prefix_len = hex_len / 2;
return mesh::Utils::fromHex(prefix, prefix_len, text);
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@@ -93,7 +200,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
// next: 310
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -125,6 +243,17 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
if (!directRetryPrefsValid(_prefs)) {
setDefaultDirectRetryPrefs(_prefs);
}
if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) {
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
}
_prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15);
_prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000);
_prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000);
_prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160);
_prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1);
file.close();
}
@@ -190,7 +319,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
markDirectRetryPrefsValid(_prefs);
file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
// next: 310
file.close();
}
@@ -301,6 +442,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else if (memcmp(command, "clear stats", 11) == 0) {
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
} else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) {
_callbacks->clearRecentRepeaters();
strcpy(reply, "OK");
} else if (memcmp(command, "get ", 4) == 0) {
handleGetCmd(sender_timestamp, command, reply);
} else if (memcmp(command, "set ", 4) == 0) {
@@ -680,6 +824,104 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error, must be 0-2");
}
} else if (memcmp(config, "retry.preset ", 13) == 0) {
uint8_t preset;
if (parseRetryPreset(&config[13], preset)) {
applyDirectRetryPreset(_prefs, preset);
savePrefs();
sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset));
} else {
strcpy(reply, "Error, must be infra, rooftop, or mobile");
}
} else if (memcmp(config, "direct.retry.margin ", 20) == 0) {
if (!looksNumeric(&config[20])) {
strcpy(reply, "Error, must be 0-40 dB");
} else {
int16_t margin_x4 = parseSnrDbX4(&config[20]);
if (margin_x4 >= 0 && margin_x4 <= 160) {
_prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-40 dB");
}
}
} else if (memcmp(config, "direct.retry.count ", 19) == 0) {
int attempts = _atoi(&config[19]);
if (attempts >= 1 && attempts <= 15) {
_prefs->direct_retry_attempts = (uint8_t)attempts;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 1-15");
}
} else if (memcmp(config, "direct.retry.base ", 18) == 0) {
int base_ms = _atoi(&config[18]);
if (base_ms >= 10 && base_ms <= 5000) {
_prefs->direct_retry_base_ms = (uint16_t)base_ms;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 10-5000 ms");
}
} else if (memcmp(config, "direct.retry.step ", 18) == 0) {
int step_ms = _atoi(&config[18]);
if (step_ms >= 0 && step_ms <= 5000) {
_prefs->direct_retry_step_ms = (uint16_t)step_ms;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-5000 ms");
}
} else if (memcmp(config, "direct.retry.cr ", 16) == 0) {
if (memcmp(&config[16], "off", 3) == 0) {
_prefs->direct_retry_cr_enabled = 0;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(tmp, &config[16]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ',');
if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) {
int16_t cr4 = parseSnrDbX4(parts[0]);
int16_t cr5 = parseSnrDbX4(parts[1]);
int16_t cr7 = parseSnrDbX4(parts[2]);
int16_t cr8 = parseSnrDbX4(parts[3]);
if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) {
_prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4;
_prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5;
_prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7;
_prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8;
_prefs->direct_retry_cr_enabled = 1;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
}
} else {
strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off");
}
}
} else if (memcmp(config, "recent.repeater ", 16) == 0) {
strcpy(tmp, &config[16]);
const char *parts[2];
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
uint8_t prefix[MAX_HASH_SIZE];
uint8_t prefix_len = 0;
int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0;
if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) {
strcpy(reply, "Error, use: set recent.repeater <hex> <snr_db>");
} else if (snr_x4 < -128 || snr_x4 > 127) {
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
} else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, table rejected prefix");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
@@ -862,6 +1104,37 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
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, "retry.preset", 12) == 0) {
sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset));
} else if (memcmp(config, "direct.retry.margin", 19) == 0) {
char margin[12];
formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4);
sprintf(reply, "> %s", margin);
} else if (memcmp(config, "direct.retry.count", 18) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts);
} else if (memcmp(config, "direct.retry.base", 17) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
} else if (memcmp(config, "direct.retry.step", 17) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms);
} else if (memcmp(config, "direct.retry.cr", 15) == 0) {
if (!_prefs->direct_retry_cr_enabled) {
strcpy(reply, "> off");
} else {
char cr4[12], cr5[12], cr7[12], cr8[12];
formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4);
formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4);
formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4);
formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4);
sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8);
}
} else if (memcmp(config, "recent.repeater", 15) == 0) {
int page = 1;
const char* cursor = &config[15];
while (*cursor == ' ') cursor++;
if (memcmp(cursor, "page ", 5) == 0) cursor += 5;
if (*cursor) page = _atoi(cursor);
if (page < 1) page = 1;
_callbacks->formatRecentRepeatersReply(reply, page);
} else if (memcmp(config, "owner.info", 10) == 0) {
auto start = reply;
*reply++ = '>';
+51
View File
@@ -19,6 +19,34 @@
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define RETRY_PRESET_INFRA 0
#define RETRY_PRESET_ROOFTOP 1
#define RETRY_PRESET_MOBILE 2
#define RETRY_PRESET_CUSTOM 0xFF
#define DIRECT_RETRY_INFRA_BASE_MS 275
#define DIRECT_RETRY_INFRA_COUNT 4
#define DIRECT_RETRY_INFRA_STEP_MS 150
#define DIRECT_RETRY_INFRA_MARGIN_X4 60
#define DIRECT_RETRY_ROOFTOP_BASE_MS 175
#define DIRECT_RETRY_ROOFTOP_COUNT 15
#define DIRECT_RETRY_ROOFTOP_STEP_MS 100
#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20
#define DIRECT_RETRY_MOBILE_BASE_MS 175
#define DIRECT_RETRY_MOBILE_COUNT 15
#define DIRECT_RETRY_MOBILE_STEP_MS 50
#define DIRECT_RETRY_MOBILE_MARGIN_X4 0
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10
#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10
#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1
#define DIRECT_RETRY_PREFS_MAGIC_1 0x52
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -65,6 +93,17 @@ 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 retry_preset;
uint8_t direct_retry_attempts;
uint16_t direct_retry_base_ms;
uint16_t direct_retry_step_ms;
uint16_t direct_retry_snr_margin_x4;
int8_t direct_retry_cr4_snr_x4;
int8_t direct_retry_cr5_snr_x4;
int8_t direct_retry_cr7_snr_x4;
int8_t direct_retry_cr8_snr_x4;
uint8_t direct_retry_cr_enabled;
uint8_t direct_retry_prefs_magic[2];
};
class CommonCLICallbacks {
@@ -88,6 +127,18 @@ public:
virtual void formatStatsReply(char *reply) = 0;
virtual void formatRadioStatsReply(char *reply) = 0;
virtual void formatPacketStatsReply(char *reply) = 0;
virtual void formatRecentRepeatersReply(char *reply, int page) {
(void)page;
if (reply != NULL) reply[0] = 0;
}
virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
(void)prefix;
(void)prefix_len;
(void)snr_x4;
return false;
}
virtual void clearRecentRepeaters() {
}
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
+22 -86
View File
@@ -9,8 +9,7 @@
#include <FS.h>
#endif
#define MAX_PACKET_HASHES 128
#define MAX_PACKET_ACKS 64
#define MAX_PACKET_HASHES (128+32)
#ifndef MAX_RECENT_REPEATERS
// Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=<n>.
#if defined(ESP32) || defined(ESP32_PLATFORM)
@@ -39,8 +38,6 @@ public:
private:
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_idx;
uint32_t _acks[MAX_PACKET_ACKS];
int _next_ack_idx;
uint32_t _direct_dups, _flood_dups;
RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS];
int _next_recent_repeater_idx;
@@ -48,20 +45,6 @@ private:
RecentRepeaterAllowFn _recent_repeater_allow_fn;
void* _recent_repeater_allow_ctx;
bool hasSeenAck(uint32_t ack) const {
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
return true;
}
}
return false;
}
void storeAck(uint32_t ack) {
_acks[_next_ack_idx] = ack;
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS;
}
bool hasSeenHash(const uint8_t* hash) const {
const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
@@ -148,8 +131,6 @@ public:
SimpleMeshTables() {
memset(_hashes, 0, sizeof(_hashes));
_next_idx = 0;
memset(_acks, 0, sizeof(_acks));
_next_ack_idx = 0;
_direct_dups = _flood_dups = 0;
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
_next_recent_repeater_idx = 0;
@@ -162,8 +143,6 @@ public:
void restoreFrom(File f) {
f.read(_hashes, sizeof(_hashes));
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
f.read((uint8_t *) &_acks[0], sizeof(_acks));
f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
// Recent repeater entries are intentionally not restored across boots.
// This avoids struct-layout migration issues and keeps stale path quality
// stats from persisting indefinitely.
@@ -173,31 +152,10 @@ public:
void saveTo(File f) {
f.write(_hashes, sizeof(_hashes));
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
f.write((const uint8_t *) &_acks[0], sizeof(_acks));
f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
}
#endif
bool hasSeen(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
recordRecentRepeater(packet);
uint32_t ack;
memcpy(&ack, packet->payload, 4);
if (hasSeenAck(ack)) {
if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats
} else {
_flood_dups++;
}
return true;
}
storeAck(ack);
return false;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
@@ -217,15 +175,6 @@ public:
void markSent(const mesh::Packet* packet) override {
// Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves.
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
if (!hasSeenAck(ack)) {
storeAck(ack);
}
return;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (!hasSeenHash(hash)) {
@@ -234,25 +183,14 @@ public:
}
void clear(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
_acks[i] = 0;
break;
}
}
} else {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
}
}
@@ -282,17 +220,13 @@ public:
return false;
}
// Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry.
// Keep exact prefixes distinct so a 1-byte path prefix does not collapse
// independent 2/3-byte repeaters that share the same first byte.
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
continue;
}
if (prefix_len > existing.prefix_len) {
memset(existing.prefix, 0, sizeof(existing.prefix));
memcpy(existing.prefix, prefix, prefix_len);
existing.prefix_len = prefix_len;
}
if (snr_locked) {
existing.snr_x4 = snr_x4;
existing.snr_locked = 1;
@@ -352,14 +286,9 @@ public:
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
continue;
}
if (prefix_len > existing.prefix_len) {
memset(existing.prefix, 0, sizeof(existing.prefix));
memcpy(existing.prefix, prefix, prefix_len);
existing.prefix_len = prefix_len;
}
if (!existing.snr_locked) {
int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4;
if (lowered < -128) {
@@ -432,18 +361,25 @@ public:
return NULL;
}
// Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match.
// Prefer exact matches. If none exists, fall back to the newest longest
// overlapping prefix so coarse learned prefixes can still inform CR.
const RecentRepeaterInfo* best = NULL;
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS;
const RecentRepeaterInfo* info = &_recent_repeaters[idx];
if (info->prefix_len == 0) {
continue;
}
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) {
return info;
}
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
if (best == NULL || info->prefix_len > best->prefix_len) {
best = info;
}
}
}
return NULL;
return best;
}
void clearRecentRepeaters() {
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));