From 4ec5d1740241f3daf201247f0c3a7c3b793200ff Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:04:31 +1000 Subject: [PATCH] update remote repeater firmware files to enable loop detection and regions --- examples/simple_repeater/MyMesh.cpp | 86 +++++++++++++++++++ examples/simple_repeater/wifimqtt.cpp | 2 +- src/helpers/CommonCLI.cpp | 8 +- src/helpers/CommonCLI.h | 6 ++ src/helpers/RegionMap.cpp | 119 ++++++++++++++++++++------ src/helpers/RegionMap.h | 15 +++- src/helpers/ui/GxEPDDisplay.cpp | 2 + src/helpers/ui/GxEPDDisplay.h | 8 +- 8 files changed, 214 insertions(+), 32 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 57a292eb..ac20a5b8 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -501,6 +501,51 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { } else { recv_pkt_region = NULL; } + + // --- Loop detection (MeshCore v1.14+) --- + // Walk the packet's path and count how many times our own hash appears. + // If it exceeds the threshold for the configured detection level, drop + // the packet to break routing loops caused by misbehaving nodes. + if (_prefs.loop_detect != LOOP_DETECT_OFF) { + uint8_t hops = pkt->path_len & 0x3F; + uint8_t bph = (pkt->path_len >> 6) + 1; // bytes per hop (1, 2, or 3) + + if (hops > 0 && hops * bph <= MAX_PATH_SIZE) { + // Count self-hash appearances in the path + int selfCount = 0; + for (uint8_t i = 0; i < hops; i++) { + if (self_id.isHashMatch(&pkt->path[i * bph], bph)) { + selfCount++; + } + } + + // Threshold depends on detection level and path hash size + // 1-byte 2-byte 3-byte + // minimal: 4 2 1 + // moderate: 2 1 1 + // strict: 1 1 1 + int threshold; + switch (_prefs.loop_detect) { + case LOOP_DETECT_MINIMAL: + threshold = (bph == 1) ? 4 : (bph == 2) ? 2 : 1; + break; + case LOOP_DETECT_MODERATE: + threshold = (bph == 1) ? 2 : 1; + break; + case LOOP_DETECT_STRICT: + default: + threshold = 1; + break; + } + + if (selfCount >= threshold) { + MESH_DEBUG_PRINTLN("Loop detected: self-hash appears %d times (threshold %d, bph=%d, mode=%d) — dropping", + selfCount, threshold, (int)bph, (int)_prefs.loop_detect); + return true; // Drop the packet + } + } + } + // do normal processing return false; } @@ -802,6 +847,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier _prefs.path_hash_mode = 0; // 1-byte path hashes (legacy default) + _prefs.loop_detect = LOOP_DETECT_OFF; // no loop detection by default } void MyMesh::begin(FILESYSTEM *fs) { @@ -1120,6 +1166,26 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (n == 2 && strcmp(parts[1], "home") == 0) { auto home = region_map.getHomeRegion(); sprintf(reply, " home is %s", home ? home->name : "*"); + } else if (n >= 3 && strcmp(parts[1], "default") == 0) { + // "region default " — set default scope + auto def = region_map.findByNamePrefix(parts[2]); + if (def) { + region_map.setDefaultRegion(def); + sprintf(reply, " default is now %s", def->name); + } else { + // empty or unrecognised name → clear default scope + region_map.setDefaultRegion(NULL); + strcpy(reply, " default cleared"); + } + } else if (n == 2 && strcmp(parts[1], "default") == 0) { + auto def = region_map.getDefaultRegion(); + sprintf(reply, " default is %s", def ? def->name : "(none)"); + } else if (n >= 3 && strcmp(parts[1], "list") == 0 && sender_timestamp == 0) { + // "region list allowed" / "region list denied" — serial only + bool denied = (strcmp(parts[2], "denied") == 0); + char buf[256]; + region_map.exportNamesTo(buf, sizeof(buf), REGION_DENY_FLOOD, denied); + Serial.printf("Regions (%s): %s\n", parts[2], buf); } else if (n >= 3 && strcmp(parts[1], "put") == 0) { auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); if (parent == NULL) { @@ -1157,6 +1223,26 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } else if (strcmp(command, "get path.hash.mode") == 0) { sprintf(reply, "> %d (%d-byte path hashes)", _prefs.path_hash_mode, _prefs.path_hash_mode + 1); + } else if (memcmp(command, "set loop.detect ", 16) == 0) { + const char* val = &command[16]; + if (strcmp(val, "off") == 0) { + _prefs.loop_detect = LOOP_DETECT_OFF; + } else if (strcmp(val, "minimal") == 0) { + _prefs.loop_detect = LOOP_DETECT_MINIMAL; + } else if (strcmp(val, "moderate") == 0) { + _prefs.loop_detect = LOOP_DETECT_MODERATE; + } else if (strcmp(val, "strict") == 0) { + _prefs.loop_detect = LOOP_DETECT_STRICT; + } else { + strcpy(reply, "ERR: use off, minimal, moderate, or strict"); + return; + } + savePrefs(); + sprintf(reply, "OK - loop.detect = %s", val); + } else if (strcmp(command, "get loop.detect") == 0) { + const char* labels[] = { "off", "minimal", "moderate", "strict" }; + uint8_t mode = _prefs.loop_detect <= 3 ? _prefs.loop_detect : 0; + sprintf(reply, "> %s", labels[mode]); } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_repeater/wifimqtt.cpp b/examples/simple_repeater/wifimqtt.cpp index 4b02386c..71dd2beb 100644 --- a/examples/simple_repeater/wifimqtt.cpp +++ b/examples/simple_repeater/wifimqtt.cpp @@ -1,10 +1,10 @@ #ifdef MECK_WIFI_REMOTE +#include "target.h" #include "WiFiMQTT.h" #include #include #include -#include "target.h" WiFiMQTT wifiMQTT; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9373589a..a4456dee 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -83,6 +83,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290 // 291 + if (file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)) != sizeof(_prefs->loop_detect)) { + _prefs->loop_detect = LOOP_DETECT_OFF; // default for older prefs files + } + // 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -109,6 +113,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); + _prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); file.close(); } @@ -168,7 +173,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290 - // 291 + file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 291 + // 292 file.close(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 5acf51db..46bb1e12 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -13,6 +13,11 @@ #define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_PREFS 2 +#define LOOP_DETECT_OFF 0 +#define LOOP_DETECT_MINIMAL 1 +#define LOOP_DETECT_MODERATE 2 +#define LOOP_DETECT_STRICT 3 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -54,6 +59,7 @@ struct NodePrefs { // persisted to file char owner_info[120]; // Multi-byte path hash support (added for Meck remote repeater) uint8_t path_hash_mode; // 0=1-byte (legacy), 1=2-byte, 2=3-byte path hashes + uint8_t loop_detect; // 0=off, 1=minimal, 2=moderate, 3=strict (MeshCore v1.14+) }; class CommonCLICallbacks { diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 35692762..0c6d43c1 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -2,8 +2,48 @@ #include #include +// helper class for region map exporter, we emulate Stream with a safe buffer writer. + +class BufStream : public Stream { +public: + BufStream(char *buf, size_t max_len) + : _buf(buf), _max_len(max_len), _pos(0) { + if (_max_len > 0) _buf[0] = 0; + } + + size_t write(uint8_t c) override { + if (_pos + 1 >= _max_len) return 0; + _buf[_pos++] = c; + _buf[_pos] = 0; + return 1; + } + + size_t write(const uint8_t *buffer, size_t size) override { + size_t written = 0; + while (written < size) { + if (!write(buffer[written])) break; + written++; + } + return written; + } + + int available() override { return 0; } + int read() override { return -1; } + int peek() override { return -1; } + void flush() override {} + + size_t length() const { return _pos; } + +private: + char *_buf; + size_t _max_len; + size_t _pos; +}; + + RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { - next_id = 1; num_regions = 0; home_id = 0; + next_id = 1; num_regions = 0; + default_id = home_id = 0; wildcard.id = wildcard.parent = 0; wildcard.flags = 0; // default behaviour, allow flood and direct strcpy(wildcard.name, "*"); @@ -40,9 +80,11 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { if (file) { uint8_t pad[128]; - num_regions = 0; next_id = 1; home_id = 0; + num_regions = 0; next_id = 1; + default_id = home_id = 0; - bool success = file.read(pad, 5) == 5; // reserved header + bool success = file.read(pad, 3) == 3; // reserved header + success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id); success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id); success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id); @@ -78,7 +120,8 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) { uint8_t pad[128]; memset(pad, 0, sizeof(pad)); - bool success = file.write(pad, 5) == 5; // reserved header + bool success = file.write(pad, 3) == 3; // reserved header + success = success && file.write((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id); success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id); success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id); @@ -125,24 +168,29 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t return region; } +int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) { + int num; + if (src.name[0] == '$') { // private region + num = _store->loadKeysFor(src.id, dest, max_num); + } else if (src.name[0] == '#') { // auto hashtag region + _store->getAutoKeyFor(src.id, src.name, dest[0]); + num = 1; + } else { // new: implicit auto hashtag region + char tmp[sizeof(src.name)+1]; + tmp[0] = '#'; + strcpy(&tmp[1], src.name); + _store->getAutoKeyFor(src.id, tmp, dest[0]); + num = 1; + } + return num; +} + RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) TransportKey keys[4]; - int num; - if (region->name[0] == '$') { // private region - num = _store->loadKeysFor(region->id, keys, 4); - } else if (region->name[0] == '#') { // auto hashtag region - _store->getAutoKeyFor(region->id, region->name, keys[0]); - num = 1; - } else { // new: implicit auto hashtag region - char tmp[sizeof(region->name)]; - tmp[0] = '#'; - strcpy(&tmp[1], region->name); - _store->getAutoKeyFor(region->id, tmp, keys[0]); - num = 1; - } + int num = getTransportKeysFor(*region, keys, 4); for (int j = 0; j < num; j++) { uint16_t code = keys[j].calcTransportCode(packet); if (packet->transport_codes[0] == code) { // a match!! @@ -198,6 +246,14 @@ void RegionMap::setHomeRegion(const RegionEntry* home) { home_id = home ? home->id : 0; } +RegionEntry* RegionMap::getDefaultRegion() { + return default_id == 0 ? NULL : findById(default_id); +} + +void RegionMap::setDefaultRegion(const RegionEntry* def) { + default_id = def ? def->id : 0; +} + bool RegionMap::removeRegion(const RegionEntry& region) { if (region.id == 0) return false; // failed (cannot remove the wildcard Region) @@ -249,27 +305,42 @@ void RegionMap::exportTo(Stream& out) const { printChildRegions(0, &wildcard, out); // recursive } -int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask) { +size_t RegionMap::exportTo(char *dest, size_t max_len) const { + if (!dest || max_len == 0) return 0; + + BufStream bs(dest, max_len); + exportTo(bs); // reuse existing logic + return bs.length(); +} + +int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) { char *dp = dest; - if ((wildcard.flags & mask) == 0) { + + // Check wildcard region + bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask); + if (wildcard_matches) { *dp++ = '*'; *dp++ = ','; } for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; - if ((region->flags & mask) == 0) { // region allowed? (per 'mask' param) - const char* name = skip_hash(region->name); - int len = strlen(name); + + // Check if region matches the filter criteria + bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask); + + if (region_matches) { + int len = strlen(skip_hash(region->name)); if ((dp - dest) + len + 2 < max_len) { // only append if name will fit - memcpy(dp, name, len); + memcpy(dp, skip_hash(region->name), len); dp += len; *dp++ = ','; } } } + if (dp > dest) { dp--; } // don't include trailing comma *dp = 0; // set null terminator return dp - dest; // return length -} +} \ No newline at end of file diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 01174d09..91f04d70 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -16,11 +16,13 @@ struct RegionEntry { uint16_t parent; uint8_t flags; char name[31]; + + bool isWildcard() const { return id == 0; } }; class RegionMap { TransportKeyStore* _store; - uint16_t next_id, home_id; + uint16_t next_id, home_id, default_id; uint16_t num_regions; RegionEntry regions[MAX_REGION_ENTRIES]; RegionEntry wildcard; @@ -43,13 +45,18 @@ public: RegionEntry* findById(uint16_t id); RegionEntry* getHomeRegion(); // NOTE: can be NULL void setHomeRegion(const RegionEntry* home); + RegionEntry* getDefaultRegion(); // NOTE: can be NULL + void setDefaultRegion(const RegionEntry* def); bool removeRegion(const RegionEntry& region); bool clear(); void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; } int getCount() const { return num_regions; } const RegionEntry* getByIdx(int i) const { return ®ions[i]; } const RegionEntry* getRoot() const { return &wildcard; } - int exportNamesTo(char *dest, int max_len, uint8_t mask); + int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false); + int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num); - void exportTo(Stream& out) const; -}; + void exportTo(Stream& out) const; + size_t exportTo(char *dest, size_t max_len) const; + +}; \ No newline at end of file diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 4c504c64..be62ee68 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -101,6 +101,7 @@ void GxEPDDisplay::setTextSize(int sz) { display_crc.update(_fontStyle); // Check for custom font style first (Noto Sans, Montserrat) +#ifdef HAS_MECK_FONTS const GFXfont* customFont = meckGetFont(_fontStyle, sz); if (customFont) { display.setFont(customFont); @@ -108,6 +109,7 @@ void GxEPDDisplay::setTextSize(int sz) { display.setTextSize(sz == 5 ? 2 : 1); return; } +#endif // Classic style (or fallback) — original FreeSans fonts switch(sz) { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 7f127748..451aa521 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -19,8 +19,12 @@ #include #include -// Meck custom font styles (Noto Sans, Montserrat) -#include "MeckFonts.h" +// Meck custom font styles (Noto Sans, Montserrat) — only available in +// companion radio builds which have -I examples/companion_radio/ui-new +#if __has_include("MeckFonts.h") + #include "MeckFonts.h" + #define HAS_MECK_FONTS 1 +#endif // Inline CRC32 for frame change detection (replaces bakercp/CRC32 // to avoid naming collision with PNGdec's bundled CRC32.h)