sync with vanilla dev

This commit is contained in:
liquidraver
2026-04-17 13:21:30 +02:00
parent ba4b86752d
commit d2cec84100
11 changed files with 692 additions and 568 deletions
+2 -1
View File
@@ -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 <name> [<parent>]` | Create a region; default parent is the wildcard root |
| `region put <name> [<parent>]` | Create a region; default parent is the wildcard root. Flood is **allowed** by default (use `region denyf` to deny) |
| `region remove <name>` | Remove a region (must have no children) |
| `region get <name>` | Show a region's parent and flood-allow flag |
| `region home [<name>]` | Get (no arg) or set the home region |
| `region default [<name>\|<null>]` | Get (no arg), set, or clear (`<null>`) 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 <name>` | Allow flood packets in a region (clears deny-flood flag) |
| `region denyf <name>` | Deny flood packets in a region (sets deny-flood flag) |
| `region list allowed` | List all regions that allow floods |
@@ -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;
@@ -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;
+63 -16
View File
@@ -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) {
+1 -17
View File
@@ -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;
+67 -9
View File
@@ -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], "<null>") == 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 <null>");
} 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 : "<null>");
} else if (n >= 3 && strcmp(parts[1], "put") == 0) {
auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_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)) {
+5 -1
View File
@@ -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; }
+127 -125
View File
@@ -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 <stdint.h>
#include <string.h>
#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 <stdint.h>
#include <string.h>
#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
}
+326 -311
View File
@@ -1,311 +1,326 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#include "RegionMap.h"
#include <helpers/TxtDataHelpers.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include <string.h>
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 = &regions[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 = &regions[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 = &regions[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 = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num;
if (region->name[0] == '$') { // private region
num = _store->loadKeysFor(region->id, keys, 4);
} else if (region->name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(region->id, region->name, keys[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(region->name) + 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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 <helpers/TxtDataHelpers.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include <string.h>
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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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 = &regions[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;
}
+72 -66
View File
@@ -1,66 +1,72 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RegionMap - Region-based flood filtering for repeaters
*/
#pragma once
#include <mesh/Packet.h>
#include "TransportKeyStore.h"
#include <cstdint>
#include <string.h>
#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 &regions[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 <mesh/Packet.h>
#include "TransportKeyStore.h"
#include <cstdint>
#include <string.h>
#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 &regions[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;
};
-19
View File
@@ -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 */