From d2cec84100b0bae28f9dc26f316bf4f19583a8c8 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:21:30 +0200 Subject: [PATCH] sync with vanilla dev --- zephcore/Repeater_CLI_commands.md | 3 +- .../adapters/datastore/ZephyrDataStore.cpp | 31 +- zephcore/adapters/datastore/ZephyrDataStore.h | 1 + zephcore/app/CompanionMesh.cpp | 79 ++- zephcore/app/CompanionMesh.h | 18 +- zephcore/app/RepeaterMesh.cpp | 76 ++- zephcore/app/RepeaterMesh.h | 6 +- zephcore/helpers/NodePrefs.h | 252 +++---- zephcore/helpers/RegionMap.cpp | 637 +++++++++--------- zephcore/helpers/RegionMap.h | 138 ++-- zephcore/src/main_companion.cpp | 19 - 11 files changed, 692 insertions(+), 568 deletions(-) diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index e8a91e2..87753ff 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -72,10 +72,11 @@ Regions control which flood packets the repeater forwards. The region tree is hi | `region` | Export the current region map (indented text tree) | | `region load` | Enter interactive region load mode. Paste indented region lines; send a blank line to commit | | `region save` | Save the current region map to persistent storage | -| `region put []` | Create a region; default parent is the wildcard root | +| `region put []` | Create a region; default parent is the wildcard root. Flood is **allowed** by default (use `region denyf` to deny) | | `region remove ` | Remove a region (must have no children) | | `region get ` | Show a region's parent and flood-allow flag | | `region home []` | Get (no arg) or set the home region | +| `region default [\|]` | Get (no arg), set, or clear (``) the default flood scope. Originated floods (self-adverts, etc.) are scoped with this region's TransportKey. Auto-creates the region if it doesn't exist and persists immediately | | `region allowf ` | Allow flood packets in a region (clears deny-flood flag) | | `region denyf ` | Deny flood packets in a region (sets deny-flood flag) | | `region list allowed` | List all regions that allow floods | diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 6ccb759..77e9c81 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -456,6 +456,22 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) prefs.apc_margin = 20; /* companion default */ } } + + /* Offset 96: default_scope_name (31 bytes) — v11 FIRMWARE_VER_CODE */ + if (off + 31 <= len) { + memcpy(prefs.default_scope_name, &buf[off], 31); + off += 31; + } else { + memset(prefs.default_scope_name, 0, sizeof(prefs.default_scope_name)); + } + + /* Offset 127: default_scope_key (16 bytes) */ + if (off + 16 <= len) { + memcpy(prefs.default_scope_key, &buf[off], 16); + off += 16; + } else { + memset(prefs.default_scope_key, 0, sizeof(prefs.default_scope_key)); + } } void ZephyrDataStore::savePrefs(const NodePrefs &prefs) @@ -510,7 +526,13 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs) buf[off++] = prefs.apc_enabled; /* Offset 95: apc_margin (ZephCore extension) */ buf[off++] = prefs.apc_margin; - /* Total: 96 bytes (Arduino reads 92, ZephCore reads 96) */ + /* Offset 96: default_scope_name (31 bytes) — v11 FIRMWARE_VER_CODE */ + memcpy(&buf[off], prefs.default_scope_name, 31); + off += 31; + /* Offset 127: default_scope_key (16 bytes) */ + memcpy(&buf[off], prefs.default_scope_key, 16); + off += 16; + /* Total: 143 bytes */ bool ok = atomicReplaceFile(PREFS_FILE, buf, off); LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'", @@ -780,8 +802,10 @@ bool ZephyrDataStore::deleteBlobByKey(const uint8_t key[], int key_len) uint32_t ZephyrDataStore::getStorageUsedKb() const { + /* Match Arduino DataStore: stats follow contacts/channels mount (/ext if present). */ + const char *mp = _has_ext_fs ? EXT_MNT_POINT : MNT_POINT; struct fs_statvfs sbuf; - if (fs_statvfs(MNT_POINT, &sbuf) != 0) { + if (fs_statvfs(mp, &sbuf) != 0) { return 0; } uint32_t total = sbuf.f_blocks * sbuf.f_frsize; @@ -791,8 +815,9 @@ uint32_t ZephyrDataStore::getStorageUsedKb() const uint32_t ZephyrDataStore::getStorageTotalKb() const { + const char *mp = _has_ext_fs ? EXT_MNT_POINT : MNT_POINT; struct fs_statvfs sbuf; - if (fs_statvfs(MNT_POINT, &sbuf) != 0) { + if (fs_statvfs(mp, &sbuf) != 0) { return 0; } return (sbuf.f_blocks * sbuf.f_frsize) / 1024; diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index ffb2845..8ffc4db 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -38,6 +38,7 @@ public: uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); bool deleteBlobByKey(const uint8_t key[], int key_len); + /* Used/total KiB for BLE storage report: /ext when mounted, else /lfs (Arduino parity). */ uint32_t getStorageUsedKb() const; uint32_t getStorageTotalKb() const; diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 8c489be..1aa375b 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -69,7 +69,7 @@ LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #define CMD_SEND_BINARY_REQ 0x32 #define CMD_FACTORY_RESET 0x33 #define CMD_SEND_PATH_DISCOVERY 0x34 -#define CMD_SET_FLOOD_SCOPE 0x36 +#define CMD_SET_FLOOD_SCOPE_KEY 0x36 /* v8+ (renamed from CMD_SET_FLOOD_SCOPE) */ #define CMD_SEND_CONTROL_DATA 0x37 #define CMD_GET_STATS 0x38 #define CMD_SEND_ANON_REQ 0x39 @@ -78,6 +78,8 @@ LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #define CMD_GET_ALLOWED_REPEAT_FREQ 0x3C #define CMD_SET_PATH_HASH_MODE 0x3D #define CMD_SEND_CHANNEL_DATA 0x3E +#define CMD_SET_DEFAULT_FLOOD_SCOPE 0x3F /* v11+ */ +#define CMD_GET_DEFAULT_FLOOD_SCOPE 0x40 /* v11+ */ /* Response packet types */ #define PACKET_OK 0x00 @@ -108,6 +110,7 @@ LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); #define PACKET_AUTOADD_CONFIG 0x19 #define PACKET_ALLOWED_REPEAT_FREQ 0x1A #define PACKET_CHANNEL_DATA_RECV 0x1B +#define PACKET_DEFAULT_FLOOD_SCOPE 0x1C #define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9) @@ -204,28 +207,36 @@ bool CompanionMesh::allowPacketForward(const mesh::Packet *packet) return prefs.client_repeat != 0; } -void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis) +void CompanionMesh::sendFloodScoped(const TransportKey &scope, mesh::Packet *pkt, uint32_t delay_millis) { - if (_send_scope.isNull()) { + if (scope.isNull()) { sendFlood(pkt, delay_millis, prefs.path_hash_mode + 1); } else { uint16_t codes[2]; - codes[0] = _send_scope.calcTransportCode(pkt); + codes[0] = scope.calcTransportCode(pkt); codes[1] = 0; sendFlood(pkt, codes, delay_millis, prefs.path_hash_mode + 1); } } +void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis) +{ + /* TODO: dynamic _send_scope, depending on recipient and current 'home' Region */ + TransportKey default_scope; + memcpy(default_scope.key, prefs.default_scope_key, sizeof(default_scope.key)); + + const TransportKey &scope = _send_scope.isNull() ? default_scope : _send_scope; + sendFloodScoped(scope, pkt, delay_millis); +} + void CompanionMesh::sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis) { - if (_send_scope.isNull()) { - sendFlood(pkt, delay_millis, prefs.path_hash_mode + 1); - } else { - uint16_t codes[2]; - codes[0] = _send_scope.calcTransportCode(pkt); - codes[1] = 0; - sendFlood(pkt, codes, delay_millis, prefs.path_hash_mode + 1); - } + /* TODO: have per-channel send_scope */ + TransportKey default_scope; + memcpy(default_scope.key, prefs.default_scope_key, sizeof(default_scope.key)); + + const TransportKey &scope = _send_scope.isNull() ? default_scope : _send_scope; + sendFloodScoped(scope, pkt, delay_millis); } bool CompanionMesh::onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len, @@ -1780,7 +1791,9 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) if (adv) { /* Optional param: data[1] == 1 means flood, else zero-hop */ if (len >= 2 && data[1] == 1) { - sendFlood(adv, (uint32_t)0, prefs.path_hash_mode + 1); + TransportKey default_scope; + memcpy(default_scope.key, prefs.default_scope_key, sizeof(default_scope.key)); + sendFloodScoped(default_scope, adv, (uint32_t)0); } else { sendZeroHop(adv); } @@ -1952,7 +1965,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) static const uint8_t version[20] = "v1.14.1-zephyr"; uint8_t rsp[82]; rsp[0] = PACKET_DEVICE_INFO; - rsp[1] = 10; // FIRMWARE_VER_CODE - v10 = path_hash_mode support + rsp[1] = 11; // FIRMWARE_VER_CODE - v11 = CMD_SET/GET_DEFAULT_FLOOD_SCOPE rsp[2] = (MAX_CONTACTS / 2 > 255) ? 255 : (MAX_CONTACTS / 2); // protocol byte, app multiplies by 2 rsp[3] = MAX_GROUP_CHANNELS; put_le32(&rsp[4], prefs.ble_pin ? prefs.ble_pin : 123456); // BLE PIN @@ -2604,8 +2617,8 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) } return true; - case CMD_SET_FLOOD_SCOPE: - /* Flood scope: [cmd][0][16-byte key] or [cmd][0] (null key) */ + case CMD_SET_FLOOD_SCOPE_KEY: + /* Set current send_scope key: [cmd][0][16-byte key] or [cmd][0] (null key) */ if (len >= 2 && data[1] == 0) { if (len >= 2 + 16) { memcpy(_send_scope.key, &data[2], sizeof(_send_scope.key)); @@ -2618,6 +2631,40 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) } return true; + case CMD_SET_DEFAULT_FLOOD_SCOPE: + /* Set default flood scope: [cmd][name:31][key:16] or [cmd] alone (clear) */ + if (len >= 1 + 31 + 16) { + int n = strnlen((const char *)&data[1], 31); + if (n > 0 && n < 31) { + memset(prefs.default_scope_name, 0, sizeof(prefs.default_scope_name)); + memcpy(prefs.default_scope_name, &data[1], n); + memcpy(prefs.default_scope_key, &data[1 + 31], 16); + _store->savePrefs(prefs); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + memset(prefs.default_scope_name, 0, sizeof(prefs.default_scope_name)); + memset(prefs.default_scope_key, 0, sizeof(prefs.default_scope_key)); + _store->savePrefs(prefs); + sendPacketOk(); + } + return true; + + case CMD_GET_DEFAULT_FLOOD_SCOPE: { + uint8_t rsp[1 + 31 + 16]; + rsp[0] = PACKET_DEFAULT_FLOOD_SCOPE; + if (strlen(prefs.default_scope_name) > 0) { + memcpy(&rsp[1], prefs.default_scope_name, 31); + memcpy(&rsp[1 + 31], prefs.default_scope_key, 16); + writeFrame(rsp, sizeof(rsp)); + } else { + writeFrame(rsp, 1); /* no name or key means null */ + } + return true; + } + case CMD_SEND_CONTROL_DATA: /* Control data: [cmd][flags | 0x80][data...] */ if (len >= 2 && (data[1] & 0x80) != 0) { diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index ff72a06..e6f61bb 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -87,9 +87,6 @@ typedef void (*RadioReconfigureCallback)(void); /* BLE PIN change callback */ typedef void (*PinChangeCallback)(uint32_t new_pin); -/* Callback for scheduling background save (called instead of blocking) */ -typedef void (*SaveScheduleCallback)(void); - /** * CompanionMesh: Application layer for ZephCore Companion device * @@ -141,19 +138,6 @@ public: */ void setPinChangeCallback(PinChangeCallback cb) { _pin_change_cb = cb; } - /** - * Set callback for scheduling background contact saves. - * When set, flushDirtyContacts() submits a work item instead of blocking. - */ - void setSaveScheduleCallback(SaveScheduleCallback cb) { _save_schedule_cb = cb; } - - /** - * Synchronous flush — saves contacts + channels to flash on the calling - * thread. Use ONLY before reboot / factory reset where we MUST block - * until the write completes. - */ - void flushAllSync(); - /** * Continue contact iteration (call each main loop iteration). * Returns true if contacts are still being sent. @@ -255,6 +239,7 @@ protected: uint8_t *extra, uint8_t extra_len) override; /* Flood scope - scoped sending for region filtering */ + void sendFloodScoped(const TransportKey &scope, mesh::Packet *pkt, uint32_t delay_millis); 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; @@ -290,7 +275,6 @@ private: GetBatteryCallback _batt_cb; RadioReconfigureCallback _radio_reconfig_cb; PinChangeCallback _pin_change_cb; - SaveScheduleCallback _save_schedule_cb; /* Contact iteration state */ bool _contact_iter_active; diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 892f93f..127b708 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -437,6 +437,30 @@ bool RepeaterMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counte return n >= max_counters[hash_size]; } +void RepeaterMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) { + if (scope.isNull()) { + sendFlood(pkt, delay_millis, path_hash_size); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, path_hash_size); + } +} + +void RepeaterMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // if _request_ packet scope is known, send reply with same scope + TransportKey scope; + if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) > 0) { + sendFloodScoped(scope, packet, delay_millis, path_hash_size); + } else { + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + } + } else { + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + } +} + bool RepeaterMesh::allowPacketForward(const mesh::Packet* packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; @@ -586,10 +610,10 @@ void RepeaterMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, c if (packet->isRouteFlood()) { mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else if (reply_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); @@ -656,14 +680,14 @@ void RepeaterMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { if (client->out_path_len != OUT_PATH_UNKNOWN) { sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -691,7 +715,7 @@ void RepeaterMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender mesh::Packet* ack = createAck(ack_hash); if (ack) { if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize()); + sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); } @@ -717,7 +741,7 @@ void RepeaterMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply_pkt) { if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply_pkt, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + sendFloodReply(reply_pkt, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply_pkt, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -823,6 +847,7 @@ RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Mil _logging = false; region_load_active = false; recv_pkt_region = nullptr; + memset(default_scope.key, 0, sizeof(default_scope.key)); pending_discover_tag = 0; pending_discover_until = 0; @@ -869,6 +894,14 @@ void RepeaterMesh::begin(RepeaterDataStore* store) { acl.load(_store->getAclPath(), self_id); region_map.load(_store->getRegionsPath()); + // establish default-scope from persisted default region (if any) + { + RegionEntry* r = region_map.getDefaultRegion(); + if (r) { + region_map.getTransportKeysFor(*r, &default_scope, 1); + } + } + /* NOTE: Radio configuration is handled by SX126xRadio adapter using * LoRaConfig defaults. The repeater uses the same radio params as companion. * Dynamic radio reconfiguration (via CLI) is not yet supported - radio @@ -994,7 +1027,7 @@ void RepeaterMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -1264,6 +1297,30 @@ void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* } 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) { + if (strcmp(parts[2], "") == 0) { + region_map.setDefaultRegion(nullptr); + memset(default_scope.key, 0, sizeof(default_scope.key)); + region_map.save(_store->getRegionsPath()); // persist in one atomic step + sprintf(reply, " default scope is now "); + } else { + auto def = region_map.findByNamePrefix(parts[2]); + if (def == nullptr) { + def = region_map.putRegion(parts[2], 0); // auto-create the default region + } + if (def) { + def->flags = 0; // make sure allow flood enabled + region_map.setDefaultRegion(def); + region_map.getTransportKeysFor(*def, &default_scope, 1); + region_map.save(_store->getRegionsPath()); // persist in one atomic step + sprintf(reply, " default scope is now %s", def->name); + } else { + strcpy(reply, "Err - region table full"); + } + } + } else if (n == 2 && strcmp(parts[1], "default") == 0) { + auto def = region_map.getDefaultRegion(); + sprintf(reply, " default scope is %s", def ? def->name : ""); } else if (n >= 3 && strcmp(parts[1], "put") == 0) { auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); if (parent == nullptr) { @@ -1273,7 +1330,8 @@ void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* if (region == nullptr) { strcpy(reply, "Err - unable to put"); } else { - strcpy(reply, "OK"); + region->flags = 0; // New default: enable flood + strcpy(reply, "OK - (flood allowed)"); } } } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { @@ -1561,7 +1619,7 @@ void RepeaterMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); - if (pkt) sendFlood(pkt, (uint32_t)0, _prefs.path_hash_mode + 1); + if (pkt) sendFloodScoped(default_scope, pkt, (uint32_t)0, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); updateAdvertTimer(); } else if (next_local_advert && millisHasNowPassed(next_local_advert)) { diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index e0dcaba..6b22fe5 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -94,6 +94,7 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks { RegionMap region_map, temp_map; RegionEntry* load_stack[8]; RegionEntry* recv_pkt_region; + TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; uint32_t pending_discover_tag; unsigned long pending_discover_until; @@ -122,13 +123,14 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks { #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); - void sendNodeDiscoverReq(); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); + void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); protected: uint8_t getDutyCyclePercent() const override { @@ -187,6 +189,8 @@ public: void begin(RepeaterDataStore* store); + void sendNodeDiscoverReq(); + /* CommonCLICallbacks */ const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 3d82c82..1a0d157 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -1,125 +1,127 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * NodePrefs - persisted node configuration (unified for all roles) - * - * Serialized field-by-field, not raw memcpy; struct layout does - * not affect on-disk compatibility. - */ - -#pragma once - -#include -#include - -#define TELEM_MODE_DENY 0 -#define TELEM_MODE_ALLOW_FLAGS 1 -#define TELEM_MODE_ALLOW_ALL 2 - -#define ADVERT_LOC_NONE 0 -#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 { - /* ---- Common fields (both roles) ---- */ - float airtime_factor; - char node_name[32]; - double node_lat, node_lon; - char password[16]; - float freq; - int8_t tx_power_dbm; - uint8_t disable_fwd; // repeater: disable forwarding - uint8_t advert_interval; // stored as minutes / 2 - uint8_t flood_advert_interval; // hours - float rx_delay_base; - float tx_delay_factor; - char guest_password[16]; - float direct_tx_delay_factor; - float backoff_multiplier; // per-dupe reactive backoff (0.0 = disabled) - uint32_t guard; - uint8_t sf; - uint8_t cr; - uint8_t allow_read_only; - uint8_t multi_acks; - float bw; - uint8_t flood_max; - uint8_t interference_threshold; - uint8_t agc_reset_interval; // stored as secs / 4 - // Power saving - uint8_t powersaving_enabled; - // GPS settings - uint8_t gps_enabled; - uint32_t gps_interval; // in seconds - uint8_t advert_loc_policy; - uint32_t discovery_mod_timestamp; - float adc_multiplier; - char owner_info[120]; - uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save - uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX - uint8_t apc_enabled; // 1 = APC on, 0 = fixed TX power - uint8_t apc_margin; // APC target link margin dB (6-30) - - /* ---- Companion-only fields ---- */ - uint8_t manual_add_contacts; - uint8_t telemetry_mode_base; - uint8_t telemetry_mode_loc; - uint8_t telemetry_mode_env; - uint32_t ble_pin; - uint8_t buzzer_quiet; - uint8_t autoadd_config; - uint8_t client_repeat; // 1 = offgrid mode (forward packets) - uint8_t path_hash_mode; // path mode 0-2 - uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops - uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT} - uint8_t leds_disabled; // 1 = LEDs off -}; - -/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */ -static inline void initNodePrefs(NodePrefs* prefs) { - memset(prefs, 0, sizeof(NodePrefs)); - prefs->airtime_factor = 10.0f; /* 10% duty cycle */ - prefs->node_lat = 0.0; - prefs->node_lon = 0.0; -#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD - strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1); -#else - strcpy(prefs->password, "password"); -#endif -#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD - strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1); -#endif - /* Radio params - MUST match LoRaConfig.h for interop with companion nodes */ - prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0 - prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH - prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR - prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8) -#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM - prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM; -#else - prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM -#endif - prefs->disable_fwd = 0; - prefs->advert_interval = 60; // 2 minutes (value / 2) - prefs->flood_advert_interval = 12; // 12 hours - prefs->rx_delay_base = 0.0f; - prefs->tx_delay_factor = 0.5f; - prefs->direct_tx_delay_factor = 0.3f; - prefs->allow_read_only = 0; - prefs->multi_acks = 0; - prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!) - prefs->interference_threshold = 0; - prefs->agc_reset_interval = 0; - prefs->powersaving_enabled = 0; - prefs->gps_enabled = 0; - prefs->gps_interval = 300; // 5 minutes - prefs->advert_loc_policy = ADVERT_LOC_NONE; - prefs->adc_multiplier = 0.0f; - prefs->rx_boost = 1; // Default to boosted RX for better sensitivity - prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability - prefs->apc_enabled = 0; // Default OFF — fixed TX power - prefs->apc_margin = 16; // Default 16 dB target link margin -} +/* + * SPDX-License-Identifier: Apache-2.0 + * NodePrefs - persisted node configuration (unified for all roles) + * + * Serialized field-by-field, not raw memcpy; struct layout does + * not affect on-disk compatibility. + */ + +#pragma once + +#include +#include + +#define TELEM_MODE_DENY 0 +#define TELEM_MODE_ALLOW_FLAGS 1 +#define TELEM_MODE_ALLOW_ALL 2 + +#define ADVERT_LOC_NONE 0 +#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 { + /* ---- Common fields (both roles) ---- */ + float airtime_factor; + char node_name[32]; + double node_lat, node_lon; + char password[16]; + float freq; + int8_t tx_power_dbm; + uint8_t disable_fwd; // repeater: disable forwarding + uint8_t advert_interval; // stored as minutes / 2 + uint8_t flood_advert_interval; // hours + float rx_delay_base; + float tx_delay_factor; + char guest_password[16]; + float direct_tx_delay_factor; + float backoff_multiplier; // per-dupe reactive backoff (0.0 = disabled) + uint32_t guard; + uint8_t sf; + uint8_t cr; + uint8_t allow_read_only; + uint8_t multi_acks; + float bw; + uint8_t flood_max; + uint8_t interference_threshold; + uint8_t agc_reset_interval; // stored as secs / 4 + // Power saving + uint8_t powersaving_enabled; + // GPS settings + uint8_t gps_enabled; + uint32_t gps_interval; // in seconds + uint8_t advert_loc_policy; + uint32_t discovery_mod_timestamp; + float adc_multiplier; + char owner_info[120]; + uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save + uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX + uint8_t apc_enabled; // 1 = APC on, 0 = fixed TX power + uint8_t apc_margin; // APC target link margin dB (6-30) + + /* ---- Companion-only fields ---- */ + uint8_t manual_add_contacts; + uint8_t telemetry_mode_base; + uint8_t telemetry_mode_loc; + uint8_t telemetry_mode_env; + uint32_t ble_pin; + uint8_t buzzer_quiet; + uint8_t autoadd_config; + uint8_t client_repeat; // 1 = offgrid mode (forward packets) + uint8_t path_hash_mode; // path mode 0-2 + uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops + uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT} + uint8_t leds_disabled; // 1 = LEDs off + char default_scope_name[31]; // companion: default flood scope region name ("" = null) + uint8_t default_scope_key[16]; // companion: default flood scope TransportKey +}; + +/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */ +static inline void initNodePrefs(NodePrefs* prefs) { + memset(prefs, 0, sizeof(NodePrefs)); + prefs->airtime_factor = 10.0f; /* 10% duty cycle */ + prefs->node_lat = 0.0; + prefs->node_lon = 0.0; +#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD + strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1); +#else + strcpy(prefs->password, "password"); +#endif +#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD + strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1); +#endif + /* Radio params - MUST match LoRaConfig.h for interop with companion nodes */ + prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0 + prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH + prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR + prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8) +#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM + prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM; +#else + prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM +#endif + prefs->disable_fwd = 0; + prefs->advert_interval = 60; // 2 minutes (value / 2) + prefs->flood_advert_interval = 12; // 12 hours + prefs->rx_delay_base = 0.0f; + prefs->tx_delay_factor = 0.5f; + prefs->direct_tx_delay_factor = 0.3f; + prefs->allow_read_only = 0; + prefs->multi_acks = 0; + prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!) + prefs->interference_threshold = 0; + prefs->agc_reset_interval = 0; + prefs->powersaving_enabled = 0; + prefs->gps_enabled = 0; + prefs->gps_interval = 300; // 5 minutes + prefs->advert_loc_policy = ADVERT_LOC_NONE; + prefs->adc_multiplier = 0.0f; + prefs->rx_boost = 1; // Default to boosted RX for better sensitivity + prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability + prefs->apc_enabled = 0; // Default OFF — fixed TX power + prefs->apc_margin = 16; // Default 16 dB target link margin +} diff --git a/zephcore/helpers/RegionMap.cpp b/zephcore/helpers/RegionMap.cpp index 07889d0..0a6e194 100644 --- a/zephcore/helpers/RegionMap.cpp +++ b/zephcore/helpers/RegionMap.cpp @@ -1,311 +1,326 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * RegionMap - Region-based flood filtering for repeaters - */ - -#include "RegionMap.h" -#include -#include -#include -#include -#include - -LOG_MODULE_REGISTER(zephcore_regions, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); - -static const char* skip_hash(const char* name) { - return *name == '#' ? name + 1 : name; -} - -RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { - next_id = 1; - num_regions = 0; - home_id = 0; - wildcard.id = 0; - wildcard.parent = 0; - wildcard.flags = 0; // default behaviour, allow flood and direct - strcpy(wildcard.name, "*"); -} - -bool RegionMap::is_name_char(uint8_t c) { - // accept all alpha-num or accented characters, but exclude most punctuation chars - return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A'; -} - -bool RegionMap::load(const char* path) { - const char* filepath = path; - - struct fs_file_t file; - fs_file_t_init(&file); - - if (fs_open(&file, filepath, FS_O_READ) < 0) { - LOG_DBG("No regions file at %s", filepath); - return false; - } - - uint8_t pad[128]; - num_regions = 0; - next_id = 1; - home_id = 0; - - bool success = fs_read(&file, pad, 5) == 5; // reserved header - success = success && fs_read(&file, &home_id, sizeof(home_id)) == sizeof(home_id); - success = success && fs_read(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); - success = success && fs_read(&file, &next_id, sizeof(next_id)) == sizeof(next_id); - - if (success) { - while (num_regions < MAX_REGION_ENTRIES) { - auto r = ®ions[num_regions]; - - success = fs_read(&file, &r->id, sizeof(r->id)) == sizeof(r->id); - success = success && fs_read(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); - success = success && fs_read(&file, r->name, sizeof(r->name)) == sizeof(r->name); - success = success && fs_read(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); - success = success && fs_read(&file, 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++; - } - } - fs_close(&file); - LOG_INF("Loaded %d regions from %s", num_regions, filepath); - return true; -} - -bool RegionMap::save(const char* path) { - const char* filepath = path; - - // Remove old file first - fs_unlink(filepath); - - struct fs_file_t file; - fs_file_t_init(&file); - - if (fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE) < 0) { - LOG_ERR("Failed to open %s for write", filepath); - return false; - } - - uint8_t pad[128]; - memset(pad, 0, sizeof(pad)); - - bool success = fs_write(&file, pad, 5) == 5; // reserved header - success = success && fs_write(&file, &home_id, sizeof(home_id)) == sizeof(home_id); - success = success && fs_write(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); - success = success && fs_write(&file, &next_id, sizeof(next_id)) == sizeof(next_id); - - if (success) { - for (int i = 0; i < num_regions; i++) { - auto r = ®ions[i]; - - success = fs_write(&file, &r->id, sizeof(r->id)) == sizeof(r->id); - success = success && fs_write(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); - success = success && fs_write(&file, r->name, sizeof(r->name)) == sizeof(r->name); - success = success && fs_write(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); - success = success && fs_write(&file, pad, sizeof(pad)) == sizeof(pad); - - if (!success) break; // write failed - } - } - fs_close(&file); - LOG_INF("Saved %d regions to %s", num_regions, filepath); - return true; -} - -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 nullptr; // error - sp++; - } - - auto region = findByName(name); - if (region) { - if (region->id == parent_id) return nullptr; // ERROR: invalid parent! - region->parent = parent_id; // re-parent / move this region in the hierarchy - } else { - if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return nullptr; // full! - - region = ®ions[num_regions++]; // alloc new RegionEntry - 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; - } - 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) == 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) + 1]; - tmp[0] = '#'; - memcpy(&tmp[1], region->name, sizeof(region->name) - 1); - tmp[sizeof(region->name)] = '\0'; - _store->getAutoKeyFor(region->id, tmp, keys[0]); - num = 1; - } - 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 nullptr; // no matches -} - -RegionEntry* RegionMap::findByName(const char* name) { - if (strcmp(name, "*") == 0) return &wildcard; - - if (*name == '#') { name++; } // ignore the '#' when matching by name - for (int i = 0; i < num_regions; i++) { - auto region = ®ions[i]; - if (strcmp(name, skip_hash(region->name)) == 0) return region; - } - return nullptr; // not found -} - -RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { - if (strcmp(prefix, "*") == 0) return &wildcard; - - if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name - RegionEntry* partial = nullptr; - for (int i = 0; i < num_regions; i++) { - auto region = ®ions[i]; - if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // complete match - if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) { - partial = region; - } - } - return partial; -} - -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 nullptr; // 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; // cannot remove wildcard - - // first check region has no child regions - for (int i = 0; i < num_regions; i++) { - if (regions[i].parent == region.id) return false; // must remove children first - } - - int i = 0; - while (i < num_regions) { - if (region.id == regions[i].id) break; - i++; - } - if (i >= num_regions) return false; // not found - - num_regions--; // remove from regions array - while (i < num_regions) { - regions[i] = regions[i + 1]; - i++; - } - return true; -} - -bool RegionMap::clear() { - num_regions = 0; - return true; -} - -void RegionMap::printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const { - // Print indentation - for (int i = 0; i < indent && pos < max_len - 1; i++) { - buf[pos++] = ' '; - } - - // Print region info - int written; - if (parent->flags & REGION_DENY_FLOOD) { - written = snprintf(&buf[pos], max_len - pos, "%s%s\n", - skip_hash(parent->name), - parent->id == home_id ? "^" : ""); - } else { - written = snprintf(&buf[pos], max_len - pos, "%s%s F\n", - skip_hash(parent->name), - parent->id == home_id ? "^" : ""); - } - if (written > 0 && pos + written < max_len) { - pos += written; - } - - // Print children recursively - for (int i = 0; i < num_regions; i++) { - auto r = ®ions[i]; - if (r->parent == parent->id) { - printChildRegions(indent + 1, r, buf, pos, max_len); - } - } -} - -size_t RegionMap::exportTo(char* dest, size_t max_len) const { - if (!dest || max_len == 0) return 0; - - int pos = 0; - printChildRegions(0, &wildcard, dest, pos, (int)max_len); - return (size_t)pos; -} - -int RegionMap::exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert) { - char* dp = dest; - - // 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]; - - // 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, 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; -} +/* + * SPDX-License-Identifier: Apache-2.0 + * RegionMap - Region-based flood filtering for repeaters + */ + +#include "RegionMap.h" +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_regions, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +static const char* skip_hash(const char* name) { + return *name == '#' ? name + 1 : name; +} + +RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { + next_id = 1; + num_regions = 0; + default_id = home_id = 0; + wildcard.id = 0; + wildcard.parent = 0; + wildcard.flags = 0; // default behaviour, allow flood and direct + strcpy(wildcard.name, "*"); +} + +bool RegionMap::is_name_char(uint8_t c) { + // accept all alpha-num or accented characters, but exclude most punctuation chars + return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A'; +} + +bool RegionMap::load(const char* path) { + const char* filepath = path; + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, filepath, FS_O_READ) < 0) { + LOG_DBG("No regions file at %s", filepath); + return false; + } + + uint8_t pad[128]; + num_regions = 0; + next_id = 1; + default_id = home_id = 0; + + bool success = fs_read(&file, pad, 3) == 3; // reserved header + success = success && fs_read(&file, &default_id, sizeof(default_id)) == sizeof(default_id); + success = success && fs_read(&file, &home_id, sizeof(home_id)) == sizeof(home_id); + success = success && fs_read(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); + success = success && fs_read(&file, &next_id, sizeof(next_id)) == sizeof(next_id); + + if (success) { + while (num_regions < MAX_REGION_ENTRIES) { + auto r = ®ions[num_regions]; + + success = fs_read(&file, &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && fs_read(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && fs_read(&file, r->name, sizeof(r->name)) == sizeof(r->name); + success = success && fs_read(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && fs_read(&file, 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++; + } + } + fs_close(&file); + LOG_INF("Loaded %d regions from %s", num_regions, filepath); + return true; +} + +bool RegionMap::save(const char* path) { + const char* filepath = path; + + // Remove old file first + fs_unlink(filepath); + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("Failed to open %s for write", filepath); + return false; + } + + uint8_t pad[128]; + memset(pad, 0, sizeof(pad)); + + bool success = fs_write(&file, pad, 3) == 3; // reserved header + success = success && fs_write(&file, &default_id, sizeof(default_id)) == sizeof(default_id); + success = success && fs_write(&file, &home_id, sizeof(home_id)) == sizeof(home_id); + success = success && fs_write(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); + success = success && fs_write(&file, &next_id, sizeof(next_id)) == sizeof(next_id); + + if (success) { + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + + success = fs_write(&file, &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && fs_write(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && fs_write(&file, r->name, sizeof(r->name)) == sizeof(r->name); + success = success && fs_write(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && fs_write(&file, pad, sizeof(pad)) == sizeof(pad); + + if (!success) break; // write failed + } + } + fs_close(&file); + LOG_INF("Saved %d regions to %s", num_regions, filepath); + return true; +} + +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 nullptr; // error + sp++; + } + + auto region = findByName(name); + if (region) { + if (region->id == parent_id) return nullptr; // ERROR: invalid parent! + region->parent = parent_id; // re-parent / move this region in the hierarchy + } else { + if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return nullptr; // full! + + region = ®ions[num_regions++]; // alloc new RegionEntry + 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; + } + 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] = '#'; + memcpy(&tmp[1], src.name, sizeof(src.name) - 1); + tmp[sizeof(src.name)] = '\0'; + _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 = 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!! + return region; + } + } + } + } + return nullptr; // no matches +} + +RegionEntry* RegionMap::findByName(const char* name) { + if (strcmp(name, "*") == 0) return &wildcard; + + if (*name == '#') { name++; } // ignore the '#' when matching by name + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (strcmp(name, skip_hash(region->name)) == 0) return region; + } + return nullptr; // not found +} + +RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { + if (strcmp(prefix, "*") == 0) return &wildcard; + + if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name + RegionEntry* partial = nullptr; + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // complete match + if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) { + partial = region; + } + } + return partial; +} + +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 nullptr; // not found +} + +RegionEntry* RegionMap::getHomeRegion() { + return findById(home_id); +} + +void RegionMap::setHomeRegion(const RegionEntry* home) { + home_id = home ? home->id : 0; +} + +RegionEntry* RegionMap::getDefaultRegion() { + return default_id == 0 ? nullptr : 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; // cannot remove wildcard + + // first check region has no child regions + for (int i = 0; i < num_regions; i++) { + if (regions[i].parent == region.id) return false; // must remove children first + } + + int i = 0; + while (i < num_regions) { + if (region.id == regions[i].id) break; + i++; + } + if (i >= num_regions) return false; // not found + + num_regions--; // remove from regions array + while (i < num_regions) { + regions[i] = regions[i + 1]; + i++; + } + return true; +} + +bool RegionMap::clear() { + num_regions = 0; + return true; +} + +void RegionMap::printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const { + // Print indentation + for (int i = 0; i < indent && pos < max_len - 1; i++) { + buf[pos++] = ' '; + } + + // Print region info + int written; + if (parent->flags & REGION_DENY_FLOOD) { + written = snprintf(&buf[pos], max_len - pos, "%s%s\n", + skip_hash(parent->name), + parent->id == home_id ? "^" : ""); + } else { + written = snprintf(&buf[pos], max_len - pos, "%s%s F\n", + skip_hash(parent->name), + parent->id == home_id ? "^" : ""); + } + if (written > 0 && pos + written < max_len) { + pos += written; + } + + // Print children recursively + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + if (r->parent == parent->id) { + printChildRegions(indent + 1, r, buf, pos, max_len); + } + } +} + +size_t RegionMap::exportTo(char* dest, size_t max_len) const { + if (!dest || max_len == 0) return 0; + + int pos = 0; + printChildRegions(0, &wildcard, dest, pos, (int)max_len); + return (size_t)pos; +} + +int RegionMap::exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert) { + char* dp = dest; + + // 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]; + + // 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, 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; +} diff --git a/zephcore/helpers/RegionMap.h b/zephcore/helpers/RegionMap.h index 57db3d8..c9ac0d0 100644 --- a/zephcore/helpers/RegionMap.h +++ b/zephcore/helpers/RegionMap.h @@ -1,66 +1,72 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * RegionMap - Region-based flood filtering for repeaters - */ - -#pragma once - -#include -#include "TransportKeyStore.h" -#include -#include - -#ifndef MAX_REGION_ENTRIES - #ifdef CONFIG_ZEPHCORE_MAX_REGION_ENTRIES - #define MAX_REGION_ENTRIES CONFIG_ZEPHCORE_MAX_REGION_ENTRIES - #else - #define MAX_REGION_ENTRIES 32 - #endif -#endif - -#define REGION_DENY_FLOOD 0x01 -#define REGION_DENY_DIRECT 0x02 // reserved for future - -struct RegionEntry { - uint16_t id; - uint16_t parent; - uint8_t flags; - char name[31]; -}; - -class RegionMap { - TransportKeyStore* _store; - uint16_t next_id; - uint16_t home_id; - uint16_t num_regions; - RegionEntry regions[MAX_REGION_ENTRIES]; - RegionEntry wildcard; - - void printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const; - -public: - RegionMap(TransportKeyStore& store); - - static bool is_name_char(uint8_t c); - - bool load(const char* path = nullptr); - bool save(const char* path = nullptr); - - 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); - 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; } - 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, bool invert = false); - - size_t exportTo(char* dest, size_t max_len) const; -}; +/* + * SPDX-License-Identifier: Apache-2.0 + * RegionMap - Region-based flood filtering for repeaters + */ + +#pragma once + +#include +#include "TransportKeyStore.h" +#include +#include + +#ifndef MAX_REGION_ENTRIES + #ifdef CONFIG_ZEPHCORE_MAX_REGION_ENTRIES + #define MAX_REGION_ENTRIES CONFIG_ZEPHCORE_MAX_REGION_ENTRIES + #else + #define MAX_REGION_ENTRIES 32 + #endif +#endif + +#define REGION_DENY_FLOOD 0x01 +#define REGION_DENY_DIRECT 0x02 // reserved for future + +struct RegionEntry { + uint16_t id; + uint16_t parent; + uint8_t flags; + char name[31]; + + bool isWildcard() const { return id == 0; } +}; + +class RegionMap { + TransportKeyStore* _store; + uint16_t next_id; + uint16_t home_id; + uint16_t default_id; + uint16_t num_regions; + RegionEntry regions[MAX_REGION_ENTRIES]; + RegionEntry wildcard; + + void printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const; + +public: + RegionMap(TransportKeyStore& store); + + static bool is_name_char(uint8_t c); + + bool load(const char* path = nullptr); + bool save(const char* path = nullptr); + + 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); + 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, bool invert = false); + int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num); + + size_t exportTo(char* dest, size_t max_len) const; +}; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index bfb2d4b..7b335d6 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -81,12 +81,10 @@ static struct k_event mesh_events; /* Work items for event-driven processing */ static void rx_process_work_fn(struct k_work *work); static void contact_iter_work_fn(struct k_work *work); -static void contact_save_work_fn(struct k_work *work); static void housekeeping_timer_fn(struct k_timer *timer); K_WORK_DEFINE(rx_process_work, rx_process_work_fn); K_WORK_DEFINE(contact_iter_work, contact_iter_work_fn); -K_WORK_DEFINE(contact_save_work, contact_save_work_fn); /* Housekeeping timer for periodic tasks (noise floor calibration, etc.) * Fires every 5 seconds to wake event loop for maintenance without @@ -394,22 +392,6 @@ static mesh::SimpleMeshTables mesh_tables; static mesh::StaticPoolPacketManager packet_mgr; static CompanionMesh companion_mesh(lora_radio, ms_clock, zephyr_rng, rtc_clock, packet_mgr, mesh_tables, data_store); - -/* Contact save work — runs on system workqueue so the main mesh thread - * stays free to drain LoRa ring buffer and process BLE frames. - * Submitted from CompanionMesh::flushDirtyContacts() via callback. */ -static void contact_save_work_fn(struct k_work *work) -{ - ARG_UNUSED(work); - if (companion_mesh_ptr) { - data_store.saveContacts(companion_mesh_ptr); - } -} - -static void schedule_contact_save(void) -{ - k_work_submit(&contact_save_work); -} #endif /* GPS enable callback - logs state changes @@ -615,7 +597,6 @@ int main(void) companion_mesh.setPinChangeCallback([](uint32_t new_pin) { zephcore_ble_set_passkey(new_pin); }); - companion_mesh.setSaveScheduleCallback(schedule_contact_save); companion_mesh_ptr = &companion_mesh; /* Set LoRa callbacks for event-driven packet processing */