mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(repeater): politeness knobs in Settings (skip-advert, max-hops, yield, min-SNR)
Four opt-in filters, all default off, consulted only when the repeater is on (MyMesh::allowPacketForward) and shown under Settings > Radio only while it is: - Skip advert: don't re-flood ADVERT packets (highest-volume flood traffic). - Max hops: drop a flood packet once it has travelled N hops. - Yield: scale the FORWARDED flood retransmit delay so a mobile companion defers to better-sited fixed repeaters; never touches the node's own sends. - Min SNR: drop a flood copy received below a dB threshold so marginal fringe traffic isn't re-flooded. All four are flood-only — on a direct route this node is the named next hop, so dropping there would kill delivery. Persisted behind a new schema sentinel (0xC0DE000E) with stray-byte clamps; min-SNR uses a -128 "disabled" sentinel so an upgraded file can't read as "filter at 0 dB". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
#ifdef DISPLAY_ROTATION
|
||||
_prefs.display_rotation = DISPLAY_ROTATION;
|
||||
#endif
|
||||
// 0 is a valid SNR threshold, so the "off" state needs its own sentinel set
|
||||
// before reading — an older file lacking this field must read as disabled,
|
||||
// not as "filter everything below 0 dB".
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
||||
File file = openRead(_fs, filename);
|
||||
if (!file) return;
|
||||
|
||||
@@ -332,6 +336,19 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
||||
rd(_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
||||
rd(_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
||||
// → 0xC0DE000E: repeater politeness knobs. On a pre-E file the bytes here are
|
||||
// that file's own sentinel tail, so clamp every out-of-range value back to its
|
||||
// "off" default (same stray-byte handling as the fields below).
|
||||
rd(&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts));
|
||||
rd(&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
||||
rd(&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
||||
rd(&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
||||
if (_prefs.repeat_skip_adverts > 1) _prefs.repeat_skip_adverts = 0;
|
||||
if (_prefs.repeat_max_hops > 64) _prefs.repeat_max_hops = 0;
|
||||
if (_prefs.repeat_delay_boost > 8) _prefs.repeat_delay_boost = 0;
|
||||
if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED &&
|
||||
(_prefs.repeat_min_snr < -30 || _prefs.repeat_min_snr > 20))
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
||||
// → 0xC0DE000B: append bot_commands_enabled + quiet-hours. Older files leave
|
||||
// stray bytes here; clamp so upgraders fall back to off / no quiet hours.
|
||||
if (_prefs.bot_commands_enabled > 1) _prefs.bot_commands_enabled = 0;
|
||||
@@ -489,6 +506,10 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
||||
file.write((uint8_t *)_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
||||
file.write((uint8_t *)_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
||||
file.write((uint8_t *)&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts));
|
||||
file.write((uint8_t *)&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
||||
file.write((uint8_t *)&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
||||
file.write((uint8_t *)&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -282,7 +282,12 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
|
||||
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
|
||||
return getRNG()->nextInt(0, 5*t + 1);
|
||||
uint32_t d = getRNG()->nextInt(0, 5*t + 1);
|
||||
// Politeness yield (Settings > Radio): scale the flood retransmit delay so a
|
||||
// mobile companion waits longer and lets better-sited fixed repeaters win the
|
||||
// flood first. Only forwarded floods reach here — own sends pass their own
|
||||
// delay to sendFlood() — so this never slows the companion's own traffic.
|
||||
return d * (1 + _prefs.repeat_delay_boost);
|
||||
}
|
||||
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
|
||||
@@ -551,7 +556,15 @@ bool MyMesh::isRepeatLooped(const mesh::Packet* packet) const {
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
|
||||
if (_prefs.client_repeat == 0) return false;
|
||||
// "Politeness" filters (Settings > Radio) — all default off, so a plain repeater
|
||||
// is unaffected. Flood-only by design: on a direct route this node is the named
|
||||
// next hop, so dropping there would kill delivery with no alternate path, while
|
||||
// dropping a flood copy just trims redundancy other nodes still carry.
|
||||
if (packet->isRouteFlood()) {
|
||||
if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED
|
||||
&& packet->getSNR() < (float)_prefs.repeat_min_snr) return false;
|
||||
if (_prefs.repeat_skip_adverts && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) return false;
|
||||
if (_prefs.repeat_max_hops > 0 && packet->getPathHashCount() >= _prefs.repeat_max_hops) return false;
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= REPEAT_MAX_ADVERT_HOPS) return false;
|
||||
if (isRepeatLooped(packet)) return false;
|
||||
}
|
||||
|
||||
@@ -152,11 +152,29 @@ struct NodePrefs { // persisted to file
|
||||
static const uint8_t USER_RADIO_PRESET_MAX = 4;
|
||||
UserRadioPreset user_radio_presets[USER_RADIO_PRESET_MAX];
|
||||
|
||||
// Repeater "politeness" — only consulted when client_repeat is on, via
|
||||
// MyMesh::allowPacketForward(). Both default to off (0) so behaviour is
|
||||
// unchanged until the user opts in (Settings > Radio).
|
||||
// repeat_skip_adverts: 1 = don't re-flood ADVERT packets (the highest-volume
|
||||
// flood traffic); messages/acks still relay.
|
||||
// repeat_max_hops: drop a flood packet once it has already travelled this
|
||||
// many hops. 0 = no hop limit.
|
||||
// repeat_delay_boost: extra retransmit-delay multiplier for FORWARDED floods
|
||||
// only (own sends are unaffected) — a mobile companion yields to better-sited
|
||||
// fixed repeaters. Effective delay = base * (1 + repeat_delay_boost). 0 = off.
|
||||
// repeat_min_snr: drop a flood packet received below this SNR (dB), so marginal
|
||||
// fringe traffic isn't re-flooded. REPEAT_SNR_DISABLED (-128) = off.
|
||||
uint8_t repeat_skip_adverts;
|
||||
uint8_t repeat_max_hops;
|
||||
uint8_t repeat_delay_boost;
|
||||
int8_t repeat_min_snr;
|
||||
static const int8_t REPEAT_SNR_DISABLED = -128;
|
||||
|
||||
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000D;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000E;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
@@ -59,6 +59,10 @@ class SettingsScreen : public UIScreen {
|
||||
RADIO_PRESET,
|
||||
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
||||
REPEATER,
|
||||
RPT_SKIP_ADV, // repeater politeness sub-items, only shown when REPEATER is on
|
||||
RPT_MAX_HOPS,
|
||||
RPT_DELAY,
|
||||
RPT_MIN_SNR,
|
||||
POWER_SAVE,
|
||||
TX_APC,
|
||||
// System section
|
||||
@@ -274,11 +278,21 @@ class SettingsScreen : public UIScreen {
|
||||
cur_collapsed = (_collapsed >> cur_sec) & 1;
|
||||
if (_vis_count < MAX_VIS) _vis[_vis_count++] = (uint8_t)i;
|
||||
} else if (!cur_collapsed && _vis_count < MAX_VIS) {
|
||||
// Repeater politeness knobs are meaningless when the repeater is off, so
|
||||
// hide them until it's enabled — keeps the Radio section short by default.
|
||||
if (isRepeaterSubItem(i)) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p || !p->client_repeat) continue;
|
||||
}
|
||||
_vis[_vis_count++] = (uint8_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isRepeaterSubItem(int item) const {
|
||||
return item == RPT_SKIP_ADV || item == RPT_MAX_HOPS || item == RPT_DELAY || item == RPT_MIN_SNR;
|
||||
}
|
||||
|
||||
bool isHomePage(int item) const {
|
||||
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
|
||||
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
|
||||
@@ -609,6 +623,31 @@ class SettingsScreen : public UIScreen {
|
||||
display.print("Repeater");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->client_repeat) ? "ON" : "OFF");
|
||||
} else if (item == RPT_SKIP_ADV) {
|
||||
display.print(" Skip advert");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->repeat_skip_adverts) ? "ON" : "OFF");
|
||||
} else if (item == RPT_MAX_HOPS) {
|
||||
display.print(" Max hops");
|
||||
char buf[6];
|
||||
if (p && p->repeat_max_hops > 0) snprintf(buf, sizeof(buf), "%d", (int)p->repeat_max_hops);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_DELAY) {
|
||||
display.print(" Yield");
|
||||
char buf[6];
|
||||
if (p && p->repeat_delay_boost > 0) snprintf(buf, sizeof(buf), "x%d", (int)p->repeat_delay_boost + 1);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_MIN_SNR) {
|
||||
display.print(" Min SNR");
|
||||
char buf[8];
|
||||
if (p && p->repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED) snprintf(buf, sizeof(buf), "%ddB", (int)p->repeat_min_snr);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -1004,9 +1043,36 @@ public:
|
||||
}
|
||||
if (_selected == REPEATER && p && (left || right || enter)) {
|
||||
p->client_repeat = p->client_repeat ? 0 : 1;
|
||||
buildVis(); // show/hide the politeness sub-items immediately
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == RPT_SKIP_ADV && p && (left || right || enter)) {
|
||||
p->repeat_skip_adverts ^= 1;
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == RPT_MAX_HOPS && p) {
|
||||
if (right && p->repeat_max_hops < 8) { p->repeat_max_hops++; _dirty = true; return true; }
|
||||
if (left && p->repeat_max_hops > 0) { p->repeat_max_hops--; _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == RPT_DELAY && p) {
|
||||
if (right && p->repeat_delay_boost < 8) { p->repeat_delay_boost++; _dirty = true; return true; }
|
||||
if (left && p->repeat_delay_boost > 0) { p->repeat_delay_boost--; _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == RPT_MIN_SNR && p) {
|
||||
// Steps -20..10 dB; stepping left past the floor turns the filter off.
|
||||
int8_t v = p->repeat_min_snr;
|
||||
if (right) {
|
||||
v = (v == NodePrefs::REPEAT_SNR_DISABLED) ? -20 : (v < 10 ? v + 1 : 10);
|
||||
p->repeat_min_snr = v; _dirty = true; return true;
|
||||
}
|
||||
if (left) {
|
||||
if (v != NodePrefs::REPEAT_SNR_DISABLED)
|
||||
p->repeat_min_snr = (v <= -20) ? NodePrefs::REPEAT_SNR_DISABLED : (int8_t)(v - 1);
|
||||
_dirty = true; return true;
|
||||
}
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
|
||||
Reference in New Issue
Block a user