mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-04 15:52:42 +02:00
update remote repeater firmware files to enable loop detection and regions
This commit is contained in:
@@ -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]) : ®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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = ®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
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -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;
|
||||
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user