mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 21:08:19 +00:00
refactor: split RepeaterMesh into RepeaterUplink + RepeaterRegionCLI
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = ®ion_map.getWildcard();
|
||||
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
|
||||
if (!processRegionDefSegment(®ion_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]) : ®ion_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();
|
||||
|
||||
@@ -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%. */
|
||||
|
||||
@@ -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 = ®ion_map.getWildcard();
|
||||
for (char* tok; (tok = takeToken(&payload)) != nullptr; ) {
|
||||
if (!processRegionDefSegment(®ion_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]) : ®ion_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:;
|
||||
}
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user