mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor), 4 persisted user preset slots, and the packet pool bumped 16->32 so queued retransmits stop starving incoming-packet allocation while relaying. Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR), all flood-only and off by default, plus overhear suppression that cancels a queued retransmit if a peer relays the same packet first. Adaptive Power Control is suppressed while relaying (pins TX power to the ceiling) and duty-cycle RX is forced off, since a repeater needs to hear and relay at consistent power. A dedicated Tools > Repeater screen consolidates the toggle, the politeness knobs, and live forwarding stats, plus an optional "Custom" radio profile — a dedicated frequency/SF/BW/CR for relaying, separate from the companion's own network, band-matched to the companion frequency by default and used everywhere a radio change can happen (boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings radio edits) so the device never silently falls back to the wrong params mid-relay. Diagnostics gains the actually-forwarded packet count, real heap-free via mallinfo(), a reset-counters popup, and hardened loop-detect bounds. Also: a frequency-floor fix and a float-equality preset-match fix in the repeater profile logic, deduped BW-table/valCol/profile- seeding helpers shared between Settings and the Repeater screen, and a build.sh fix tolerating control characters in `pio project config`'s JSON dump. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -240,6 +240,9 @@ void Dispatcher::checkRecv() {
|
||||
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
|
||||
|
||||
n_recv_by_type[pkt->getPayloadType()]++;
|
||||
// Overhear suppression: if we just heard a flood packet that we still have
|
||||
// queued to retransmit, another node beat us to it — drop our copy.
|
||||
if (pkt->isRouteFlood() && wantsOverhearSuppress()) suppressQueuedDuplicate(pkt);
|
||||
if (pkt->isRouteFlood()) {
|
||||
n_recv_flood++;
|
||||
|
||||
@@ -261,6 +264,27 @@ void Dispatcher::checkRecv() {
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::suppressQueuedDuplicate(Packet* pkt) {
|
||||
uint8_t h[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(h);
|
||||
// Scan the whole outbound queue (not just due entries). hasSeen() already
|
||||
// keeps at most one copy queued, so the first hash match is the only one.
|
||||
int n = _mgr->getOutboundTotal();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Packet* q = _mgr->getOutboundByIdx(i);
|
||||
if (q == NULL || !q->isRouteFlood()) continue; // only ever suppress a queued flood retransmit
|
||||
uint8_t qh[MAX_HASH_SIZE];
|
||||
q->calculatePacketHash(qh);
|
||||
if (memcmp(h, qh, MAX_HASH_SIZE) == 0) {
|
||||
_mgr->removeOutboundByIdx(i);
|
||||
onRetransmitCancelled(q);
|
||||
_mgr->free(q);
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher: overhear suppressed a queued retransmit", getLogDateTime());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::processRecvPacket(Packet* pkt) {
|
||||
DispatcherAction action = onRecvPacket(pkt);
|
||||
if (action == ACTION_RELEASE) {
|
||||
|
||||
@@ -170,6 +170,14 @@ protected:
|
||||
virtual int getAGCResetInterval() const { return 0; } // disabled by default
|
||||
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
|
||||
|
||||
// When true, a received flood packet whose hash matches one still waiting in
|
||||
// the outbound queue cancels that queued retransmit (overhear suppression):
|
||||
// another node already relayed it, so this node stays quiet. Default off.
|
||||
virtual bool wantsOverhearSuppress() const { return false; }
|
||||
// Hook fired when such a queued retransmit is cancelled — lets a sub-class
|
||||
// keep its forward counter honest. Default no-op.
|
||||
virtual void onRetransmitCancelled(Packet* packet) { }
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void loop();
|
||||
@@ -189,7 +197,8 @@ public:
|
||||
uint32_t getNumRecvByType(uint8_t payload_type) const { return n_recv_by_type[payload_type & 0x0F]; }
|
||||
int getPoolFreeCount() const { return _mgr->getFreeCount(); }
|
||||
int getOutboundQueueLen() const { return _mgr->getOutboundTotal(); }
|
||||
void resetStats() {
|
||||
uint16_t getErrFlags() const { return _err_flags; } // ERR_EVENT_* bitmask since last resetStats()
|
||||
virtual void resetStats() {
|
||||
n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0;
|
||||
memset(n_sent_by_type, 0, sizeof(n_sent_by_type));
|
||||
memset(n_recv_by_type, 0, sizeof(n_recv_by_type));
|
||||
@@ -205,6 +214,7 @@ public:
|
||||
private:
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
void suppressQueuedDuplicate(Packet* pkt); // overhear cancel (see wantsOverhearSuppress)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace mesh {
|
||||
|
||||
void Mesh::begin() {
|
||||
Dispatcher::begin();
|
||||
n_forwarded = 0;
|
||||
}
|
||||
|
||||
void Mesh::loop() {
|
||||
@@ -60,6 +61,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
|
||||
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
n_forwarded++;
|
||||
return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable?
|
||||
}
|
||||
}
|
||||
@@ -100,7 +102,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
removeSelfFromPath(pkt);
|
||||
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
|
||||
n_forwarded++;
|
||||
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
|
||||
}
|
||||
}
|
||||
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
|
||||
@@ -336,6 +339,7 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
|
||||
packet->setPathHashCount(n + 1);
|
||||
|
||||
uint32_t d = getRetransmitDelay(packet);
|
||||
n_forwarded++;
|
||||
// as this propagates outwards, give it lower and lower priority
|
||||
return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away
|
||||
}
|
||||
|
||||
12
src/Mesh.h
12
src/Mesh.h
@@ -27,6 +27,7 @@ class Mesh : public Dispatcher {
|
||||
RTCClock* _rtc;
|
||||
RNG* _rng;
|
||||
MeshTables* _tables;
|
||||
uint32_t n_forwarded; // packets actually re-transmitted (repeater/transport role)
|
||||
|
||||
void removeSelfFromPath(Packet* packet);
|
||||
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
@@ -55,6 +56,10 @@ protected:
|
||||
*/
|
||||
virtual bool allowPacketForward(const Packet* packet);
|
||||
|
||||
// Keep the forward counter honest when an overhear cancels a queued retransmit:
|
||||
// the packet was counted at the ACTION_RETRANSMIT decision, so back it out here.
|
||||
void onRetransmitCancelled(Packet* packet) override { if (n_forwarded) n_forwarded--; }
|
||||
|
||||
/**
|
||||
* \returns number of milliseconds delay to apply to retransmitting the given packet.
|
||||
*/
|
||||
@@ -178,6 +183,13 @@ public:
|
||||
|
||||
LocalIdentity self_id;
|
||||
|
||||
// packets actually re-transmitted (vs. just permitted by allowPacketForward()) —
|
||||
// confirms a repeater/transport role is really forwarding, not just configured to.
|
||||
uint32_t getNumForwarded() const { return n_forwarded; }
|
||||
|
||||
// also clear the forward counter alongside the Dispatcher packet counters.
|
||||
void resetStats() override { Dispatcher::resetStats(); n_forwarded = 0; }
|
||||
|
||||
RNG* getRNG() const { return _rng; }
|
||||
RTCClock* getRTCClock() const { return _rtc; }
|
||||
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include <malloc.h>
|
||||
|
||||
// Same linker symbols + _sbrk() the core's own new.cpp uses to implement
|
||||
// malloc's heap (see framework-arduinoadafruitnrf52/cores/nRF5/new.cpp) —
|
||||
// there's no separate "free heap" API on this core, so this mirrors it.
|
||||
extern "C" char* _sbrk(int incr);
|
||||
// Heap region size from the linker symbols the core's new.cpp uses for sbrk
|
||||
// (framework-arduinoadafruitnrf52/cores/nRF5/new.cpp). "Used" comes from
|
||||
// newlib's mallinfo().uordblks (bytes actually outstanding) rather than
|
||||
// sbrk(0) - __HeapBase: sbrk only ever grows, so a high-water mark there
|
||||
// counts freed-and-recycled blocks as permanently "used" and free heap looks
|
||||
// far lower than it really is.
|
||||
extern unsigned char __HeapBase[];
|
||||
extern unsigned char __HeapLimit[];
|
||||
|
||||
void DeviceDiag::getHeapStats(uint32_t& free_bytes, uint32_t& total_bytes) {
|
||||
total_bytes = (uint32_t)(__HeapLimit - __HeapBase);
|
||||
uint32_t used = (uint32_t)((unsigned char*)_sbrk(0) - __HeapBase);
|
||||
uint32_t used = (uint32_t)mallinfo().uordblks;
|
||||
free_bytes = (used < total_bytes) ? (total_bytes - used) : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ public:
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
// From RadioLib's SX1262::setFrequency(): RADIOLIB_CHECK_RANGE(freq, 150.0f, 960.0f, ...).
|
||||
void getFreqBounds(float& min_mhz, float& max_mhz) const override { min_mhz = 150.0f; max_mhz = 960.0f; }
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSX1262 *)_radio)->isReceiving();
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ public:
|
||||
}
|
||||
|
||||
virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0;
|
||||
// RadioLib's own setFrequency() silently rejects values outside the chip's
|
||||
// validated range and leaves the radio retuned to its previous frequency —
|
||||
// setParams() above doesn't check that return code, so the UI clamps to this
|
||||
// instead of letting NodePrefs drift out of sync with the actual radio.
|
||||
// Default is the generic sanity bound the app's CMD_SET_RADIO_PARAMS already
|
||||
// uses; chips with a narrower RadioLib-validated range override it.
|
||||
virtual void getFreqBounds(float& min_mhz, float& max_mhz) const { min_mhz = 150.0f; max_mhz = 2500.0f; }
|
||||
uint32_t getRngSeed();
|
||||
void setTxPower(int8_t dbm);
|
||||
int8_t getTxPower() const { return _tx_dbm; } // actual current power (reflects APC)
|
||||
|
||||
Reference in New Issue
Block a user