diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 1d466c6..b58a9b2 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -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 diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 594c66c..76ba070 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -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], "") == 0) { - region_map.setDefaultRegion(nullptr); - memset(default_scope.key, 0, sizeof(default_scope.key)); - region_map.save(_store->getRegionsPath()); // persist in one atomic step - sprintf(reply, " default scope is now "); - } else { - auto def = region_map.findByNamePrefix(parts[2]); - if (def == nullptr) { - def = region_map.putRegion(parts[2], 0); // auto-create the default region - } - if (def) { - def->flags = 0; // make sure allow flood enabled - region_map.setDefaultRegion(def); - region_map.getTransportKeysFor(*def, &default_scope, 1); - region_map.save(_store->getRegionsPath()); // persist in one atomic step - sprintf(reply, " default scope is now %s", def->name); - } else { - strcpy(reply, "Err - region table full"); - } - } - } else if (n == 2 && strcmp(parts[1], "default") == 0) { - auto def = region_map.getDefaultRegion(); - sprintf(reply, " default scope is %s", def ? def->name : ""); - } else if (n >= 3 && strcmp(parts[1], "put") == 0) { - auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); - if (parent == nullptr) { - 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(); diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index f5c0730..d4ea3b2 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -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%. */ diff --git a/zephcore/app/RepeaterRegionCLI.cpp b/zephcore/app/RepeaterRegionCLI.cpp new file mode 100644 index 0000000..51cbbf1 --- /dev/null +++ b/zephcore/app/RepeaterRegionCLI.cpp @@ -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 +#include + +#include +#include + +/* ---------- 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], "") == 0) { + region_map.setDefaultRegion(nullptr); + memset(default_scope.key, 0, sizeof(default_scope.key)); + region_map.save(_store->getRegionsPath()); // persist in one atomic step + sprintf(reply, " default scope is now "); + } else { + auto def = region_map.findByNamePrefix(parts[2]); + if (def == nullptr) { + def = region_map.putRegion(parts[2], 0); // auto-create the default region + } + if (def) { + def->flags = 0; // make sure allow flood enabled + region_map.setDefaultRegion(def); + region_map.getTransportKeysFor(*def, &default_scope, 1); + region_map.save(_store->getRegionsPath()); // persist in one atomic step + sprintf(reply, " default scope is now %s", def->name); + } else { + strcpy(reply, "Err - region table full"); + } + } + } else if (n == 2 && strcmp(parts[1], "default") == 0) { + auto def = region_map.getDefaultRegion(); + sprintf(reply, " default scope is %s", def ? def->name : ""); + } else if (n >= 3 && strcmp(parts[1], "put") == 0) { + auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + if (parent == nullptr) { + 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:; +} diff --git a/zephcore/app/RepeaterUplink.cpp b/zephcore/app/RepeaterUplink.cpp new file mode 100644 index 0000000..39f4902 --- /dev/null +++ b/zephcore/app/RepeaterUplink.cpp @@ -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 +#include +#include +#include +#include "observer_creds.h" +#include +#include + +#include +#include +#include + +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(_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 */