mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 02:08:25 +00:00
sync with vanilla dev
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]) : ®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)) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = ®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 <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 = ®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;
|
||||
}
|
||||
|
||||
@@ -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 ®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 <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 ®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;
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user