update remote repeater firmware files to enable loop detection and regions

This commit is contained in:
pelgraine
2026-04-19 22:04:31 +10:00
parent ca5283af4f
commit 4ec5d17402
8 changed files with 214 additions and 32 deletions
+86
View File
@@ -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 <name>" — 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]) : &region_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
}
+1 -1
View File
@@ -1,10 +1,10 @@
#ifdef MECK_WIFI_REMOTE
#include "target.h"
#include "WiFiMQTT.h"
#include <esp_mac.h>
#include <Update.h>
#include <HTTPClient.h>
#include "target.h"
WiFiMQTT wifiMQTT;
+7 -1
View File
@@ -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();
}
+6
View File
@@ -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 {
+95 -24
View File
@@ -2,8 +2,48 @@
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
// 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 = &regions[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 = &regions[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
}
}
+11 -4
View File
@@ -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 &regions[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;
};
+2
View File
@@ -101,6 +101,7 @@ void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<uint8_t>(_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) {
+6 -2
View File
@@ -19,8 +19,12 @@
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
// 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)