refactor: split RepeaterMesh into RepeaterUplink + RepeaterRegionCLI

This commit is contained in:
liquidraver
2026-05-29 15:25:18 +02:00
parent 5895d61211
commit ea27b93ac5
5 changed files with 453 additions and 388 deletions
+2
View File
@@ -487,6 +487,7 @@ if(CONFIG_ZEPHCORE_ROLE_REPEATER)
target_sources(app PRIVATE
src/main_repeater.cpp
app/RepeaterMesh.cpp
app/RepeaterRegionCLI.cpp
app/RepeaterDataStore.cpp
helpers/ClientACL.cpp
helpers/RegionMap.cpp
@@ -505,6 +506,7 @@ if(CONFIG_ZEPHCORE_ROLE_REPEATER)
)
if(CONFIG_ZEPHCORE_REPEATER_UPLINK AND CONFIG_MQTT_LIB)
target_sources(app PRIVATE
app/RepeaterUplink.cpp
app/observer_creds.cpp
adapters/wifi/ZephyrWiFiStation.c
adapters/mqtt/ZephyrMQTTPublisher.c
+8 -388
View File
@@ -1188,78 +1188,12 @@ void RepeaterMesh::resetDutyCycleTimeoutRestarts() {
getRadioDriver(_radio).resetDutyCycleTimeoutRestarts();
}
/* ---------- region def helpers ---------- */
static char* skipSpaces(char* s) { while (*s == ' ') s++; return s; }
static void rtrimSpaces(char* s) { char* e = s + strlen(s); while (e > s && e[-1] == ' ') *--e = '\0'; }
static char* takeToken(char** cursor) {
char* p = skipSpaces(*cursor);
if (*p == '\0') { *cursor = p; return nullptr; }
char* tok = p;
while (*p && *p != ' ') p++;
if (*p) *p++ = '\0';
*cursor = p;
return tok;
}
static char* splitNameJump(char* tok) {
for (char* q = tok; *q; q++) {
if (*q == '|' || *q == ',') {
*q = '\0';
char* jump = skipSpaces(q + 1);
rtrimSpaces(jump);
return jump;
}
}
return nullptr;
}
static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) {
char* jump = splitNameJump(tok);
char* name = skipSpaces(tok);
if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; }
if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; }
RegionEntry* r = map->putRegion(name, (*cursor)->id);
if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; }
r->flags = 0;
if (jump) {
RegionEntry* j = map->findByNamePrefix(jump);
if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; }
*cursor = j;
} else {
*cursor = r;
}
return true;
}
/* ---------------------------------------- */
/* Region-def CLI (handleRegionLoadLine / handleRegionCommand) and its static
* parser helpers live in app/RepeaterRegionCLI.cpp. */
void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
if (region_load_active) {
if (StrHelper::isBlank(command)) {
region_map = temp_map;
region_load_active = false;
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
} else {
char* np = command;
while (*np == ' ') np++;
int indent = np - command;
char* ep = np;
while (RegionMap::is_name_char(*ep)) ep++;
if (*ep) { *ep++ = 0; }
while (*ep && *ep != 'F') ep++;
if (indent > 0 && indent < 8 && strlen(np) > 0) {
auto parent = load_stack[indent - 1];
if (parent) {
auto old = region_map.findByName(np);
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0);
if (nw) {
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD);
load_stack[indent] = nw;
}
}
}
reply[0] = 0;
}
handleRegionLoadLine(command, reply);
return;
}
@@ -1330,148 +1264,7 @@ void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char*
}
reply[0] = 0;
} else if (memcmp(command, "region", 6) == 0) {
reply[0] = 0;
// `region def`: cursor-walk bulk region builder — must run before parseTextParts
// mutates and truncates the buffer to 4 segments.
char* cmd = skipSpaces(command);
if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) {
char* payload = skipSpaces(cmd + 10);
rtrimSpaces(payload);
if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); goto region_done; }
RegionEntry* cursor = &region_map.getWildcard();
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
if (!processRegionDefSegment(&region_map, tok, &cursor, reply)) goto region_done;
}
region_map.exportTo(reply, 160);
goto region_done;
}
{
const char* parts[4];
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
if (n == 1) {
region_map.exportTo(reply, 160);
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
temp_map.resetFrom(region_map);
memset(load_stack, 0, sizeof(load_stack));
load_stack[0] = &temp_map.getWildcard();
region_load_active = true;
} else if (n >= 2 && strcmp(parts[1], "save") == 0) {
_prefs.discovery_mod_timestamp = getRTCClock()->getCurrentTime();
savePrefs();
bool success = region_map.save(_store->getRegionsPath());
strcpy(reply, success ? "OK" : "Err - save failed");
} else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags &= ~REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags |= REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "get") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
auto parent = region_map.findById(region->parent);
if (parent && parent->id != 0) {
sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
} else {
sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
}
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "home") == 0) {
auto home = region_map.findByNamePrefix(parts[2]);
if (home) {
region_map.setHomeRegion(home);
sprintf(reply, " home is now %s", home->name);
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n == 2 && strcmp(parts[1], "home") == 0) {
auto home = region_map.getHomeRegion();
sprintf(reply, " home is %s", home ? home->name : "*");
} else 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) {
strcpy(reply, "Err - unknown parent");
} else {
auto region = region_map.putRegion(parts[2], parent->id);
if (region == nullptr) {
strcpy(reply, "Err - unable to put");
} else {
region->flags = 0; // New default: enable flood
strcpy(reply, "OK - (flood allowed)");
}
}
} else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
auto region = region_map.findByName(parts[2]);
if (region) {
if (region_map.removeRegion(*region)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - not empty");
}
} else {
strcpy(reply, "Err - not found");
}
} else if (n >= 3 && strcmp(parts[1], "list") == 0) {
uint8_t mask = 0;
bool invert = false;
if (strcmp(parts[2], "allowed") == 0) {
mask = REGION_DENY_FLOOD;
invert = false;
} else if (strcmp(parts[2], "denied") == 0) {
mask = REGION_DENY_FLOOD;
invert = true;
} else {
strcpy(reply, "Err - use 'allowed' or 'denied'");
return;
}
int len = region_map.exportNamesTo(reply, 160, mask, invert);
if (len == 0) {
strcpy(reply, "-none-");
}
} else {
strcpy(reply, "Err - ??");
}
} // end parseTextParts scope
region_done:;
handleRegionCommand(command, reply);
} else if (memcmp(command, "discover.neighbors", 18) == 0) {
const char* sub = command + 18;
while (*sub == ' ') sub++;
@@ -1486,183 +1279,10 @@ void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char*
}
}
#if IS_ENABLED(CONFIG_ZEPHCORE_REPEATER_UPLINK) && IS_ENABLED(CONFIG_MQTT_LIB)
bool RepeaterMesh::saveUplinkCreds()
{
if (!_store) return false;
return observer_creds_save(&_uplink_creds, _store->getBasePath());
}
bool RepeaterMesh::handleUplinkCommand(const char *command, char *reply)
{
if (memcmp(command, "get uplink.", 11) == 0) {
const char *key = command + 11;
if (strcmp(key, "enable") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", isUplinkEnabled() ? "on" : "off");
} else if (strcmp(key, "wifi.ssid") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.wifi_ssid[0] ? _uplink_creds.wifi_ssid : "(not set)");
} else if (strcmp(key, "mqtt.host") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_host[0] ? _uplink_creds.mqtt_host : "(not set)");
} else if (strcmp(key, "mqtt.port") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)_uplink_creds.mqtt_port);
} else if (strcmp(key, "mqtt.tls") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)_uplink_creds.mqtt_tls);
} else if (strcmp(key, "mqtt.user") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_user[0] ? _uplink_creds.mqtt_user : "(not set)");
} else if (strcmp(key, "mqtt.iata") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_iata[0] ? _uplink_creds.mqtt_iata : "(not set)");
} else if (strcmp(key, "status") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> enabled=%s wifi=%s mqtt=%s reboot_required=%s",
isUplinkEnabled() ? "yes" : "no",
zc_wifi_station_is_connected() ? "up" : "down",
mqtt_publisher_is_connected() ? "up" : "down",
_uplink_reboot_required ? "yes" : "no");
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: uplink.%s", key);
}
return true;
}
if (memcmp(command, "set uplink.", 11) == 0) {
const char *cfg = command + 11;
const char *val = strchr(cfg, ' ');
if (!val) {
strcpy(reply, "Error: value required");
return true;
}
int key_len = (int)(val - cfg);
val++;
if (key_len == 6 && memcmp(cfg, "enable", 6) == 0) {
if (strcmp(val, "on") == 0) {
setUplinkEnabled(true);
} else if (strcmp(val, "off") == 0) {
setUplinkEnabled(false);
} else {
strcpy(reply, "Error: must be on or off");
return true;
}
} else if (key_len == 9 && memcmp(cfg, "wifi.ssid", 9) == 0) {
StrHelper::strncpy(_uplink_creds.wifi_ssid, val, sizeof(_uplink_creds.wifi_ssid));
} else if (key_len == 8 && memcmp(cfg, "wifi.psk", 8) == 0) {
StrHelper::strncpy(_uplink_creds.wifi_psk, val, sizeof(_uplink_creds.wifi_psk));
} else if (key_len == 9 && memcmp(cfg, "mqtt.host", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_host, val, sizeof(_uplink_creds.mqtt_host));
} else if (key_len == 9 && memcmp(cfg, "mqtt.port", 9) == 0) {
int port = atoi(val);
if (port < 1 || port > 65535) {
strcpy(reply, "Error: port range 1-65535");
return true;
}
_uplink_creds.mqtt_port = (uint16_t)port;
} else if (key_len == 8 && memcmp(cfg, "mqtt.tls", 8) == 0) {
_uplink_creds.mqtt_tls = (atoi(val) != 0) ? 1 : 0;
} else if (key_len == 9 && memcmp(cfg, "mqtt.user", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_user, val, sizeof(_uplink_creds.mqtt_user));
} else if (key_len == 13 && memcmp(cfg, "mqtt.password", 13) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_password, val, sizeof(_uplink_creds.mqtt_password));
} else if (key_len == 9 && memcmp(cfg, "mqtt.iata", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_iata, val, sizeof(_uplink_creds.mqtt_iata));
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: uplink.%.*s", key_len, cfg);
return true;
}
if (!saveUplinkCreds()) {
strcpy(reply, "Error: save failed");
return true;
}
markUplinkRebootRequired();
strcpy(reply, "OK - reboot to apply");
return true;
}
return false;
}
void RepeaterMesh::publishUplinkPacket(mesh::Packet *pkt)
{
if (!isUplinkEnabled() || !mqtt_publisher_is_connected()) return;
if (_uplink_packets_topic[0] == '\0') return;
char raw_hex[MAX_TRANS_UNIT * 2 + 1];
mesh::Utils::toHex(raw_hex, _uplink_last_raw, _uplink_last_raw_len);
raw_hex[_uplink_last_raw_len * 2] = '\0';
uint8_t hash_bytes[MAX_HASH_SIZE];
char hash_hex[MAX_HASH_SIZE * 2 + 1];
pkt->calculatePacketHash(hash_bytes);
mesh::Utils::toHex(hash_hex, hash_bytes, MAX_HASH_SIZE);
hash_hex[MAX_HASH_SIZE * 2] = '\0';
uint32_t now_epoch = getRTCClock()->getCurrentTime();
static char json_buf[1024];
struct MeshcorePacketJson pj = {
_prefs.node_name,
_uplink_pubkey_hex,
now_epoch,
_uplink_last_raw_len,
(unsigned)pkt->getPayloadType(),
pkt->isRouteDirect() ? "D" : "F",
(unsigned)pkt->payload_len,
raw_hex,
(int)pkt->getSNR(),
(int)_uplink_last_rssi,
(int)(_uplink_last_score * 1000.0f),
hash_hex,
};
int json_len = meshcore_build_packet_json(json_buf, sizeof(json_buf), &pj);
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
return;
}
mqtt_publisher_enqueue(_uplink_packets_topic, json_buf, json_len);
}
void RepeaterMesh::publishUplinkStatus(const char *status)
{
if (!isUplinkEnabled()) return;
if (_uplink_status_topic[0] == '\0') return;
auto& radio_driver = getRadioDriver(_radio);
uint32_t now_epoch = getRTCClock()->getCurrentTime();
char radio_buf[48];
snprintf(radio_buf, sizeof(radio_buf), "%.3f,%.1f,%u,%u",
(double)_prefs.freq, (double)_prefs.bw,
(unsigned)_prefs.sf, (unsigned)_prefs.cr);
static char json_buf[768];
struct MeshcoreStatusJson sj = {
status,
now_epoch,
_prefs.node_name,
_uplink_pubkey_hex,
radio_buf,
#ifdef CONFIG_ZEPHCORE_BOARD_NAME
CONFIG_ZEPHCORE_BOARD_NAME,
#else
"unknown",
#endif
FIRMWARE_VERSION,
(unsigned)_board.getBattMilliVolts(),
(unsigned)(uptime_millis / 1000),
(unsigned)_err_flags,
(unsigned)_mgr->getOutboundTotal(),
_radio->getNoiseFloor(),
(unsigned)(getTotalAirTime() / 1000),
(unsigned)(getReceiveAirTime() / 1000),
(unsigned)radio_driver.getPacketsRecvErrors(),
};
int json_len = meshcore_build_status_json(json_buf, sizeof(json_buf), &sj);
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
return;
}
mqtt_publisher_enqueue(_uplink_status_topic, json_buf, json_len);
}
#endif
/* MQTT uplink methods (saveUplinkCreds / handleUplinkCommand /
* publishUplinkPacket / publishUplinkStatus) live in app/RepeaterUplink.cpp,
* compiled only when CONFIG_ZEPHCORE_REPEATER_UPLINK && CONFIG_MQTT_LIB.
* The uplink init (WiFi/MQTT start + topic strings) stays in begin() above. */
void RepeaterMesh::loop() {
mesh::Mesh::loop();
+6
View File
@@ -132,6 +132,12 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks {
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);
/* Region-definition CLI (defined in app/RepeaterRegionCLI.cpp).
* handleRegionLoadLine: a continuation line during `region load`.
* handleRegionCommand: a `region ...` command. */
void handleRegionLoadLine(char* command, char* reply);
void handleRegionCommand(char* command, char* reply);
protected:
uint8_t getDutyCyclePercent() const override {
/* Arduino formula: duty% = 100 / (af + 1). af=0 → 100%, af=9 → 10%. */
+234
View File
@@ -0,0 +1,234 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RepeaterMesh region-definition CLI the `region ...` command family and the
* `region load` continuation-line parser.
*
* Split out of RepeaterMesh.cpp for readability. Defines two RepeaterMesh
* methods invoked from RepeaterMesh::handleCommand(); the file-static parser
* helpers below are used only by this command family.
*/
#include "RepeaterMesh.h"
#include <mesh/Utils.h>
#include <helpers/TxtDataHelpers.h>
#include <stdio.h>
#include <string.h>
/* ---------- region def helpers ---------- */
static char* skipSpaces(char* s) { while (*s == ' ') s++; return s; }
static void rtrimSpaces(char* s) { char* e = s + strlen(s); while (e > s && e[-1] == ' ') *--e = '\0'; }
static char* takeToken(char** cursor) {
char* p = skipSpaces(*cursor);
if (*p == '\0') { *cursor = p; return nullptr; }
char* tok = p;
while (*p && *p != ' ') p++;
if (*p) *p++ = '\0';
*cursor = p;
return tok;
}
static char* splitNameJump(char* tok) {
for (char* q = tok; *q; q++) {
if (*q == '|' || *q == ',') {
*q = '\0';
char* jump = skipSpaces(q + 1);
rtrimSpaces(jump);
return jump;
}
}
return nullptr;
}
static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) {
char* jump = splitNameJump(tok);
char* name = skipSpaces(tok);
if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; }
if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; }
RegionEntry* r = map->putRegion(name, (*cursor)->id);
if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; }
r->flags = 0;
if (jump) {
RegionEntry* j = map->findByNamePrefix(jump);
if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; }
*cursor = j;
} else {
*cursor = r;
}
return true;
}
/* ---------------------------------------- */
void RepeaterMesh::handleRegionLoadLine(char* command, char* reply) {
if (StrHelper::isBlank(command)) {
region_map = temp_map;
region_load_active = false;
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
} else {
char* np = command;
while (*np == ' ') np++;
int indent = np - command;
char* ep = np;
while (RegionMap::is_name_char(*ep)) ep++;
if (*ep) { *ep++ = 0; }
while (*ep && *ep != 'F') ep++;
if (indent > 0 && indent < 8 && strlen(np) > 0) {
auto parent = load_stack[indent - 1];
if (parent) {
auto old = region_map.findByName(np);
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0);
if (nw) {
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD);
load_stack[indent] = nw;
}
}
}
reply[0] = 0;
}
}
void RepeaterMesh::handleRegionCommand(char* command, char* reply) {
reply[0] = 0;
// `region def`: cursor-walk bulk region builder — must run before parseTextParts
// mutates and truncates the buffer to 4 segments.
char* cmd = skipSpaces(command);
if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) {
char* payload = skipSpaces(cmd + 10);
rtrimSpaces(payload);
if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); goto region_done; }
RegionEntry* cursor = &region_map.getWildcard();
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
if (!processRegionDefSegment(&region_map, tok, &cursor, reply)) goto region_done;
}
region_map.exportTo(reply, 160);
goto region_done;
}
{
const char* parts[4];
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
if (n == 1) {
region_map.exportTo(reply, 160);
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
temp_map.resetFrom(region_map);
memset(load_stack, 0, sizeof(load_stack));
load_stack[0] = &temp_map.getWildcard();
region_load_active = true;
} else if (n >= 2 && strcmp(parts[1], "save") == 0) {
_prefs.discovery_mod_timestamp = getRTCClock()->getCurrentTime();
savePrefs();
bool success = region_map.save(_store->getRegionsPath());
strcpy(reply, success ? "OK" : "Err - save failed");
} else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags &= ~REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
region->flags |= REGION_DENY_FLOOD;
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "get") == 0) {
auto region = region_map.findByNamePrefix(parts[2]);
if (region) {
auto parent = region_map.findById(region->parent);
if (parent && parent->id != 0) {
sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
} else {
sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
}
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n >= 3 && strcmp(parts[1], "home") == 0) {
auto home = region_map.findByNamePrefix(parts[2]);
if (home) {
region_map.setHomeRegion(home);
sprintf(reply, " home is now %s", home->name);
} else {
strcpy(reply, "Err - unknown region");
}
} else if (n == 2 && strcmp(parts[1], "home") == 0) {
auto home = region_map.getHomeRegion();
sprintf(reply, " home is %s", home ? home->name : "*");
} else 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) {
strcpy(reply, "Err - unknown parent");
} else {
auto region = region_map.putRegion(parts[2], parent->id);
if (region == nullptr) {
strcpy(reply, "Err - unable to put");
} else {
region->flags = 0; // New default: enable flood
strcpy(reply, "OK - (flood allowed)");
}
}
} else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
auto region = region_map.findByName(parts[2]);
if (region) {
if (region_map.removeRegion(*region)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - not empty");
}
} else {
strcpy(reply, "Err - not found");
}
} else if (n >= 3 && strcmp(parts[1], "list") == 0) {
uint8_t mask = 0;
bool invert = false;
if (strcmp(parts[2], "allowed") == 0) {
mask = REGION_DENY_FLOOD;
invert = false;
} else if (strcmp(parts[2], "denied") == 0) {
mask = REGION_DENY_FLOOD;
invert = true;
} else {
strcpy(reply, "Err - use 'allowed' or 'denied'");
return;
}
int len = region_map.exportNamesTo(reply, 160, mask, invert);
if (len == 0) {
strcpy(reply, "-none-");
}
} else {
strcpy(reply, "Err - ??");
}
} // end parseTextParts scope
region_done:;
}
+203
View File
@@ -0,0 +1,203 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RepeaterMesh MQTT uplink WiFi/MQTT telemetry publishing for repeaters.
*
* Split out of RepeaterMesh.cpp for readability. Defines the RepeaterMesh
* uplink methods; the uplink *init* (WiFi/MQTT start, topic strings) stays in
* RepeaterMesh::begin(). Compiled only when both CONFIG_ZEPHCORE_REPEATER_UPLINK
* and CONFIG_MQTT_LIB are set added to target_sources in that CMake branch.
*/
#include "RepeaterMesh.h"
#if IS_ENABLED(CONFIG_ZEPHCORE_REPEATER_UPLINK) && IS_ENABLED(CONFIG_MQTT_LIB)
#include <mesh/Utils.h>
#include <helpers/MeshcoreJson.h>
#include <helpers/TxtDataHelpers.h>
#include <adapters/radio/LoRaRadioBase.h>
#include "observer_creds.h"
#include <ZephyrWiFiStation.h>
#include <ZephyrMQTTPublisher.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool RepeaterMesh::saveUplinkCreds()
{
if (!_store) return false;
return observer_creds_save(&_uplink_creds, _store->getBasePath());
}
bool RepeaterMesh::handleUplinkCommand(const char *command, char *reply)
{
if (memcmp(command, "get uplink.", 11) == 0) {
const char *key = command + 11;
if (strcmp(key, "enable") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", isUplinkEnabled() ? "on" : "off");
} else if (strcmp(key, "wifi.ssid") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.wifi_ssid[0] ? _uplink_creds.wifi_ssid : "(not set)");
} else if (strcmp(key, "mqtt.host") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_host[0] ? _uplink_creds.mqtt_host : "(not set)");
} else if (strcmp(key, "mqtt.port") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)_uplink_creds.mqtt_port);
} else if (strcmp(key, "mqtt.tls") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)_uplink_creds.mqtt_tls);
} else if (strcmp(key, "mqtt.user") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_user[0] ? _uplink_creds.mqtt_user : "(not set)");
} else if (strcmp(key, "mqtt.iata") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _uplink_creds.mqtt_iata[0] ? _uplink_creds.mqtt_iata : "(not set)");
} else if (strcmp(key, "status") == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> enabled=%s wifi=%s mqtt=%s reboot_required=%s",
isUplinkEnabled() ? "yes" : "no",
zc_wifi_station_is_connected() ? "up" : "down",
mqtt_publisher_is_connected() ? "up" : "down",
_uplink_reboot_required ? "yes" : "no");
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: uplink.%s", key);
}
return true;
}
if (memcmp(command, "set uplink.", 11) == 0) {
const char *cfg = command + 11;
const char *val = strchr(cfg, ' ');
if (!val) {
strcpy(reply, "Error: value required");
return true;
}
int key_len = (int)(val - cfg);
val++;
if (key_len == 6 && memcmp(cfg, "enable", 6) == 0) {
if (strcmp(val, "on") == 0) {
setUplinkEnabled(true);
} else if (strcmp(val, "off") == 0) {
setUplinkEnabled(false);
} else {
strcpy(reply, "Error: must be on or off");
return true;
}
} else if (key_len == 9 && memcmp(cfg, "wifi.ssid", 9) == 0) {
StrHelper::strncpy(_uplink_creds.wifi_ssid, val, sizeof(_uplink_creds.wifi_ssid));
} else if (key_len == 8 && memcmp(cfg, "wifi.psk", 8) == 0) {
StrHelper::strncpy(_uplink_creds.wifi_psk, val, sizeof(_uplink_creds.wifi_psk));
} else if (key_len == 9 && memcmp(cfg, "mqtt.host", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_host, val, sizeof(_uplink_creds.mqtt_host));
} else if (key_len == 9 && memcmp(cfg, "mqtt.port", 9) == 0) {
int port = atoi(val);
if (port < 1 || port > 65535) {
strcpy(reply, "Error: port range 1-65535");
return true;
}
_uplink_creds.mqtt_port = (uint16_t)port;
} else if (key_len == 8 && memcmp(cfg, "mqtt.tls", 8) == 0) {
_uplink_creds.mqtt_tls = (atoi(val) != 0) ? 1 : 0;
} else if (key_len == 9 && memcmp(cfg, "mqtt.user", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_user, val, sizeof(_uplink_creds.mqtt_user));
} else if (key_len == 13 && memcmp(cfg, "mqtt.password", 13) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_password, val, sizeof(_uplink_creds.mqtt_password));
} else if (key_len == 9 && memcmp(cfg, "mqtt.iata", 9) == 0) {
StrHelper::strncpy(_uplink_creds.mqtt_iata, val, sizeof(_uplink_creds.mqtt_iata));
} else {
snprintf(reply, CLI_REPLY_SIZE, "unknown config: uplink.%.*s", key_len, cfg);
return true;
}
if (!saveUplinkCreds()) {
strcpy(reply, "Error: save failed");
return true;
}
markUplinkRebootRequired();
strcpy(reply, "OK - reboot to apply");
return true;
}
return false;
}
void RepeaterMesh::publishUplinkPacket(mesh::Packet *pkt)
{
if (!isUplinkEnabled() || !mqtt_publisher_is_connected()) return;
if (_uplink_packets_topic[0] == '\0') return;
char raw_hex[MAX_TRANS_UNIT * 2 + 1];
mesh::Utils::toHex(raw_hex, _uplink_last_raw, _uplink_last_raw_len);
raw_hex[_uplink_last_raw_len * 2] = '\0';
uint8_t hash_bytes[MAX_HASH_SIZE];
char hash_hex[MAX_HASH_SIZE * 2 + 1];
pkt->calculatePacketHash(hash_bytes);
mesh::Utils::toHex(hash_hex, hash_bytes, MAX_HASH_SIZE);
hash_hex[MAX_HASH_SIZE * 2] = '\0';
uint32_t now_epoch = getRTCClock()->getCurrentTime();
static char json_buf[1024];
struct MeshcorePacketJson pj = {
_prefs.node_name,
_uplink_pubkey_hex,
now_epoch,
_uplink_last_raw_len,
(unsigned)pkt->getPayloadType(),
pkt->isRouteDirect() ? "D" : "F",
(unsigned)pkt->payload_len,
raw_hex,
(int)pkt->getSNR(),
(int)_uplink_last_rssi,
(int)(_uplink_last_score * 1000.0f),
hash_hex,
};
int json_len = meshcore_build_packet_json(json_buf, sizeof(json_buf), &pj);
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
return;
}
mqtt_publisher_enqueue(_uplink_packets_topic, json_buf, json_len);
}
void RepeaterMesh::publishUplinkStatus(const char *status)
{
if (!isUplinkEnabled()) return;
if (_uplink_status_topic[0] == '\0') return;
auto& radio_driver = *static_cast<mesh::LoRaRadioBase *>(_radio);
uint32_t now_epoch = getRTCClock()->getCurrentTime();
char radio_buf[48];
snprintf(radio_buf, sizeof(radio_buf), "%.3f,%.1f,%u,%u",
(double)_prefs.freq, (double)_prefs.bw,
(unsigned)_prefs.sf, (unsigned)_prefs.cr);
static char json_buf[768];
struct MeshcoreStatusJson sj = {
status,
now_epoch,
_prefs.node_name,
_uplink_pubkey_hex,
radio_buf,
#ifdef CONFIG_ZEPHCORE_BOARD_NAME
CONFIG_ZEPHCORE_BOARD_NAME,
#else
"unknown",
#endif
FIRMWARE_VERSION,
(unsigned)_board.getBattMilliVolts(),
(unsigned)(uptime_millis / 1000),
(unsigned)_err_flags,
(unsigned)_mgr->getOutboundTotal(),
_radio->getNoiseFloor(),
(unsigned)(getTotalAirTime() / 1000),
(unsigned)(getReceiveAirTime() / 1000),
(unsigned)radio_driver.getPacketsRecvErrors(),
};
int json_len = meshcore_build_status_json(json_buf, sizeof(json_buf), &sj);
if (json_len <= 0 || json_len >= (int)sizeof(json_buf)) {
return;
}
mqtt_publisher_enqueue(_uplink_status_topic, json_buf, json_len);
}
#endif /* CONFIG_ZEPHCORE_REPEATER_UPLINK && CONFIG_MQTT_LIB */