From 03fc94901477b4c7fc15fa6757d1a1940d7eaa34 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 3 Nov 2025 14:23:32 +1100 Subject: [PATCH 01/16] * setting up framework for Regions, TransportKeys, etc --- examples/simple_repeater/MyMesh.cpp | 22 +++++++++++++-- examples/simple_repeater/MyMesh.h | 5 ++++ src/helpers/RegionMap.cpp | 35 +++++++++++++++++++++++ src/helpers/RegionMap.h | 39 +++++++++++++++++++++++++ src/helpers/TransportKeyStore.cpp | 44 +++++++++++++++++++++++++++++ src/helpers/TransportKeyStore.h | 23 +++++++++++++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 src/helpers/RegionMap.cpp create mode 100644 src/helpers/RegionMap.h create mode 100644 src/helpers/TransportKeyStore.cpp create mode 100644 src/helpers/TransportKeyStore.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 1afad18b..21f5a76b 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -404,6 +404,23 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } +mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) { + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + auto region = region_map.findMatch(pkt, REGION_ALLOW_FLOOD); + if (region == NULL) { + MESH_DEBUG_PRINTLN("onRecvPacket: unknown transport code for FLOOD packet"); + return ACTION_RELEASE; + } + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if ((region_map.getWildcard().flags & REGION_ALLOW_FLOOD) == 0) { + MESH_DEBUG_PRINTLN("onRecvPacket: wildcard FLOOD packet not allowed"); + return ACTION_RELEASE; + } + } + // otherwise do normal processing + return mesh::Mesh::onRecvPacket(pkt); +} + void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, uint8_t *data, size_t len) { if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin @@ -593,7 +610,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) + _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store) #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) #endif @@ -652,8 +669,9 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); - acl.load(_fs); + // TODO: key_store.begin(); + region_map.load(_fs); #if defined(WITH_BRIDGE) if (_prefs.bridge_enabled) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 694e8ff9..7e34ef5f 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -32,6 +32,7 @@ #include #include #include +#include #ifdef WITH_BRIDGE extern AbstractBridge* bridge; @@ -87,6 +88,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; ClientACL acl; + TransportKeyStore key_store; + RegionMap region_map; unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; @@ -144,6 +147,8 @@ protected: } #endif + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp new file mode 100644 index 00000000..4c270ff8 --- /dev/null +++ b/src/helpers/RegionMap.cpp @@ -0,0 +1,35 @@ +#include "RegionMap.h" +#include + +void RegionMap::load(FILESYSTEM* _fs) { + // TODO +} +void RegionMap::save(FILESYSTEM* _fs) { + // TODO +} + +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) { // does region allow this? (per 'mask' param) + TransportKey keys[4]; + int num = _store->loadKeysFor(region->name, region->id, keys, 4); + for (int j = 0; j < num; j++) { + uint16_t code = keys[j].calcTransportCode(packet); + if (packet->transport_codes[0] == code) { // a match!! + return region; + } + } + } + } + return NULL; // no matches +} + +const RegionEntry* RegionMap::findName(const char* name) const { + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (strcmp(name, region->name) == 0) return region; + } + return NULL; // not found +} + diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h new file mode 100644 index 00000000..4620a745 --- /dev/null +++ b/src/helpers/RegionMap.h @@ -0,0 +1,39 @@ +#pragma once + +#include // needed for PlatformIO +#include +#include "TransportKeyStore.h" + +#ifndef MAX_REGION_ENTRIES + #define MAX_REGION_ENTRIES 32 +#endif + +#define REGION_ALLOW_FLOOD 0x01 + +struct RegionEntry { + uint16_t id; + uint16_t parent; + uint8_t flags; + char name[31]; +}; + +class RegionMap { + TransportKeyStore* _store; + uint16_t next_id; + uint16_t num_regions; + RegionEntry regions[MAX_REGION_ENTRIES]; + RegionEntry wildcard; + +public: + RegionMap(TransportKeyStore& store) : _store(&store) { + next_id = 1; num_regions = 0; + wildcard.id = wildcard.parent = 0; + wildcard.flags = REGION_ALLOW_FLOOD; // default behaviour, allow flood + } + void load(FILESYSTEM* _fs); + void save(FILESYSTEM* _fs); + + RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask); + const RegionEntry& getWildcard() const { return wildcard; } + const RegionEntry* findName(const char* name) const; +}; diff --git a/src/helpers/TransportKeyStore.cpp b/src/helpers/TransportKeyStore.cpp new file mode 100644 index 00000000..973ad703 --- /dev/null +++ b/src/helpers/TransportKeyStore.cpp @@ -0,0 +1,44 @@ +#include "TransportKeyStore.h" +#include + +uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const { + uint16_t code; + SHA256 sha; + sha.resetHMAC(key, sizeof(key)); + uint8_t type = packet->getPayloadType(); + sha.update(&type, 1); + sha.update(packet->payload, packet->payload_len); + sha.finalizeHMAC(key, sizeof(key), &code, 2); + return code; +} + +int TransportKeyStore::loadKeysFor(const char* name, uint16_t id, TransportKey keys[], int max_num) { + int n = 0; + for (int i = 0; i < num_cache && n < max_num; i++) { // first, check cache + if (cache_ids[i] == id) { + keys[n++] = cache_keys[i]; + } + } + if (n > 0) return n; // cache hit! + + if (*name == '#') { // is a publicly-known hashtag region + SHA256 sha; + sha.update(name, strlen(name)); + sha.finalize(&keys[0], sizeof(keys[0].key)); + n = 1; + } else { + // TODO: retrieve from difficult-to-copy keystore + } + + // store in cache (if room) + for (int i = 0; i < n; i++) { + if (num_cache < MAX_TKS_ENTRIES) { + cache_ids[num_cache] = id; + cache_keys[num_cache] = keys[i]; + num_cache++; + } else { + // TODO: evict oldest cache entry + } + } + return n; +} diff --git a/src/helpers/TransportKeyStore.h b/src/helpers/TransportKeyStore.h new file mode 100644 index 00000000..f25ed53f --- /dev/null +++ b/src/helpers/TransportKeyStore.h @@ -0,0 +1,23 @@ +#pragma once + +#include // needed for PlatformIO +#include +#include + +struct TransportKey { + uint8_t key[16]; + + uint16_t calcTransportCode(const mesh::Packet* packet) const; +}; + +#define MAX_TKS_ENTRIES 16 + +class TransportKeyStore { + uint16_t cache_ids[MAX_TKS_ENTRIES]; + TransportKey cache_keys[MAX_TKS_ENTRIES]; + int num_cache; + +public: + TransportKeyStore() { num_cache = 0; } + int loadKeysFor(const char* name, uint16_t id, TransportKey keys[], int max_num); +}; From f797744f7c4148e1f77888dbb121451c6745f0c0 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 3 Nov 2025 18:14:44 +1100 Subject: [PATCH 02/16] * misc RegionMap and key store methods --- src/helpers/RegionMap.cpp | 67 ++++++++++++++++++++++++++++-- src/helpers/RegionMap.h | 8 +++- src/helpers/TransportKeyStore.cpp | 68 +++++++++++++++++++++++-------- src/helpers/TransportKeyStore.h | 9 +++- 4 files changed, 130 insertions(+), 22 deletions(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 4c270ff8..db9ea2d5 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -1,4 +1,5 @@ #include "RegionMap.h" +#include #include void RegionMap::load(FILESYSTEM* _fs) { @@ -8,12 +9,36 @@ void RegionMap::save(FILESYSTEM* _fs) { // TODO } +RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id) { + auto region = findByName(name); + if (region) { + if (region->id == parent_id) return NULL; // ERROR: invalid parent! + + region->parent = parent_id; // re-parent / move this region in the hierarchy + } else { + if (num_regions >= MAX_REGION_ENTRIES) return NULL; // full! + + region = ®ions[num_regions++]; // alloc new RegionEntry + region->flags = 0; + region->id = next_id++; + StrHelper::strncpy(region->name, name, sizeof(region->name)); + region->parent = parent_id; + } + return region; +} + 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) { // does region allow this? (per 'mask' param) + if ((region->flags & mask) == mask) { // does region allow this? (per 'mask' param) TransportKey keys[4]; - int num = _store->loadKeysFor(region->name, region->id, keys, 4); + int num; + if (region->name[0] == '#') { // auto hashtag region + _store->getAutoKeyFor(region->id, region->name, keys[0]); + num = 1; + } else { + num = _store->loadKeysFor(region->id, keys, 4); + } for (int j = 0; j < num; j++) { uint16_t code = keys[j].calcTransportCode(packet); if (packet->transport_codes[0] == code) { // a match!! @@ -25,7 +50,7 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { return NULL; // no matches } -const RegionEntry* RegionMap::findName(const char* name) const { +RegionEntry* RegionMap::findByName(const char* name) { for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if (strcmp(name, region->name) == 0) return region; @@ -33,3 +58,39 @@ const RegionEntry* RegionMap::findName(const char* name) const { return NULL; // not found } +RegionEntry* RegionMap::findById(uint16_t id) { + if (id == 0) return &wildcard; // special root Region + + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (region->id == id) return region; + } + return NULL; // not found +} + +bool RegionMap::removeRegion(const RegionEntry& region) { + if (region.id == 0) return false; // failed (cannot remove the wildcard Region) + + int i; // first check region has no child regions + for (i = 0; i < num_regions; i++) { + if (regions[i].parent == region.id) return false; // failed (must remove child Regions first) + } + + i = 0; + while (i < num_regions) { + if (region.id == regions[i].id) break; + i++; + } + if (i >= num_regions) return false; // failed (not found) + + num_regions--; // remove from regions array + while (i + 1 < num_regions) { + regions[i] = regions[i + 1]; + } + return true; // success +} + +bool RegionMap::clear() { + num_regions = 0; + return true; // success +} diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 4620a745..69c5220b 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -33,7 +33,11 @@ public: void load(FILESYSTEM* _fs); void save(FILESYSTEM* _fs); + RegionEntry* putRegion(const char* name, uint16_t parent_id); RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask); - const RegionEntry& getWildcard() const { return wildcard; } - const RegionEntry* findName(const char* name) const; + RegionEntry& getWildcard() { return wildcard; } + RegionEntry* findByName(const char* name); + RegionEntry* findById(uint16_t id); + bool removeRegion(const RegionEntry& region); + bool clear(); }; diff --git a/src/helpers/TransportKeyStore.cpp b/src/helpers/TransportKeyStore.cpp index 973ad703..4f689a12 100644 --- a/src/helpers/TransportKeyStore.cpp +++ b/src/helpers/TransportKeyStore.cpp @@ -12,7 +12,32 @@ uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const { return code; } -int TransportKeyStore::loadKeysFor(const char* name, uint16_t id, TransportKey keys[], int max_num) { +void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) { + if (num_cache < MAX_TKS_ENTRIES) { + cache_ids[num_cache] = id; + cache_keys[num_cache] = key; + num_cache++; + } else { + // TODO: evict oldest cache entry + } +} + +void TransportKeyStore::getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest) { + for (int i = 0; i < num_cache; i++) { // first, check cache + if (cache_ids[i] == id) { // cache hit! + dest = cache_keys[i]; + return; + } + } + // calc key for publicly-known hashtag region name + SHA256 sha; + sha.update(name, strlen(name)); + sha.finalize(&dest.key, sizeof(dest.key)); + + putCache(id, dest); +} + +int TransportKeyStore::loadKeysFor(uint16_t id, TransportKey keys[], int max_num) { int n = 0; for (int i = 0; i < num_cache && n < max_num; i++) { // first, check cache if (cache_ids[i] == id) { @@ -21,24 +46,35 @@ int TransportKeyStore::loadKeysFor(const char* name, uint16_t id, TransportKey k } if (n > 0) return n; // cache hit! - if (*name == '#') { // is a publicly-known hashtag region - SHA256 sha; - sha.update(name, strlen(name)); - sha.finalize(&keys[0], sizeof(keys[0].key)); - n = 1; - } else { - // TODO: retrieve from difficult-to-copy keystore - } + // TODO: retrieve from difficult-to-copy keystore // store in cache (if room) for (int i = 0; i < n; i++) { - if (num_cache < MAX_TKS_ENTRIES) { - cache_ids[num_cache] = id; - cache_keys[num_cache] = keys[i]; - num_cache++; - } else { - // TODO: evict oldest cache entry - } + putCache(id, keys[i]); } return n; } + +bool TransportKeyStore::saveKeysFor(uint16_t id, const TransportKey keys[], int num) { + invalidateCache(); + + // TODO: update hardware keystore + + return false; // failed +} + +bool TransportKeyStore::removeKeys(uint16_t id) { + invalidateCache(); + + // TODO: remove from hardware keystore + + return false; // failed +} + +bool TransportKeyStore::clear() { + invalidateCache(); + + // TODO: clear hardware keystore + + return false; // failed +} diff --git a/src/helpers/TransportKeyStore.h b/src/helpers/TransportKeyStore.h index f25ed53f..fc9d2532 100644 --- a/src/helpers/TransportKeyStore.h +++ b/src/helpers/TransportKeyStore.h @@ -17,7 +17,14 @@ class TransportKeyStore { TransportKey cache_keys[MAX_TKS_ENTRIES]; int num_cache; + void putCache(uint16_t id, const TransportKey& key); + void invalidateCache() { num_cache = 0; } + public: TransportKeyStore() { num_cache = 0; } - int loadKeysFor(const char* name, uint16_t id, TransportKey keys[], int max_num); + void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest); + int loadKeysFor(uint16_t id, TransportKey keys[], int max_num); + bool saveKeysFor(uint16_t id, const TransportKey keys[], int num); + bool removeKeys(uint16_t id); + bool clear(); }; From ecd30f4d364d190beac467224063d56ed681c5f2 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 3 Nov 2025 22:53:14 +1100 Subject: [PATCH 03/16] * new CLI commands: region, region load, region save, region get, region allow --- examples/simple_repeater/MyMesh.cpp | 83 +++++++++++++++++- examples/simple_repeater/MyMesh.h | 4 +- examples/simple_repeater/main.cpp | 4 +- src/helpers/RegionMap.cpp | 128 ++++++++++++++++++++++++++-- src/helpers/RegionMap.h | 22 +++-- 5 files changed, 219 insertions(+), 22 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 21f5a76b..2cd81ef6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -610,7 +610,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store) + _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store) #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) #endif @@ -624,6 +624,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; + region_load_active = false; #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); @@ -846,9 +847,46 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } +static bool is_name_char(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= 'z') || c == '-' || c == '.' || c == '_' || c == '#'; +} + void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { - while (*command == ' ') - command++; // skip leading spaces + if (region_load_active) { + if (*command == 0) { // empty line, signal to terminate 'load' operation + region_map = temp_map; // copy over the temp instance as new current map + region_load_active = false; + + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; + + char *ep = np; + while (is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name + + while (*ep && *ep != 'F') ep++; // look for (optional flags) + + if (indent > 0 && indent < 8) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? REGION_ALLOW_FLOOD : 0); // carry-over flags from curr + + load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's + } + } + } + reply[0] = 0; + } + return; + } + + while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) memcpy(reply, command, 3); // reflect the prefix back @@ -890,6 +928,45 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (memcmp(command, "region", 6) == 0) { + reply[0] = 0; + + const char* parts[4]; + int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); + if (n == 1 && sender_timestamp == 0) { + region_map.exportTo(Serial); + } else if (n >= 2 && strcmp(parts[1], "load") == 0) { + temp_map.resetFrom(region_map); // rebuild regions in a temp instance + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; + } else if (n >= 2 && strcmp(parts[1], "save") == 0) { + bool success = region_map.save(_fs); + strcpy(reply, success ? "OK" : "Err - save failed"); + } else if (n >= 3 && strcmp(parts[1], "allow") == 0) { + auto region = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + if (region) { + strcpy(reply, "OK"); + if (strcmp(parts[2], "F") == 0) { + region->flags = REGION_ALLOW_FLOOD; + } else if (strcmp(parts[2], "0") == 0) { + region->flags = 0; + } else { + sprintf(reply, "Err - invalid flag: %s", parts[2]); + } + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "get") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + sprintf(reply, " %s %s", region->name, region->flags == REGION_ALLOW_FLOOD ? "F" : ""); + } else { + strcpy(reply, "Err - unknown region"); + } + } else { + strcpy(reply, "Err - ??"); + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 7e34ef5f..9ab93747 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -89,7 +89,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t reply_data[MAX_PACKET_PAYLOAD]; ClientACL acl; TransportKeyStore key_store; - RegionMap region_map; + RegionMap region_map, temp_map; + RegionEntry* load_stack[8]; + bool region_load_active; unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 5843df74..7387e77e 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -91,14 +91,16 @@ void loop() { if (c != '\n') { command[len++] = c; command[len] = 0; + Serial.print(c); } - Serial.print(c); + if (c == '\r') break; } if (len == sizeof(command)-1) { // command buffer full command[sizeof(command)-1] = '\r'; } if (len > 0 && command[len - 1] == '\r') { // received complete line + Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index db9ea2d5..7b6456b4 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -2,25 +2,106 @@ #include #include -void RegionMap::load(FILESYSTEM* _fs) { - // TODO -} -void RegionMap::save(FILESYSTEM* _fs) { - // TODO +RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { + next_id = 1; num_regions = 0; + wildcard.id = wildcard.parent = 0; + wildcard.flags = REGION_ALLOW_FLOOD; // default behaviour, allow flood + strcpy(wildcard.name, "(*)"); } -RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id) { +static File openWrite(FILESYSTEM* _fs, const char* filename) { + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(filename); + return _fs->open(filename, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(filename, "w"); + #else + return _fs->open(filename, "w", true); + #endif +} + +bool RegionMap::load(FILESYSTEM* _fs) { + if (_fs->exists("/regions2")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/regions2", "r"); + #else + File file = _fs->open("/regions2"); + #endif + + if (file) { + uint8_t pad[128]; + + num_regions = 0; next_id = 1; + + bool success = file.read(pad, 7) == 7; // reserved header + 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); + + if (success) { + while (num_regions < MAX_REGION_ENTRIES) { + auto r = ®ions[num_regions]; + + success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); + success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && file.read(pad, sizeof(pad)) == sizeof(pad); + + if (!success) break; // EOF + + if (r->id >= next_id) { // make sure next_id is valid + next_id = r->id + 1; + } + num_regions++; + } + } + file.close(); + return true; + } + } + return false; // failed +} + +bool RegionMap::save(FILESYSTEM* _fs) { + File file = openWrite(_fs, "/regions2"); + if (file) { + uint8_t pad[128]; + memset(pad, 0, sizeof(pad)); + + bool success = file.write(pad, 7) == 7; // reserved header + 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); + + if (success) { + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + + success = file.write((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && file.write((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && file.write((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); + success = success && file.write((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && file.write(pad, sizeof(pad)) == sizeof(pad); + if (!success) break; // write failed + } + } + file.close(); + return true; + } + return false; // failed +} + +RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) { auto region = findByName(name); if (region) { if (region->id == parent_id) return NULL; // ERROR: invalid parent! region->parent = parent_id; // re-parent / move this region in the hierarchy } else { - if (num_regions >= MAX_REGION_ENTRIES) return NULL; // full! + if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full! region = ®ions[num_regions++]; // alloc new RegionEntry region->flags = 0; - region->id = next_id++; + region->id = id == 0 ? next_id++ : id; StrHelper::strncpy(region->name, name, sizeof(region->name)); region->parent = parent_id; } @@ -58,6 +139,14 @@ RegionEntry* RegionMap::findByName(const char* name) { return NULL; // not found } +RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (memcmp(prefix, region->name, strlen(prefix)) == 0) return region; + } + return NULL; // not found +} + RegionEntry* RegionMap::findById(uint16_t id) { if (id == 0) return &wildcard; // special root Region @@ -94,3 +183,26 @@ bool RegionMap::clear() { num_regions = 0; return true; // success } + +void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out) const { + for (int i = 0; i < indent; i++) { + out.print(' '); + } + + if (parent->flags & REGION_ALLOW_FLOOD) { + out.printf("%s F\n", parent->name); + } else { + out.printf("%s\n", parent->name); + } + + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + if (r->parent == parent->id) { + printChildRegions(indent + 1, r, out); + } + } +} + +void RegionMap::exportTo(Stream& out) const { + printChildRegions(0, &wildcard, out); // recursive +} diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 69c5220b..3858bfbd 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -24,20 +24,24 @@ class RegionMap { RegionEntry regions[MAX_REGION_ENTRIES]; RegionEntry wildcard; -public: - RegionMap(TransportKeyStore& store) : _store(&store) { - next_id = 1; num_regions = 0; - wildcard.id = wildcard.parent = 0; - wildcard.flags = REGION_ALLOW_FLOOD; // default behaviour, allow flood - } - void load(FILESYSTEM* _fs); - void save(FILESYSTEM* _fs); + void printChildRegions(int indent, const RegionEntry* parent, Stream& out) const; - RegionEntry* putRegion(const char* name, uint16_t parent_id); +public: + RegionMap(TransportKeyStore& store); + + bool load(FILESYSTEM* _fs); + bool save(FILESYSTEM* _fs); + + RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0); RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask); RegionEntry& getWildcard() { return wildcard; } RegionEntry* findByName(const char* name); + RegionEntry* findByNamePrefix(const char* prefix); RegionEntry* findById(uint16_t id); 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; } + + void exportTo(Stream& out) const; }; From d9ff3a4d02d25005eac27c40bd58b6c43f29c9ed Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 4 Nov 2025 01:21:56 +1100 Subject: [PATCH 04/16] * Mesh: new sendFlood() overload with transport codes. * BaseChatMesh: sendFloodScoped(), for overriding with some outbound 'scope' / TransportKey * companion: new 'send_scope' variable. --- examples/companion_radio/MyMesh.cpp | 24 ++++++++++++++++++++++ examples/companion_radio/MyMesh.h | 6 ++++++ src/Mesh.cpp | 25 +++++++++++++++++++++++ src/Mesh.h | 6 ++++++ src/helpers/BaseChatMesh.cpp | 31 ++++++++++++++++++----------- src/helpers/BaseChatMesh.h | 3 +++ src/helpers/TransportKeyStore.cpp | 7 +++++++ src/helpers/TransportKeyStore.h | 1 + 8 files changed, 91 insertions(+), 12 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 02f1a21d..b8ddbfa7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -378,6 +378,29 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe #endif } +void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { + // TODO: dynamic send_scope, depending on recipient and current 'home' Region + if (send_scope.isNull()) { + sendFlood(pkt, delay_millis); + } else { + uint16_t codes[2]; + codes[0] = send_scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis); + } +} +void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { + // TODO: have per-channel send_scope + if (send_scope.isNull()) { + sendFlood(pkt, delay_millis); + } else { + uint16_t codes[2]; + codes[0] = send_scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis); + } +} + void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) { markConnectionActive(from); // in case this is from a server, and we have a connection @@ -663,6 +686,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe sign_data = NULL; dirty_contacts_expiry = 0; memset(advert_paths, 0, sizeof(advert_paths)); + memset(send_scope.key, 0, sizeof(send_scope.key)); // defaults memset(&_prefs, 0, sizeof(_prefs)); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index e6400871..bfb8bb18 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -68,6 +68,7 @@ #endif #include +#include /* -------------------------------------------------------------------------------------- */ @@ -107,6 +108,9 @@ protected: int calcRxDelay(float score, uint32_t air_time) const override; uint8_t getExtraAckTransmitCount() const override; + void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; + void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; bool isAutoAddEnabled() const override; bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; @@ -191,6 +195,8 @@ private: uint32_t sign_data_len; unsigned long dirty_contacts_expiry; + TransportKey send_scope; + uint8_t cmd_frame[MAX_FRAME_SIZE + 1]; uint8_t out_frame[MAX_FRAME_SIZE + 1]; CayenneLPP telemetry; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 53dc74f5..1ee6ce5c 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -610,6 +610,31 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { sendPacket(packet, pri, delay_millis); } +void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) { + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); + return; + } + + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->path_len = 0; + + _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; // de-prioritie these + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis); +} + void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) { packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_DIRECT; diff --git a/src/Mesh.h b/src/Mesh.h index cbf1c9cf..e96043e8 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -186,6 +186,12 @@ public: */ void sendFlood(Packet* packet, uint32_t delay_millis=0); + /** + * \brief send a locally-generated Packet with flood routing + * \param transport_codes array of 2 codes to attach to packet + */ + void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); + /** * \brief send a locally-generated Packet with Direct routing */ diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 9b1eb1ce..b4072657 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,6 +9,13 @@ #define TXT_ACK_DELAY 200 #endif +void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { + sendFlood(pkt, delay_millis); +} +void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { + sendFlood(pkt, delay_millis); +} + mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) { uint8_t app_data[MAX_ADVERT_DATA_SIZE]; uint8_t app_data_len; @@ -34,7 +41,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { if (dest.out_path_len < 0) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY); + if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -175,7 +182,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY); + if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash); } @@ -186,7 +193,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0); - if (path) sendFlood(path); + if (path) sendFloodScoped(from, path); } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date @@ -202,7 +209,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY); + if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash); } @@ -218,14 +225,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); if (reply) { if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); + sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); } } } @@ -346,7 +353,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, int rc; if (recipient.out_path_len < 0) { - sendFlood(pkt); + sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; } else { @@ -372,7 +379,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; if (recipient.out_path_len < 0) { - sendFlood(pkt); + sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; } else { @@ -398,7 +405,7 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); if (pkt) { - sendFlood(pkt); + sendFloodScoped(channel, pkt); return true; } return false; @@ -460,7 +467,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { - sendFlood(pkt); + sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; } else { @@ -487,7 +494,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_ if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { - sendFlood(pkt); + sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; } else { @@ -514,7 +521,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { - sendFlood(pkt); + sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; } else { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 9392001e..76b0dd1c 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -107,6 +107,9 @@ protected: virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0; virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len); + virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0); + virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0); + // storage concepts, for sub-classes to override/implement virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; } diff --git a/src/helpers/TransportKeyStore.cpp b/src/helpers/TransportKeyStore.cpp index 4f689a12..8b7c891b 100644 --- a/src/helpers/TransportKeyStore.cpp +++ b/src/helpers/TransportKeyStore.cpp @@ -12,6 +12,13 @@ uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const { return code; } +bool TransportKey::isNull() const { + for (int i = 0; i < sizeof(key); i++) { + if (key[i]) return false; + } + return true; // key is all zeroes +} + void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) { if (num_cache < MAX_TKS_ENTRIES) { cache_ids[num_cache] = id; diff --git a/src/helpers/TransportKeyStore.h b/src/helpers/TransportKeyStore.h index fc9d2532..e3ba1524 100644 --- a/src/helpers/TransportKeyStore.h +++ b/src/helpers/TransportKeyStore.h @@ -8,6 +8,7 @@ struct TransportKey { uint8_t key[16]; uint16_t calcTransportCode(const mesh::Packet* packet) const; + bool isNull() const; }; #define MAX_TKS_ENTRIES 16 From 9ebeb477aa3313123323a967c8981a6ab96ea82f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Nov 2025 14:34:44 +1100 Subject: [PATCH 05/16] * RegionMap: inverted 'flags' to _deny_ bits * Mesh: new filterRecvFloodPacket() for overriding * repeater CLI: 'allow' -> 'allowf' or 'denyf' --- examples/companion_radio/MyMesh.cpp | 6 +++++ examples/companion_radio/MyMesh.h | 1 + examples/simple_repeater/MyMesh.cpp | 38 +++++++++++++++-------------- examples/simple_repeater/MyMesh.h | 2 +- src/Mesh.cpp | 2 ++ src/Mesh.h | 5 ++++ src/helpers/RegionMap.cpp | 12 ++++----- src/helpers/RegionMap.h | 3 ++- 8 files changed, 43 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b8ddbfa7..3c882d18 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -378,6 +378,12 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe #endif } +bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { + // REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses + // if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender + return false; +} + void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: dynamic send_scope, depending on recipient and current 'home' Region if (send_scope.isNull()) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index bfb8bb18..fb0fb479 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -107,6 +107,7 @@ protected: int getInterferenceThreshold() const override; int calcRxDelay(float score, uint32_t air_time) const override; uint8_t getExtraAckTransmitCount() const override; + bool filterRecvFloodPacket(mesh::Packet* packet) override; void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 2cd81ef6..dce0f18b 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -404,21 +404,21 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } -mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) { +bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { - auto region = region_map.findMatch(pkt, REGION_ALLOW_FLOOD); + auto region = region_map.findMatch(pkt, REGION_DENY_FLOOD); if (region == NULL) { MESH_DEBUG_PRINTLN("onRecvPacket: unknown transport code for FLOOD packet"); - return ACTION_RELEASE; + return true; } } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { - if ((region_map.getWildcard().flags & REGION_ALLOW_FLOOD) == 0) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { MESH_DEBUG_PRINTLN("onRecvPacket: wildcard FLOOD packet not allowed"); - return ACTION_RELEASE; + return true; } } // otherwise do normal processing - return mesh::Mesh::onRecvPacket(pkt); + return false; } void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, @@ -867,7 +867,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply while (is_name_char(*ep)) ep++; if (*ep) { *ep++ = 0; } // set null terminator for end of name - while (*ep && *ep != 'F') ep++; // look for (optional flags) + while (*ep && *ep != 'F') ep++; // look for (optional) flags if (indent > 0 && indent < 8) { auto parent = load_stack[indent - 1]; @@ -875,7 +875,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply auto old = region_map.findByName(np); auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) if (nw) { - nw->flags = old ? old->flags : (*ep == 'F' ? REGION_ALLOW_FLOOD : 0); // carry-over flags from curr + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's } @@ -943,24 +943,26 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (n >= 2 && strcmp(parts[1], "save") == 0) { bool success = region_map.save(_fs); strcpy(reply, success ? "OK" : "Err - save failed"); - } else if (n >= 3 && strcmp(parts[1], "allow") == 0) { - auto region = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + } else if (n >= 2 && strcmp(parts[1], "allowf") == 0) { + auto region = n >= 3 ? region_map.findByNamePrefix(parts[2]) : ®ion_map.getWildcard(); if (region) { + region->flags &= ~REGION_DENY_FLOOD; + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 2 && strcmp(parts[1], "denyf") == 0) { + auto region = n >= 3 ? region_map.findByNamePrefix(parts[2]) : ®ion_map.getWildcard(); + if (region) { + region->flags |= REGION_DENY_FLOOD; strcpy(reply, "OK"); - if (strcmp(parts[2], "F") == 0) { - region->flags = REGION_ALLOW_FLOOD; - } else if (strcmp(parts[2], "0") == 0) { - region->flags = 0; - } else { - sprintf(reply, "Err - invalid flag: %s", parts[2]); - } } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "get") == 0) { auto region = region_map.findByNamePrefix(parts[2]); if (region) { - sprintf(reply, " %s %s", region->name, region->flags == REGION_ALLOW_FLOOD ? "F" : ""); + sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); } else { strcpy(reply, "Err - unknown region"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 9ab93747..83331541 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -149,7 +149,7 @@ protected: } #endif - mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; + bool filterRecvFloodPacket(mesh::Packet* pkt) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 1ee6ce5c..71b8eaee 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -90,6 +90,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. } + if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; + DispatcherAction action = ACTION_RELEASE; switch (pkt->getPayloadType()) { diff --git a/src/Mesh.h b/src/Mesh.h index e96043e8..70fa80f3 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -43,6 +43,11 @@ protected: */ DispatcherAction routeRecvPacket(Packet* packet); + /** + * \returns true, if given packet should be NOT be processed. + */ + virtual bool filterRecvFloodPacket(Packet* packet) { return false; } + /** * \brief Check whether this packet should be forwarded (re-transmitted) or not. * Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 7b6456b4..074084ec 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -5,7 +5,7 @@ RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { next_id = 1; num_regions = 0; wildcard.id = wildcard.parent = 0; - wildcard.flags = REGION_ALLOW_FLOOD; // default behaviour, allow flood + wildcard.flags = 0; // default behaviour, allow flood and direct strcpy(wildcard.name, "(*)"); } @@ -100,7 +100,7 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full! region = ®ions[num_regions++]; // alloc new RegionEntry - region->flags = 0; + region->flags = REGION_DENY_FLOOD; // DENY by default region->id = id == 0 ? next_id++ : id; StrHelper::strncpy(region->name, name, sizeof(region->name)); region->parent = parent_id; @@ -111,7 +111,7 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t 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) == mask) { // does region allow this? (per 'mask' param) + if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) TransportKey keys[4]; int num; if (region->name[0] == '#') { // auto hashtag region @@ -189,10 +189,10 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out.print(' '); } - if (parent->flags & REGION_ALLOW_FLOOD) { - out.printf("%s F\n", parent->name); - } else { + if (parent->flags & REGION_DENY_FLOOD) { out.printf("%s\n", parent->name); + } else { + out.printf("%s F\n", parent->name); } for (int i = 0; i < num_regions; i++) { diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 3858bfbd..5ad9df29 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -8,7 +8,8 @@ #define MAX_REGION_ENTRIES 32 #endif -#define REGION_ALLOW_FLOOD 0x01 +#define REGION_DENY_FLOOD 0x01 +#define REGION_DENY_DIRECT 0x02 // reserved for future struct RegionEntry { uint16_t id; From 937865c8fd8d3e82cf5078bc77226d725c556344 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Nov 2025 14:56:18 +1100 Subject: [PATCH 06/16] * companion: new CMD_SET_FLOOD_SCOPE (54) --- examples/companion_radio/MyMesh.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3c882d18..904cd871 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -50,6 +50,7 @@ #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 +#define CMD_SET_FLOOD_SCOPE 54 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -1515,6 +1516,13 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_FILE_IO_ERROR); } + } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) { + if (len >= 2 + 16) { + memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey + } else { + memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null + } + writeOKFrame(); } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); From 3ef53e64a1fd0f7b654e949f3caf16d3a760c1ba Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Nov 2025 15:34:23 +1100 Subject: [PATCH 07/16] * is_name_char() bug fix --- examples/simple_repeater/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index dce0f18b..0bfb7c89 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -848,7 +848,7 @@ void MyMesh::clearStats() { } static bool is_name_char(char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= 'z') || c == '-' || c == '.' || c == '_' || c == '#'; + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#'; } void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { From 82b4c1e6b0771031d53c6c301567cb021577a9cd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 00:56:54 +1100 Subject: [PATCH 08/16] * new PAYLOAD_TYPE_CONTROL (11) * repeater: onControlDataRecv(), now responds to new CTL_TYPE_NODE_DISCOVER_REQ (zero hop only) * node prefs: new discovery_mod_timestamp (will be set to affect when node should respond to DISCOVERY_REQ's ) --- examples/simple_repeater/MyMesh.cpp | 32 +++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 24 ++++++++++++++++++++++ src/Mesh.h | 6 ++++++ src/Packet.h | 1 + src/helpers/CommonCLI.cpp | 6 ++++-- src/helpers/CommonCLI.h | 1 + 7 files changed, 69 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 0bfb7c89..74fca86c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -607,6 +607,38 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t return false; } +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + +void MyMesh::onControlDataRecv(mesh::Packet* packet) { + uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits + if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) { + // TODO: apply rate limiting to these! + int i = 1; + uint8_t filter = packet->payload[i++]; + uint32_t tag; + memcpy(&tag, &packet->payload[i], 4); i += 4; + uint32_t since; + if (packet->payload_len >= i+4) { // optional since field + memcpy(&since, &packet->payload[i], 4); i += 4; + } else { + since = 0; + } + + if ((filter & (1 << ADV_TYPE_REPEATER)) != 0 && _prefs.discovery_mod_timestamp >= since) { + uint8_t data[6 + PUB_KEY_SIZE]; + data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_REPEATER; // low 4-bits for node type + data[1] = packet->_snr; // let sender know the inbound SNR ( x 4) + memcpy(&data[2], &tag, 4); // include tag from request, for client to match to + memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE); + auto resp = createControlData(data, sizeof(data)); + if (resp) { + sendZeroHop(resp, getRetransmitDelay(resp)); // apply random delay, as multiple nodes can respond to this + } + } + } +} + MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 83331541..fd0b4d6a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -157,6 +157,7 @@ protected: void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + void onControlDataRecv(mesh::Packet* packet) override; public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 71b8eaee..f1271574 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -68,6 +68,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { + if (pkt->path_len == 0) { + onControlDataRecv(pkt); + } + // just zero-hop control packets allowed (for this subset of payloads) + return ACTION_RELEASE; + } + if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) { if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { @@ -589,6 +597,22 @@ Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) { return packet; } +Packet* Mesh::createControlData(const uint8_t* data, size_t len) { + if (len > sizeof(Packet::payload)) return NULL; // invalid arg + + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createControlData(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + memcpy(packet->payload, data, len); + packet->payload_len = len; + + return packet; +} + void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); diff --git a/src/Mesh.h b/src/Mesh.h index 70fa80f3..bc1ac54d 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -133,6 +133,11 @@ protected: */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } + /** + * \brief A control packet has been received. + */ + virtual void onControlDataRecv(Packet* packet) { } + /** * \brief A packet with PAYLOAD_TYPE_RAW_CUSTOM has been received. */ @@ -185,6 +190,7 @@ public: Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createRawData(const uint8_t* data, size_t len); Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); + Packet* createControlData(const uint8_t* data, size_t len); /** * \brief send a locally-generated Packet with flood routing diff --git a/src/Packet.h b/src/Packet.h index e52ab526..42d73f41 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -27,6 +27,7 @@ namespace mesh { #define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) #define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop #define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets +#define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet //... #define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 88327aa8..caed368b 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -69,7 +69,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 - // 162 + file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 + // 166 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -146,7 +147,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 - // 162 + file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 + // 166 file.close(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3cfca46c..a665e014 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -46,6 +46,7 @@ struct NodePrefs { // persisted to file uint8_t gps_enabled; uint32_t gps_interval; // in seconds uint8_t advert_loc_policy; + uint32_t discovery_mod_timestamp; }; class CommonCLICallbacks { From 7419ed71f7a57f7bf6f2ec157d5222fa1fa2283c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 12:27:25 +1100 Subject: [PATCH 09/16] * region filtering now applied in allowPacketForward() --- examples/simple_repeater/MyMesh.cpp | 22 ++++++++++++++-------- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.h | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 74fca86c..2fb01029 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -306,6 +306,10 @@ File MyMesh::openAppend(const char *fname) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + if (packet->isRouteFlood() && recv_pkt_region == NULL) { + MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); + return false; + } return true; } @@ -405,19 +409,19 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { + // just try to determine region for packet (apply later in allowPacketForward()) if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { - auto region = region_map.findMatch(pkt, REGION_DENY_FLOOD); - if (region == NULL) { - MESH_DEBUG_PRINTLN("onRecvPacket: unknown transport code for FLOOD packet"); - return true; - } + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { - MESH_DEBUG_PRINTLN("onRecvPacket: wildcard FLOOD packet not allowed"); - return true; + recv_pkt_region = NULL; + } else { + recv_pkt_region = ®ion_map.getWildcard(); } + } else { + recv_pkt_region = NULL; } - // otherwise do normal processing + // do normal processing return false; } @@ -973,6 +977,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply load_stack[0] = &temp_map.getWildcard(); region_load_active = true; } else if (n >= 2 && strcmp(parts[1], "save") == 0) { + _prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime(); // this node is now 'modified' (for discovery info) + savePrefs(); bool success = region_map.save(_fs); strcpy(reply, success ? "OK" : "Err - save failed"); } else if (n >= 2 && strcmp(parts[1], "allowf") == 0) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index fd0b4d6a..45001597 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -91,6 +91,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* load_stack[8]; + RegionEntry* recv_pkt_region; bool region_load_active; unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS diff --git a/src/Mesh.h b/src/Mesh.h index bc1ac54d..00f7ed00 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -44,6 +44,7 @@ protected: DispatcherAction routeRecvPacket(Packet* packet); /** + * \brief Called _before_ the packet is dispatched to the on..Recv() methods. * \returns true, if given packet should be NOT be processed. */ virtual bool filterRecvFloodPacket(Packet* packet) { return false; } From cf547da85735f86df5836e6803708845a7677ac8 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 17:28:45 +1100 Subject: [PATCH 10/16] * RegionMap: get/set Home Region * repeater: admin CLI, changed "allowf *", "denyf *", added "home" --- examples/simple_repeater/MyMesh.cpp | 21 ++++++++++++++----- src/helpers/RegionMap.cpp | 32 +++++++++++++++++++++-------- src/helpers/RegionMap.h | 4 +++- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 2fb01029..7489d611 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -637,7 +637,7 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE); auto resp = createControlData(data, sizeof(data)); if (resp) { - sendZeroHop(resp, getRetransmitDelay(resp)); // apply random delay, as multiple nodes can respond to this + sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this } } } @@ -981,16 +981,16 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply savePrefs(); bool success = region_map.save(_fs); strcpy(reply, success ? "OK" : "Err - save failed"); - } else if (n >= 2 && strcmp(parts[1], "allowf") == 0) { - auto region = n >= 3 ? region_map.findByNamePrefix(parts[2]) : ®ion_map.getWildcard(); + } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { + auto region = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); if (region) { region->flags &= ~REGION_DENY_FLOOD; strcpy(reply, "OK"); } else { strcpy(reply, "Err - unknown region"); } - } else if (n >= 2 && strcmp(parts[1], "denyf") == 0) { - auto region = n >= 3 ? region_map.findByNamePrefix(parts[2]) : ®ion_map.getWildcard(); + } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { + auto region = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); if (region) { region->flags |= REGION_DENY_FLOOD; strcpy(reply, "OK"); @@ -1004,6 +1004,17 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - unknown region"); } + } else if (n >= 3 && strcmp(parts[1], "home") == 0) { + auto home = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); + if (home) { + region_map.setHomeRegion(home); + sprintf(reply, " home is now %s", home->name); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n == 2 && strcmp(parts[1], "home") == 0) { + auto home = region_map.getHomeRegion(); + sprintf(reply, " home is %s", home ? home->name : "*"); } else { strcpy(reply, "Err - ??"); } diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 074084ec..c6221db0 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -3,10 +3,10 @@ #include RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { - next_id = 1; num_regions = 0; + next_id = 1; num_regions = 0; home_id = 0; wildcard.id = wildcard.parent = 0; wildcard.flags = 0; // default behaviour, allow flood and direct - strcpy(wildcard.name, "(*)"); + strcpy(wildcard.name, "*"); } static File openWrite(FILESYSTEM* _fs, const char* filename) { @@ -31,9 +31,10 @@ bool RegionMap::load(FILESYSTEM* _fs) { if (file) { uint8_t pad[128]; - num_regions = 0; next_id = 1; + num_regions = 0; next_id = 1; home_id = 0; - bool success = file.read(pad, 7) == 7; // reserved header + bool success = file.read(pad, 5) == 5; // reserved header + 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); @@ -68,7 +69,8 @@ bool RegionMap::save(FILESYSTEM* _fs) { uint8_t pad[128]; memset(pad, 0, sizeof(pad)); - bool success = file.write(pad, 7) == 7; // reserved header + bool success = file.write(pad, 5) == 5; // reserved header + 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); @@ -140,11 +142,15 @@ RegionEntry* RegionMap::findByName(const char* name) { } RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { + RegionEntry* partial = NULL; for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; - if (memcmp(prefix, region->name, strlen(prefix)) == 0) return region; + if (strcmp(prefix, region->name) == 0) return region; // is a complete match, preference this one + if (memcmp(prefix, region->name, strlen(prefix)) == 0) { + partial = region; + } } - return NULL; // not found + return partial; } RegionEntry* RegionMap::findById(uint16_t id) { @@ -157,6 +163,14 @@ RegionEntry* RegionMap::findById(uint16_t id) { return NULL; // not found } +RegionEntry* RegionMap::getHomeRegion() { + return findById(home_id); +} + +void RegionMap::setHomeRegion(const RegionEntry* home) { + home_id = home ? home->id : 0; +} + bool RegionMap::removeRegion(const RegionEntry& region) { if (region.id == 0) return false; // failed (cannot remove the wildcard Region) @@ -190,9 +204,9 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& } if (parent->flags & REGION_DENY_FLOOD) { - out.printf("%s\n", parent->name); + out.printf("%s%s\n", parent->name, parent->id == home_id ? "^" : ""); } else { - out.printf("%s F\n", parent->name); + out.printf("%s%s F\n", parent->name, parent->id == home_id ? "^" : ""); } for (int i = 0; i < num_regions; i++) { diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 5ad9df29..c3f897c5 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -20,7 +20,7 @@ struct RegionEntry { class RegionMap { TransportKeyStore* _store; - uint16_t next_id; + uint16_t next_id, home_id; uint16_t num_regions; RegionEntry regions[MAX_REGION_ENTRIES]; RegionEntry wildcard; @@ -39,6 +39,8 @@ public: RegionEntry* findByName(const char* name); RegionEntry* findByNamePrefix(const char* prefix); RegionEntry* findById(uint16_t id); + RegionEntry* getHomeRegion(); // NOTE: can be NULL + void setHomeRegion(const RegionEntry* home); bool removeRegion(const RegionEntry& region); bool clear(); void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; } From 09eab330a2f3f22008948aa244a81e29462a985d Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 20:15:01 +1100 Subject: [PATCH 11/16] * repeater: onAnonDataRecv(), now rejecting non-ASCII password (preparing for future request codes) * repeater: DISCOVER requests now with a simple RateLimiter (max 4, every 2 minutes) --- examples/simple_repeater/MyMesh.cpp | 15 +++++++++++---- examples/simple_repeater/MyMesh.h | 2 ++ examples/simple_repeater/RateLimiter.h | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 examples/simple_repeater/RateLimiter.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7489d611..9d313d21 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -433,7 +433,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m memcpy(×tamp, data, 4); data[len] = 0; // ensure null terminator - uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); + uint8_t reply_len; + if (data[0] == 0 || data[0] >= ' ') { // is password, ie. a login request + reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); + //} else if (data[0] == ANON_REQ_TYPE_*) { // future type codes + // TODO + } else { + reply_len = 0; // unknown request type + } if (reply_len == 0) return; // invalid request @@ -616,8 +623,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t void MyMesh::onControlDataRecv(mesh::Packet* packet) { uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits - if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) { - // TODO: apply rate limiting to these! + if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) { int i = 1; uint8_t filter = packet->payload[i++]; uint32_t tag; @@ -646,7 +652,8 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store) + _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store), + discover_limiter(4, 120) // max 4 every 2 minutes #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) #endif diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 45001597..86fac3f4 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -33,6 +33,7 @@ #include #include #include +#include "RateLimiter.h" #ifdef WITH_BRIDGE extern AbstractBridge* bridge; @@ -92,6 +93,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionMap region_map, temp_map; RegionEntry* load_stack[8]; RegionEntry* recv_pkt_region; + RateLimiter discover_limiter; bool region_load_active; unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS diff --git a/examples/simple_repeater/RateLimiter.h b/examples/simple_repeater/RateLimiter.h new file mode 100644 index 00000000..a6633c0a --- /dev/null +++ b/examples/simple_repeater/RateLimiter.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +class RateLimiter { + uint32_t _start_timestamp; + uint32_t _secs; + uint16_t _maximum, _count; + +public: + RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { } + + bool allow(uint32_t now) { + if (now < _start_timestamp + _secs) { + _count++; + if (_count > _maximum) return false; // deny + } else { // time window now expired + _start_timestamp = now; + _count = 1; + } + return true; + } +}; \ No newline at end of file From 256848208d6303b2f11f16d5026d027a3be7c0e8 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 20:22:40 +1100 Subject: [PATCH 12/16] * repeater: onAnonDataRecv(), future code check bug fix (offset 4) * sensor: onAnonDataRecv(), future request code provision --- examples/simple_repeater/MyMesh.cpp | 4 ++-- examples/simple_sensor/SensorMesh.cpp | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9d313d21..a996cbf6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -434,9 +434,9 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m data[len] = 0; // ensure null terminator uint8_t reply_len; - if (data[0] == 0 || data[0] >= ' ') { // is password, ie. a login request + if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); - //} else if (data[0] == ANON_REQ_TYPE_*) { // future type codes + //} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes // TODO } else { reply_len = 0; // unknown request type diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 6564c4ef..58bce766 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -449,7 +449,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con memcpy(×tamp, data, 4); data[len] = 0; // ensure null terminator - uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); + uint8_t reply_len; + if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request + reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); + //} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes + // TODO + } else { + reply_len = 0; // unknown request type + } if (reply_len == 0) return; // invalid request From ddac13ae80c74960a0879975741236f9021dc2cd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 21:40:52 +1100 Subject: [PATCH 13/16] * repeater: CLI, added "region put" and "region remove" commands --- examples/simple_repeater/MyMesh.cpp | 29 ++++++++++++++++++++++++----- src/helpers/RegionMap.cpp | 13 ++++++++++++- src/helpers/RegionMap.h | 2 ++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a996cbf6..b06ae42a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -890,10 +890,6 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -static bool is_name_char(char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#'; -} - void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { if (region_load_active) { if (*command == 0) { // empty line, signal to terminate 'load' operation @@ -907,7 +903,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply int indent = np - command; char *ep = np; - while (is_name_char(*ep)) ep++; + while (RegionMap::is_name_char(*ep)) ep++; if (*ep) { *ep++ = 0; } // set null terminator for end of name while (*ep && *ep != 'F') ep++; // look for (optional) flags @@ -1022,6 +1018,29 @@ 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], "put") == 0) { + auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + if (parent == NULL) { + strcpy(reply, "Err - unknown parent"); + } else { + auto region = region_map.putRegion(parts[2], parent->id); + if (region == NULL) { + strcpy(reply, "Err - unable to put"); + } else { + strcpy(reply, "OK"); + } + } + } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { + auto region = region_map.findByName(parts[2]); + if (region) { + if (region_map.removeRegion(*region)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - not empty"); + } + } else { + strcpy(reply, "Err - not found"); + } } else { strcpy(reply, "Err - ??"); } diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index c6221db0..7d1c08e6 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -9,6 +9,10 @@ RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { strcpy(wildcard.name, "*"); } +bool RegionMap::is_name_char(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#'; +} + static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); @@ -93,6 +97,12 @@ bool RegionMap::save(FILESYSTEM* _fs) { } RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) { + const char* sp = name; // check for illegal name chars + while (*sp) { + if (!is_name_char(*sp)) return NULL; // error + sp++; + } + auto region = findByName(name); if (region) { if (region->id == parent_id) return NULL; // ERROR: invalid parent! @@ -187,8 +197,9 @@ bool RegionMap::removeRegion(const RegionEntry& region) { if (i >= num_regions) return false; // failed (not found) num_regions--; // remove from regions array - while (i + 1 < num_regions) { + while (i < num_regions) { regions[i] = regions[i + 1]; + i++; } return true; // success } diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index c3f897c5..50513be1 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -30,6 +30,8 @@ class RegionMap { public: RegionMap(TransportKeyStore& store); + static bool is_name_char(char c); + bool load(FILESYSTEM* _fs); bool save(FILESYSTEM* _fs); From 4a5404d997ce89dae793e47b498f076b9a244d76 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 22:10:20 +1100 Subject: [PATCH 14/16] * companion: added CMD_SEND_CONTROL_DATA, and PUSH_CODE_CONTROL_DATA --- examples/companion_radio/MyMesh.cpp | 30 +++++++++++++++++++++++++++++ examples/companion_radio/MyMesh.h | 1 + 2 files changed, 31 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 904cd871..16be3e9c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -51,6 +51,7 @@ #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 #define CMD_SET_FLOOD_SCOPE 54 +#define CMD_SEND_CONTROL_DATA 55 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -100,6 +101,7 @@ #define PUSH_CODE_TELEMETRY_RESPONSE 0x8B #define PUSH_CODE_BINARY_RESPONSE 0x8C #define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D +#define PUSH_CODE_CONTROL_DATA 0x8E #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -626,6 +628,26 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); } +void MyMesh::onControlDataRecv(mesh::Packet *packet) { + if (packet->payload_len + 4 > sizeof(out_frame)) { + MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); + return; + } + int i = 0; + out_frame[i++] = PUSH_CODE_CONTROL_DATA; + out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); + out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = packet->path_len; + memcpy(&out_frame[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); + } +} + void MyMesh::onRawDataRecv(mesh::Packet *packet) { if (packet->payload_len + 4 > sizeof(out_frame)) { MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len); @@ -1523,6 +1545,14 @@ void MyMesh::handleCmdFrame(size_t len) { memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null } writeOKFrame(); + } else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) { + auto resp = createControlData(&cmd_frame[1], len - 1); + if (resp) { + sendZeroHop(resp); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index fb0fb479..927f22e4 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -133,6 +133,7 @@ protected: uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) override; void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override; + void onControlDataRecv(mesh::Packet *packet) override; void onRawDataRecv(mesh::Packet *packet) override; void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override; From 2e63499ae594e14842a3be35f5678389b748ad0a Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 6 Nov 2025 22:51:17 +1100 Subject: [PATCH 15/16] * companion: protocol ver bumped to 8. --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/MyMesh.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16be3e9c..598d535f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -50,8 +50,8 @@ #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 -#define CMD_SET_FLOOD_SCOPE 54 -#define CMD_SEND_CONTROL_DATA 55 +#define CMD_SET_FLOOD_SCOPE 54 // v8+ +#define CMD_SEND_CONTROL_DATA 55 // v8+ #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -101,7 +101,7 @@ #define PUSH_CODE_TELEMETRY_RESPONSE 0x8B #define PUSH_CODE_BINARY_RESPONSE 0x8C #define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D -#define PUSH_CODE_CONTROL_DATA 0x8E +#define PUSH_CODE_CONTROL_DATA 0x8E // v8+ #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 927f22e4..f2b56e5e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 7 +#define FIRMWARE_VER_CODE 8 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "2 Oct 2025" From 963290ea159c00edf4283bafd71464b27c8d95dc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 7 Nov 2025 14:42:06 +1100 Subject: [PATCH 16/16] * repeater: various "region" CLI changes * transport codes 0000 and FFFF reserved --- examples/simple_repeater/MyMesh.cpp | 17 +++++++++++------ src/helpers/RegionMap.cpp | 4 ++++ src/helpers/TransportKeyStore.cpp | 5 +++++ src/helpers/TxtDataHelpers.cpp | 7 +++++++ src/helpers/TxtDataHelpers.h | 1 + 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b06ae42a..cae05b20 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -892,7 +892,7 @@ void MyMesh::clearStats() { void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { if (region_load_active) { - if (*command == 0) { // empty line, signal to terminate 'load' operation + if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map region_load_active = false; @@ -908,7 +908,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply while (*ep && *ep != 'F') ep++; // look for (optional) flags - if (indent > 0 && indent < 8) { + if (indent > 0 && indent < 8 && strlen(np) > 0) { auto parent = load_stack[indent - 1]; if (parent) { auto old = region_map.findByName(np); @@ -985,7 +985,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply bool success = region_map.save(_fs); strcpy(reply, success ? "OK" : "Err - save failed"); } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { - auto region = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); + auto region = region_map.findByNamePrefix(parts[2]); if (region) { region->flags &= ~REGION_DENY_FLOOD; strcpy(reply, "OK"); @@ -993,7 +993,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { - auto region = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); + auto region = region_map.findByNamePrefix(parts[2]); if (region) { region->flags |= REGION_DENY_FLOOD; strcpy(reply, "OK"); @@ -1003,12 +1003,17 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (n >= 3 && strcmp(parts[1], "get") == 0) { auto region = region_map.findByNamePrefix(parts[2]); if (region) { - sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + auto parent = region_map.findById(region->parent); + if (parent && parent->id != 0) { + sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } else { + sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } } else { strcpy(reply, "Err - unknown region"); } } else if (n >= 3 && strcmp(parts[1], "home") == 0) { - auto home = strcmp(parts[2], "*") == 0 ? ®ion_map.getWildcard() : region_map.findByNamePrefix(parts[2]); + auto home = region_map.findByNamePrefix(parts[2]); if (home) { region_map.setHomeRegion(home); sprintf(reply, " home is now %s", home->name); diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 7d1c08e6..36844615 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -144,6 +144,8 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { } RegionEntry* RegionMap::findByName(const char* name) { + if (strcmp(name, "*") == 0) return &wildcard; + for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if (strcmp(name, region->name) == 0) return region; @@ -152,6 +154,8 @@ RegionEntry* RegionMap::findByName(const char* name) { } RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { + if (strcmp(prefix, "*") == 0) return &wildcard; + RegionEntry* partial = NULL; for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; diff --git a/src/helpers/TransportKeyStore.cpp b/src/helpers/TransportKeyStore.cpp index 8b7c891b..f34610b6 100644 --- a/src/helpers/TransportKeyStore.cpp +++ b/src/helpers/TransportKeyStore.cpp @@ -9,6 +9,11 @@ uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const { sha.update(&type, 1); sha.update(packet->payload, packet->payload_len); sha.finalizeHMAC(key, sizeof(key), &code, 2); + if (code == 0) { // reserve codes 0000 and FFFF + code++; + } else if (code == 0xFFFF) { + code--; + } return code; } diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index 0044fd28..224eb873 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -19,6 +19,13 @@ void StrHelper::strzcpy(char* dest, const char* src, size_t buf_sz) { } } +bool StrHelper::isBlank(const char* str) { + while (*str) { + if (*str++ != ' ') return false; + } + return true; +} + #include union int32_Float_t diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index 3154766c..89789990 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -12,4 +12,5 @@ public: static void strncpy(char* dest, const char* src, size_t buf_sz); static void strzcpy(char* dest, const char* src, size_t buf_sz); // pads with trailing nulls static const char* ftoa(float f); + static bool isBlank(const char* str); };