mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-29 09:48:52 +00:00
Add direct retry controls
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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>`.
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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++ = '>';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user