Add saved node and region discovery response switches

This commit is contained in:
mikecarper committed 2026-10-24 20:37:51 -07:00
1 parent 1ab4091a05
commit 4fd3358a45
15 files changed
+705 -5

No files matched your search

+1
View File
@@ -577,6 +577,7 @@ jobs:
python3 -B test/test_companion_ethernet.py -v
python3 -B test/test_companion_discovery.py -v
python3 -B test/test_repeater_discovery.py -v
python3 -B test/test_repeater_discovery_settings.py -v
python3 -B test/test_companion_discovery_correlation.py -v
python3 -B test/test_pico_ble_queues.py -v
python3 -B test/test_companion_preferences_transaction.py -v
+2
View File
@@ -150,6 +150,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes |
| Neighbors | [`neighbor.remove [pubkey_prefix]`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table; omit prefix to clear all | Yes | Yes | Yes |
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes |
| Neighbors | [`get/set discovery.node.responses`; `get/set discovery.region.responses`](cli_commands.md#enable-or-disable-discovery-responses) | Repeater only; newer source, not published 1.18.0.1 binaries; saved, default on, no reboot | Yes | Yes | Yes |
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No |
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | All full-parser text CLI roles | Yes | Yes | Yes |
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local connection | Local | Local | Local |
@@ -325,6 +326,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes | Yes | Yes |
| Neighbors | [`neighbor.remove [pubkey_prefix]`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table; omit prefix to clear all | Yes | Yes | Yes | Yes | Yes |
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes | Yes | Yes |
| Neighbors | [`get/set discovery.node.responses`; `get/set discovery.region.responses`](cli_commands.md#enable-or-disable-discovery-responses) | Repeater only; newer source, not published 1.18.0.1 binaries; saved, default on, no reboot | Yes | Yes | Yes | Yes | Yes |
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No |
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | Full parser | Yes | Yes | Yes | Yes | Yes |
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local connection | Local | Local | Local | Local | Local |
+48
View File
@@ -636,6 +636,44 @@ require MQTT or PSRAM.
---
### Enable or disable discovery responses
**Availability:** Repeater firmware in newer source builds. These switches are
not included in the existing published **1.18.0.1** binaries. They do not require
MQTT or PSRAM and are not Companion, Room Server, or Sensor settings.
```text
get discovery.node.responses
set discovery.node.responses on
set discovery.node.responses off
get discovery.region.responses
set discovery.region.responses on
set discovery.region.responses off
```
| Setting | Default | What `off` does |
| --- | --- | --- |
| `discovery.node.responses` | `on` | Stops replies to normal zero-hop node-discovery requests. |
| `discovery.region.responses` | `on` | Stops replies to anonymous region/scope queries, even while adverts remain enabled. |
The two choices are independent. They apply immediately after a successful
save and survive reboot; no reboot is needed. Fresh installations and upgrades
without saved choices keep both on, preserving the previous behavior. Use a
local console or authenticated administrator CLI to change them.
Enabling node responses still honors the existing hidden-node rule: when both
`advert.interval` and `flood.advert.interval` are `0`, the repeater does not
answer normal discovery requests. Enabling a response switch also retains the
normal request-validation and rate limits.
These switches do not stop the node's own adverts, outgoing
`discover.neighbors` or `discover.scopes` commands, or automatic
[MQTT neighbor discovery](#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature).
They do not restrict an authorized administrator's `region` CLI commands.
Use `set mqtt.neighbors off` to stop the periodic outgoing MQTT discovery pass.
---
### Discover neighbor scopes (MQTT observer, neighbors feature)
Refreshes the zero-hop neighbor table, then queries each neighbor for its region
@@ -649,6 +687,10 @@ scopes and publishes the assembled table to the MQTT `neighbors` topic once.
Elsewhere it replies `Err - neighbors not enabled in this build`. If a
`discover.neighbors` refresh is already in flight, the scope pass is queued behind it.
This is an outgoing query. A repeater's separate
[discovery response switches](#enable-or-disable-discovery-responses) control
whether that repeater answers other nodes' requests.
---
## Statistics
@@ -6399,6 +6441,12 @@ default-off updates keep the existing record size.
> While enabled, `get mqtt.status` gains a trailing `nbr: <next>/<last>` field
> (time to next publish, and how the last publish went).
This setting controls this node's outgoing periodic discovery and MQTT
publication. It does not control replies to requests from other nodes. Newer
repeater source builds have separate
[node and region response switches](#enable-or-disable-discovery-responses),
both defaulting to on; those switches are not in the published 1.18.0.1 images.
---
#### View or change the neighbors publish interval (MQTT observer, neighbors feature)
+45 -2
View File
@@ -748,6 +748,7 @@ static bool hasCompleteAnonReplyPath(const uint8_t* data, size_t data_len) {
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp,
const uint8_t* data, size_t data_len) {
if (!(_prefs.discovery_responses & mesh::discovery::REGION_RESPONSES)) return 0;
if (!hasCompleteAnonReplyPath(data, data_len)) return 0;
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
@@ -2832,6 +2833,8 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
if (data[4] == ANON_REQ_TYPE_REGIONS
&& !(_prefs.discovery_responses & mesh::discovery::REGION_RESPONSES)) return;
const bool login_request = data[4] == 0 || data[4] >= ' ';
uint8_t reply_len;
@@ -3579,6 +3582,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
&& (_prefs.discovery_responses & mesh::discovery::NODE_RESPONSES)
&& !isHiddenNode() // this node wishes to NOT be discoverable
&& discover_limiter.allow(rtc_clock.getCurrentTime())
) {
@@ -5585,6 +5589,43 @@ void MyMesh::dumpLogFile() {
void MyMesh::dumpLogFile() { }
#endif
bool MyMesh::handleDiscoveryResponseCommand(const char* command, char* reply, size_t capacity) {
const bool get = strncmp(command, "get ", 4) == 0;
const bool set = strncmp(command, "set ", 4) == 0;
if (!get && !set) return false;
const char* name = command + 4;
const char* setting;
uint8_t flag;
if (strncmp(name, "discovery.node.responses", 24) == 0) {
setting = "discovery.node.responses";
flag = mesh::discovery::NODE_RESPONSES;
} else if (strncmp(name, "discovery.region.responses", 26) == 0) {
setting = "discovery.region.responses";
flag = mesh::discovery::REGION_RESPONSES;
} else {
return false;
}
const char* value = name + strlen(setting);
if (*value && *value != ' ' && *value != '\t') return false;
while (*value == ' ' || *value == '\t') ++value;
if ((get && *value) || (set && strcmp(value, "on") && strcmp(value, "off"))) {
snprintf(reply, capacity, "Err - use get %s or set %s on|off", setting, setting);
} else if (get) {
snprintf(reply, capacity, "> %s", (_prefs.discovery_responses & flag) ? "on" : "off");
} else {
const uint8_t previous = _prefs.discovery_responses;
if (strcmp(value, "on") == 0) _prefs.discovery_responses |= flag;
else _prefs.discovery_responses &= ~flag;
if (_cli.saveCommonPrefs()) {
snprintf(reply, capacity, "OK - %s %s", setting, value);
} else {
_prefs.discovery_responses = previous;
snprintf(reply, capacity, "Err - save unconfirmed; reboot may differ");
}
}
return true;
}
bool MyMesh::handleGuestCommand(const char* command, char* reply, size_t capacity) {
const bool get = strncmp(command, "get guest", 9) == 0;
const bool set = strncmp(command, "set guest", 9) == 0;
@@ -12762,7 +12803,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
return;
}
}
if (handleGuestCommand(command, reply, 157)
if (handleDiscoveryResponseCommand(command, reply, 157)
|| handleGuestCommand(command, reply, 157)
|| mesh::cli::handleGuestsGet(acl, command, reply, 157)
|| mesh::cli::handleLoginsGet(login_history, command, reply, 157)) return;
_cli.handleCommand(sender_timestamp, command, reply);
@@ -12785,7 +12827,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
}
}
if (handleGuestCommand(command, reply, 157)
if (handleDiscoveryResponseCommand(command, reply, 157)
|| handleGuestCommand(command, reply, 157)
|| mesh::cli::handleGuestsGet(acl, command, reply, 157)
|| mesh::cli::handleLoginsGet(login_history, command, reply, 157)) return;
+1
View File
@@ -399,6 +399,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
mesh::GuestLoginSettings guest_login;
mesh::LoginHistory login_history;
bool handleGuestCommand(const char* command, char* reply, size_t capacity);
bool handleDiscoveryResponseCommand(const char* command, char* reply, size_t capacity);
ClientACL acl;
CommonCLI _cli;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
+17
View File
@@ -832,6 +832,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
#endif
_prefs->usb_debug_enabled = 0;
_prefs->trace_when_repeat_off = 0;
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
_prefs->ota_channel = 0;
#ifdef WITH_RS232_BRIDGE
_prefs->rs232_heartbeat = 1;
@@ -1105,6 +1106,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// from a previous load of a newer image.
_prefs->usb_debug_enabled = 0;
_prefs->trace_when_repeat_off = 0;
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
_prefs->gps_sync_interval_hours = 0;
_prefs->ota_channel = 0;
_prefs->rs232_bridge_enabled = 0;
@@ -1809,6 +1811,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
recalculateRxPowerSavingFromLevel(_prefs); // retune supported level-based timings to loaded SF/BW
// Read reply controls independently of the current UART profile. Older
// images have no record and retain both replies enabled.
_prefs->discovery_responses = mesh::discovery::readPrefs(file);
// Read the footer independently of optional legacy UART tails/profile.
#if defined(RAK_4631)
_prefs->peripherals_shared = mesh::peripherals::readCommonMode(file);
@@ -1983,6 +1988,11 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
prefs->rs232_idle_seconds);
WRITE_COMMON_PREFS_BYTES(power_tail, sizeof(power_tail));
#endif
if (prefs->discovery_responses != mesh::discovery::DEFAULT_RESPONSES) {
uint8_t discovery_tail[mesh::discovery::PREFS_SIZE];
mesh::discovery::encodePrefs(discovery_tail, prefs->discovery_responses);
WRITE_COMMON_PREFS_BYTES(discovery_tail, sizeof(discovery_tail));
}
#if defined(RAK_4631)
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
mesh::peripherals::encodeFooter(peripheral_footer, prefs->peripherals_shared);
@@ -2078,6 +2088,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
_common_save_succeeded = false;
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
if (_prefs->discovery_responses > mesh::discovery::DEFAULT_RESPONSES)
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
#if defined(ESP32_PLATFORM)
if (!mesh::esp32::cpuclock::validSelection(_prefs->cpu_freq_mhz)
|| (_prefs->cpu_freq_mhz && !_board->supportsCpuFrequencyMHz(_prefs->cpu_freq_mhz)))
@@ -2234,6 +2246,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
_prefs->rs232_idle_seconds);
file.write(power_tail, sizeof(power_tail));
#endif
if (_prefs->discovery_responses != mesh::discovery::DEFAULT_RESPONSES) {
uint8_t discovery_tail[mesh::discovery::PREFS_SIZE];
mesh::discovery::encodePrefs(discovery_tail, _prefs->discovery_responses);
file.write(discovery_tail, sizeof(discovery_tail));
}
#if defined(RAK_4631)
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
mesh::peripherals::encodeFooter(peripheral_footer, _prefs->peripherals_shared);
+5
View File
@@ -13,6 +13,7 @@
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RadioProfileCLI.h>
#include <helpers/MorseReplyHandoff.h>
#include <helpers/WirelessControl.h>
@@ -203,6 +204,9 @@ public:
// Repeater-only opt-in for routed trace diagnostics while repeat is off.
// Appended after usb_debug in /com_prefs; existing images stay disabled.
uint8_t trace_when_repeat_off = 0;
// Independent repeater reply gates: bit 0=node, bit 1=anonymous regions.
// Stored in a checked extension before the platform-specific EOF footer.
uint8_t discovery_responses = 3;
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
// /com_prefs offset 862; single-UART builds keep their compiled port here.
uint8_t bridge_uart = 0;
@@ -472,6 +476,7 @@ protected:
def("lat", node_lat);
def("lon", node_lon);
def("disc_mod", discovery_mod_timestamp);
def("disc_responses", discovery_responses);
def("ota_ch", ota_channel); // WiFi OTA release channel selector
def("radio", radio);
def("bridge", bridge);
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh {
namespace discovery {
static constexpr uint8_t NODE_RESPONSES = 1;
static constexpr uint8_t REGION_RESPONSES = 2;
static constexpr uint8_t DEFAULT_RESPONSES = NODE_RESPONSES | REGION_RESPONSES;
static constexpr size_t PREFS_SIZE = 8;
static constexpr size_t COMMON_LEGACY_SIZE = 877;
static constexpr size_t LEGACY_RS232_TAIL_SIZE = 10;
inline uint16_t checksum(const uint8_t* bytes, size_t length) {
uint16_t crc = 0xffff;
for (size_t i = 0; i < length; ++i) {
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
for (unsigned bit = 0; bit < 8; ++bit)
crc = static_cast<uint16_t>((crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1);
}
return crc;
}
// The record is omitted when both response types are enabled. Keep it before
// existing platform footers, whose readers locate those policies at EOF.
inline void encodePrefs(uint8_t* out, uint8_t mask) {
out[0] = 'D'; out[1] = 'R'; out[2] = 'P'; out[3] = 1;
out[4] = mask <= DEFAULT_RESPONSES ? mask : DEFAULT_RESPONSES;
out[5] = 0;
const uint16_t crc = checksum(out, 6);
out[6] = static_cast<uint8_t>(crc >> 8);
out[7] = static_cast<uint8_t>(crc);
}
inline bool decodePrefs(const uint8_t* in, size_t size, uint8_t& mask) {
mask = DEFAULT_RESPONSES;
if (!in || size != PREFS_SIZE || in[0] != 'D' || in[1] != 'R'
|| in[2] != 'P' || in[3] != 1 || in[4] > DEFAULT_RESPONSES || in[5] != 0
|| checksum(in, 6) != static_cast<uint16_t>((in[6] << 8) | in[7]))
return false;
mask = in[4];
return true;
}
template <typename File>
uint8_t readPrefs(File& file) {
uint8_t mask = DEFAULT_RESPONSES;
uint8_t bytes[PREFS_SIZE] = {};
const size_t size = file.size();
size_t offset = COMMON_LEGACY_SIZE;
if (size < offset + PREFS_SIZE || !file.seek(offset)
|| file.read(bytes, sizeof(bytes)) != sizeof(bytes))
return mask;
// The source image decides whether a UART power tail exists. Depending on
// this build's bridge flags would lose saved settings across role changes.
if (bytes[0] == 'R' && bytes[1] == 'S' && bytes[2] == 'P' && bytes[3] == 1) {
offset += LEGACY_RS232_TAIL_SIZE;
if (size < offset + PREFS_SIZE || !file.seek(offset)
|| file.read(bytes, sizeof(bytes)) != sizeof(bytes))
return mask;
}
decodePrefs(bytes, sizeof(bytes), mask);
return mask;
}
} // namespace discovery
} // namespace mesh
+84
View File
@@ -8,6 +8,7 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/DatagramPayloadLimits.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RoutingPolicy.h>
#include "examples/simple_repeater/RateLimiter.h"
#include "production_constants.inc"
@@ -34,6 +35,7 @@ struct MyMesh {
char node_name[32] = "Bellevue";
char owner_info[80] = "owner";
bool disable_fwd = false;
uint8_t discovery_responses = 3;
} _prefs;
struct Clock {
uint32_t now = 123456;
@@ -164,6 +166,87 @@ static void checkAnonymousHandlers() {
}
}
static void checkDiscoveryResponseSwitches() {
const mesh::Identity sender;
const uint8_t secret[PUB_KEY_SIZE] = {};
mesh::Packet request;
request.header = (PAYLOAD_TYPE_ANON_REQ << PH_TYPE_SHIFT) | ROUTE_TYPE_DIRECT;
const uint8_t path[] = {0x42, 0x11, 0x22, 0x33, 0x44};
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
MyMesh mesh;
mesh._prefs.discovery_responses = node | (region << 1);
mesh.reply_path_len = 0x02;
memset(mesh.reply_data, 0xc7, sizeof(mesh.reply_data));
const auto limiter_before = mesh.anon_limiter;
const uint8_t length = mesh.handleAnonRegionsReq(sender, 42, path, sizeof(path));
require((length != 0) == (region != 0),
"node and region discovery choices are not independent");
if (!region) {
require(mesh.reply_path_len == 0x02, "disabled region request changed reply route");
for (uint8_t byte : mesh.reply_path)
require(byte == 0xa5, "disabled region request changed reply path bytes");
for (uint8_t byte : mesh.reply_data)
require(byte == 0xc7, "disabled region request changed reply payload");
require(mesh.region_map.exports == 0, "disabled region request exported names");
// Compare future decisions from the real limiter, instead of relying
// on the private counter representation or an access-order mock.
auto expected = limiter_before;
for (unsigned count = 0; count < 10001; ++count) {
require(mesh.anon_limiter.allow(mesh.rtc_clock.now)
== expected.allow(mesh.rtc_clock.now),
"disabled region request consumed anonymous quota");
}
} else {
require(mesh.reply_path_len == path[0] && mesh.region_map.exports == 1,
"enabled region response lost the requested path or names");
}
// Exercise the real receive dispatcher too: no allocation or RF send
// may happen for a disabled query, including an already-known caller.
MyMesh dispatched;
dispatched._prefs.discovery_responses = node | (region << 1);
dispatched.acl.present = true;
dispatched.acl.client.out_path_len = 0;
dispatched.reply_path_len = 0x02;
memset(dispatched.reply_data, 0xc7, sizeof(dispatched.reply_data));
uint8_t bytes[11] = {42, 0, 0, 0, ANON_REQ_TYPE_REGIONS,
0x42, 0x11, 0x22, 0x33, 0x44, 0};
dispatched.onAnonDataRecv(&request, secret, sender, bytes, 10);
require(dispatched.allocations == unsigned(region)
&& dispatched.direct_sends == unsigned(region)
&& dispatched.flood_sends == 0 && dispatched.logins == 0,
"disabled region query generated TX or fell through to login");
if (!region) {
require(dispatched.reply_path_len == 0x02,
"disabled dispatched region query changed pending reply route");
for (uint8_t byte : dispatched.reply_path)
require(byte == 0xa5, "disabled dispatched query changed reply path bytes");
for (uint8_t byte : dispatched.reply_data)
require(byte == 0xc7, "disabled dispatched query changed reply payload");
}
// The switches apply only to public discovery. Both anonymous metadata
// services and login remain reachable with all four combinations.
for (unsigned type : {ANON_REQ_TYPE_OWNER, ANON_REQ_TYPE_BASIC}) {
MyMesh metadata;
metadata._prefs.discovery_responses = node | (region << 1);
bytes[4] = type;
metadata.onAnonDataRecv(&request, secret, sender, bytes, 10);
require(metadata.allocations == 1 && metadata.direct_sends == 1
&& metadata.logins == 0,
"discovery switches disabled owner or clock metadata");
}
MyMesh login;
login._prefs.discovery_responses = node | (region << 1);
uint8_t credentials[] = {42, 0, 0, 0, 'x', 0};
login.onAnonDataRecv(&request, secret, sender, credentials, 5);
require(login.logins == 1 && login.allocations == 1,
"discovery switches disabled administrator login");
}
}
}
static void checkAnonymousDispatch() {
const mesh::Identity sender;
const uint8_t secret[PUB_KEY_SIZE] = {};
@@ -302,5 +385,6 @@ int main() {
checkAdverts();
checkAnonymousHandlers();
checkAnonymousDispatch();
checkDiscoveryResponseSwitches();
std::puts("PASS: production protocol parser bounds");
}
+2
View File
@@ -19,6 +19,7 @@ HARNESS = r'''
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/AtomicFileWriter.h>
#include <helpers/PrefsSaveRouting.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/GpsPowerPolicy.h>
#include <helpers/radiolib/RXPowerSaving.h>
#if defined(ESP32_PLATFORM)
@@ -76,6 +77,7 @@ struct CommonCLI {
prefs.freq=910; prefs.bw=62.5f; prefs.sf=7; prefs.cr=5;
prefs.tx_power_dbm=30; prefs.primary_radio_preamble=48;
prefs.espnow_bridge_enabled=1;
prefs.discovery_responses=3;
prefs.gps_sync_interval_hours=336;
prefs.rx_ps_rx_us=111; prefs.rx_ps_sleep_us=222;
}
+1
View File
@@ -35,6 +35,7 @@ class TestFS : public MemoryFS {
#include <helpers/HilStartupTrace.h>
#include <helpers/MQTTPrefsAtomicStore.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/GpsPowerPolicy.h>
#if defined(ESP32_PLATFORM)
+2
View File
@@ -248,6 +248,7 @@ inline char* utoa(unsigned value,char* out,int base){
#include <vector>
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/bridges/RS232PowerSettings.h>
#include <helpers/DiscoveryResponsePrefs.h>
@CONSTANTS@
struct NodePrefs { @FIELDS@ };
@MARKER@
@@ -269,6 +270,7 @@ void non_mqtt(File& file,NodePrefs* _prefs){ @SCALAR@ }
int main(){
using namespace mesh::esp32::cpuclock;
NodePrefs prefs{};prefs.freq=910.525f;prefs.bw=62.5f;prefs.sf=7;prefs.cr=5;
prefs.discovery_responses=3;
prefs.powersaving_enabled=1;strcpy(prefs.node_name,"unchanged prefix");
File legacy;assert(writeCommonPrefsImage(legacy,&prefs));
File nonlegacy;non_mqtt(nonlegacy,&prefs);assert(nonlegacy.bytes==legacy.bytes);
+66 -1
View File
@@ -24,8 +24,14 @@ HARNESS = r'''
#include <initializer_list>
#include "examples/simple_repeater/RateLimiter.h"
#include "src/RadioProfiles.h"
#include "src/helpers/DiscoveryResponsePrefs.h"
@CONSTANTS@
#define MESH_DEBUG_PRINTLN(...) ((void)0)
struct CountedLimiter : RateLimiter {
using RateLimiter::RateLimiter;
unsigned calls = 0;
bool allow(uint32_t now) { ++calls; return RateLimiter::allow(now); }
};
static uint32_t now_ms = 100;
bool millisHasNowPassed(uint32_t deadline) {
return static_cast<int32_t>(now_ms - deadline) > 0;
@@ -71,13 +77,14 @@ struct MyMesh {
struct {
uint16_t advert_interval = 30, flood_advert_interval = 0;
bool disable_fwd = false;
uint8_t discovery_responses = 3;
uint32_t discovery_mod_timestamp = 500;
} _prefs;
struct Clock {
uint32_t now = 1000;
uint32_t getCurrentTime() const { return now; }
} rtc_clock;
RateLimiter @LIMITER@;
CountedLimiter @LIMITER@;
mesh::Radio radio;
mesh::Radio* _radio = &radio;
mesh::Identity self_id, neighbour;
@@ -206,6 +213,53 @@ static void gates() {
unrelated.onControlDataRecv(&packet);
require(unrelated.create_attempts == 0, "unrelated control type produced a reply");
}
static void response_switches() {
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
for (unsigned flags = 0; flags < 4; ++flags) {
MyMesh receiver;
receiver._prefs.discovery_responses = node | (region << 1);
receiver.response.payload_len = 17;
receiver.response.tx_radio = mesh::RADIO_TX_BOTH;
std::memset(receiver.response.payload, 0xa5, sizeof(receiver.response.payload));
auto packet = request(flags & 1, 0, false, flags & 2);
for (unsigned count = 0; count < 8; ++count) {
receiver.onControlDataRecv(&packet);
}
if (node) {
require(receiver.sent_packets == 4 && receiver.create_attempts == 4,
"region response choice affected normal node discovery");
} else {
require(receiver.discover_limiter.calls == 0,
"disabled node discovery consumed rate-limit quota");
require(receiver.sent_packets == 0 && receiver.create_attempts == 0,
"disabled node discovery allocated or transmitted a reply");
require(receiver.response.payload_len == 17
&& receiver.response.tx_radio == mesh::RADIO_TX_BOTH,
"disabled node discovery changed pending reply metadata");
for (uint8_t byte : receiver.response.payload) {
require(byte == 0xa5, "disabled node discovery changed reply bytes");
}
// Turning the live switch back on must admit the complete original
// quota: rejected requests did not secretly exhaust the limiter.
receiver._prefs.discovery_responses |= 1;
for (unsigned count = 1; count <= 5; ++count) {
receiver.onControlDataRecv(&packet);
require(receiver.sent_packets == (count < 5 ? count : 4),
"disabled requests exhausted the later enabled quota");
}
}
// Enabling responses must not override the legacy hidden-node rule.
MyMesh hidden;
hidden._prefs.discovery_responses = node | (region << 1);
hidden._prefs.advert_interval = hidden._prefs.flood_advert_interval = 0;
hidden.onControlDataRecv(&packet);
require(hidden.create_attempts == 0 && hidden.discover_limiter.calls == 0,
"response setting bypassed hidden-node protection");
}
}
}
}
static void wire() {
for (bool prefix : {false, true}) {
MyMesh receiver;
@@ -333,7 +387,10 @@ static void radio2_disabled() {
}
}
static void received() {
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
MyMesh receiver;
receiver._prefs.discovery_responses = node | (region << 1);
receiver._prefs.advert_interval = 0;
receiver._prefs.flood_advert_interval = 0;
receiver.pending_discover_tag = 0x78563412;
@@ -363,11 +420,15 @@ static void received() {
receiver.onControlDataRecv(&packet);
require(receiver.neighbours == 1 && receiver.pending_discover_tag == 0,
"expired discovery response retained its pending request");
now_ms = 100;
}
}
}
int main(int argc, char** argv) {
if (argc != 2) return 2;
if (std::strcmp(argv[1], "policy") == 0) policy();
else if (std::strcmp(argv[1], "gates") == 0) gates();
else if (std::strcmp(argv[1], "response_switches") == 0) response_switches();
else if (std::strcmp(argv[1], "wire") == 0) wire();
else if (std::strcmp(argv[1], "radio2_wire") == 0) radio2_wire();
else if (std::strcmp(argv[1], "radio2_disabled") == 0) radio2_disabled();
@@ -442,6 +503,10 @@ class RepeaterDiscoveryTests(unittest.TestCase):
checked = self.run_case("wire")
self.assertEqual(checked.returncode, 0, checked.stderr)
def test_saved_response_switches_are_independent_and_reject_before_quota(self):
checked = self.run_case("response_switches")
self.assertEqual(checked.returncode, 0, checked.stderr)
def test_hidden_nodes_still_process_matching_discovery_responses(self):
checked = self.run_case("received")
self.assertEqual(checked.returncode, 0, checked.stderr)
+355
View File
@@ -0,0 +1,355 @@
"""Execute saved repeater discovery controls with real CLI and prefs images."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_repeater_trace_repeat import production_harness
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
MAIN = r'''
struct MyMesh {
CommonCLI _cli;
NodePrefs& _prefs = _cli.prefs;
bool handleDiscoveryResponseCommand(const char*, char*, size_t);
};
@HANDLER@
static void run(MyMesh& mesh, const char* command, const char* expected) {
char reply[160] = {};
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, expected) == 0);
}
static std::vector<uint8_t> image(CommonCLI& cli) {
Capture capture;
assert(writeCommonPrefsImage(capture, &cli.prefs));
return capture.bytes;
}
static void assertLoaded(const std::vector<uint8_t>& bytes, uint8_t expected) {
CommonCLI reader;
reader.prefs.discovery_responses = uint8_t(expected ^ 3);
reader.fs.files["/com_prefs"] = bytes;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.discovery_responses == expected);
assert(reader.prefs.freq == 910 && reader.prefs.primary_radio_preamble == 48);
}
static void cliChoices() {
for (uint8_t initial : {0, 1, 2, 3}) {
for (uint8_t bit : {1, 2}) {
for (bool enabled : {false, true}) {
MyMesh mesh;
mesh._prefs.discovery_responses = initial;
const char* name = bit == 1 ? "discovery.node.responses" : "discovery.region.responses";
char command[80], expected[100];
snprintf(command, sizeof(command), "get %s", name);
run(mesh, command, (initial & bit) ? "> on" : "> off");
snprintf(command, sizeof(command), "set %s %s", name, enabled ? "on" : "off");
snprintf(expected, sizeof(expected), "OK - %s %s", name, enabled ? "on" : "off");
run(mesh, command, expected);
const uint8_t mask = enabled ? uint8_t(initial | bit) : uint8_t(initial & ~bit);
assert(mesh._prefs.discovery_responses == mask);
assert((mesh._prefs.discovery_responses & (bit ^ 3)) == (initial & (bit ^ 3)));
assert(mesh._cli._common_save_succeeded);
assert(mesh._cli.fs.files["/com_prefs"] == image(mesh._cli));
assertLoaded(mesh._cli.fs.files["/com_prefs"], mask);
snprintf(command, sizeof(command), "get %s", name);
run(mesh, command, enabled ? "> on" : "> off");
}
}
}
MyMesh mesh;
assert(mesh._prefs.discovery_responses == 3);
run(mesh, "set discovery.node.responses\t \toff", "OK - discovery.node.responses off");
run(mesh, "set discovery.region.responses off", "OK - discovery.region.responses off");
assert(mesh._prefs.discovery_responses == 0);
}
static void invalidCommands() {
for (const char* name : {"discovery.node.responses", "discovery.region.responses"}) {
for (const char* value : {"", " ", "true", "false", "1", "0", "ON", "Off",
"on reboot", "off reboot", "on junk", "off ", "on\t", "on\n", "on;off"}) {
MyMesh mesh;
mesh._prefs.discovery_responses = 1;
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
const auto before = mesh._cli.fs.files["/com_prefs"];
char command[100], reply[160] = {};
snprintf(command, sizeof(command), "set %s %s", name, value);
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strncmp(reply, "Err - use ", 10) == 0);
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
snprintf(command, sizeof(command), "get %s on", name);
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strncmp(reply, "Err - use ", 10) == 0);
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
}
}
for (const char* command : {"get discovery.node.responsesx", "set discovery.region.responses_extra off",
"get discovery.node.response", "set discovery.region.responses/off", "discovery.node.responses on"}) {
MyMesh mesh;
const auto before = mesh._cli.fs.files;
char reply[160] = "unchanged";
assert(!mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0 && mesh._prefs.discovery_responses == 3);
assert(mesh._cli.fs.files == before);
}
}
static void failedSaves() {
for (uint8_t initial : {0, 1, 2, 3}) {
for (uint8_t bit : {1, 2}) {
for (unsigned fault : {0, 1, 2, 3}) {
MyMesh mesh;
mesh._prefs.discovery_responses = initial;
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
assert(mesh._cli._common_save_succeeded);
const auto before = mesh._cli.fs.files["/com_prefs"];
if (fault == 0) mesh._cli.fs.fail_write = true;
if (fault == 1) {
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
#ifdef WITH_RS232_BRIDGE
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
#endif
;
mesh._cli.fs.fail_write_after = (initial ^ bit) == 3 ? offset - 1 : offset + 4;
}
if (fault == 2) mesh._cli.fs.fail_read_open = true;
if (fault == 3) mesh._cli.fs.fail_rename = 1;
char command[100];
snprintf(command, sizeof(command), "set discovery.%s.responses %s",
bit == 1 ? "node" : "region", (initial & bit) ? "off" : "on");
run(mesh, command, "Err - save unconfirmed; reboot may differ");
assert(mesh._prefs.discovery_responses == initial);
assert(mesh._cli.fs.files["/com_prefs"] == before);
}
}
}
}
static void persistence() {
CommonCLI cli;
const auto baseline = image(cli);
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
#ifdef WITH_RS232_BRIDGE
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
#endif
;
assert(cli.prefs.discovery_responses == 3);
for (uint8_t mask : {0, 1, 2, 3}) {
cli.prefs.discovery_responses = mask;
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto saved = image(cli);
assert(cli.fs.files["/com_prefs"] == saved);
assert(saved.size() == baseline.size() + (mask == 3 ? 0 : mesh::discovery::PREFS_SIZE));
assert(std::equal(saved.begin(), saved.begin() + offset, baseline.begin()));
assertLoaded(saved, mask);
if (mask == 3) continue;
uint8_t decoded = 3;
assert(mesh::discovery::decodePrefs(saved.data() + offset, mesh::discovery::PREFS_SIZE, decoded));
assert(decoded == mask);
// All historical prefixes and partial optional records keep replies ON,
// even when a previous load into the same object had disabled them.
for (size_t length = 0; length < offset + mesh::discovery::PREFS_SIZE; ++length) {
CommonCLI reader;
reader.prefs.discovery_responses = 0;
reader.fs.files["/com_prefs"] = {saved.begin(), saved.begin() + length};
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.discovery_responses == 3);
}
for (size_t byte = 0; byte < mesh::discovery::PREFS_SIZE; ++byte) {
auto damaged = saved;
damaged[offset + byte] ^= 0x80;
assertLoaded(damaged, 3);
}
}
CommonCLI missing;
missing.prefs.discovery_responses = 0;
missing.loadPrefsInt(&missing.fs, "/missing");
assert(missing.prefs.discovery_responses == 3);
// A complete, checksummed record with a forbidden mask must still reject.
auto invalid = baseline;
uint8_t record[mesh::discovery::PREFS_SIZE];
mesh::discovery::encodePrefs(record, 0);
record[4] = 4;
const uint16_t crc = mesh::discovery::checksum(record, 6);
record[6] = uint8_t(crc >> 8); record[7] = uint8_t(crc);
invalid.insert(invalid.begin() + offset, record, record + sizeof(record));
assertLoaded(invalid, 3);
#if defined(ESP32_PLATFORM)
// The optional discovery record must not displace the existing CPU footer.
cli.prefs.discovery_responses = 0;
cli.prefs.cpu_freq_mhz = 80;
auto combined = image(cli);
CommonCLI cpu;
cpu.fs.files["/com_prefs"] = combined;
cpu.loadPrefsInt(&cpu.fs, "/com_prefs");
assert(cpu.prefs.discovery_responses == 0 && cpu.prefs.cpu_freq_mhz == 80);
#endif
#if defined(RAK_4631)
cli.prefs.discovery_responses = 1;
cli.prefs.peripherals_shared = 1;
auto combined = image(cli);
CommonCLI peripheral;
peripheral.fs.files["/com_prefs"] = combined;
peripheral.loadPrefsInt(&peripheral.fs, "/com_prefs");
assert(peripheral.prefs.discovery_responses == 1 && peripheral.prefs.peripherals_shared == 1);
#endif
#ifndef WITH_RS232_BRIDGE
// Loading an old UART profile on a non-UART image must still find its saved
// discovery record after the source image's checked ten-byte power tail.
CommonCLI plain;
auto uart = image(plain);
uint8_t power[mesh::bridge::rs232power::PREFS_SIZE];
mesh::bridge::rs232power::encodePrefs(power, 1, 1, 300);
mesh::discovery::encodePrefs(record, 2);
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE, power, power + sizeof(power));
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE + sizeof(power),
record, record + sizeof(record));
assertLoaded(uart, 2);
#endif
}
int main() {
cliChoices();
invalidCommands();
failedSaves();
persistence();
}
'''
OUTBOUND = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <helpers/DiscoveryResponsePrefs.h>
#define PUB_KEY_SIZE 32
#define PAYLOAD_TYPE_ANON_REQ 7
namespace mesh {
struct Identity {
void calcSharedSecret(uint8_t* out, const Identity&) { memset(out, 0xa5, PUB_KEY_SIZE); }
};
struct Packet { uint8_t payload[16] = {}; size_t length = 0; };
}
struct MyMesh {
static constexpr unsigned NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = @MIN_FREE@;
struct { uint8_t discovery_responses = 3; } _prefs;
struct Manager { unsigned free = NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
unsigned getFreeCount() { return free; }
} manager;
Manager* _mgr = &manager;
struct Clock { uint32_t unique = 1000; uint32_t getCurrentTimeUnique() { return ++unique; } } clock;
Clock* getRTCClock() { return &clock; }
mesh::Identity self_id;
mesh::Packet packet;
bool allocation_ok = true;
unsigned allocations = 0, sends = 0;
mesh::Packet* createAnonDatagram(unsigned type, const mesh::Identity&,
const mesh::Identity&, const uint8_t*, const uint8_t* bytes, size_t length) {
++allocations;
assert(type == PAYLOAD_TYPE_ANON_REQ && length == 6);
if (!allocation_ok) return nullptr;
memcpy(packet.payload, bytes, length); packet.length = length;
return &packet;
}
void sendDirect(mesh::Packet* sent, const uint8_t* path, uint8_t length, uint32_t delay) {
assert(sent == &packet && path == nullptr && length == 0 && delay == 0); ++sends;
}
mesh::Packet* sendAnonRegionsReq(const mesh::Identity&, uint32_t&);
};
@CONSTANTS@
@METHOD@
int main() {
const mesh::Identity target;
for (uint8_t mask : {0, 1, 2, 3}) {
MyMesh mesh;
mesh._prefs.discovery_responses = mask;
uint32_t tag = 99;
assert(mesh.sendAnonRegionsReq(target, tag) == &mesh.packet);
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
uint32_t sent_tag = 0; memcpy(&sent_tag, mesh.packet.payload, sizeof(sent_tag));
assert(sent_tag == tag && mesh.packet.payload[4] == ANON_REQ_TYPE_REGIONS
&& mesh.packet.payload[5] == 0);
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS - 1;
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
mesh.allocation_ok = false;
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
assert(mesh.allocations == 2 && mesh.sends == 1);
}
}
'''
class RepeaterDiscoverySettingsTests(unittest.TestCase):
def test_response_choices_do_not_disable_outbound_region_queries(self):
import re
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text()
minimum = re.search(r'NEIGHBOR_DISCOVER_MIN_FREE_PACKETS\s*=\s*(\d+)', header).group(1)
constant = re.search(r'^#define ANON_REQ_TYPE_REGIONS\s+[^\n]+', source, re.M).group()
code = (OUTBOUND.replace('@MIN_FREE@', minimum).replace('@CONSTANTS@', constant)
.replace('@METHOD@', extract_braced(source, 'mesh::Packet* MyMesh::sendAnonRegionsReq(')))
with tempfile.TemporaryDirectory(prefix='repeater-discovery-outbound-') as directory:
work = Path(directory)
(work / 'test.cpp').write_text(code)
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
'-Wall', '-Wextra', '-Werror', '-I' + str(ROOT / 'src'),
str(work / 'test.cpp'), '-o', str(work / 'test')],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(work / 'test')], capture_output=True, text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_real_commands_saved_images_failures_and_legacy_defaults(self):
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
header = (ROOT / 'src/helpers/CommonCLI.h').read_text()
repeater = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
code = production_harness(source, header).split('int main() {', 1)[0]
code = code.replace(' bool trySavePrefs(', ' bool saveCommonPrefs();\n bool trySavePrefs(', 1)
code = ('#include <algorithm>\n#include <helpers/PeripheralModePrefs.h>\n'
'#include <helpers/bridges/RS232PowerSettings.h>\n' + code)
code += extract_braced(source, 'bool CommonCLI::saveCommonPrefs(') + '\n'
code += MAIN.replace('@HANDLER@', extract_braced(
repeater, 'bool MyMesh::handleDiscoveryResponseCommand('))
sanitizer_flags = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
variants = [('ENABLE_OTA', platform) for platform in (
'NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM')]
variants += [('NRF52_PLATFORM', 'RAK_4631'),
('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'),
('NRF52_PLATFORM', 'WITH_RS232_BRIDGE', 'WITH_RS232_BRIDGE_UART=2')]
with tempfile.TemporaryDirectory(prefix='repeater-discovery-settings-') as directory:
work = Path(directory)
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
(work / 'test.cpp').write_text(code)
for macros in variants:
with self.subTest(macros=macros):
binary = work / ('test.exe' if os.name == 'nt' else 'test')
built = subprocess.run([
os.environ.get('CXX', 'g++'), '-std=c++17',
*['-D' + macro if '=' in macro else '-D' + macro + '=1' for macro in macros],
*sanitizer_flags, '-I' + str(work),
'-I' + str(ROOT / 'test/fixtures/radio_profiles/mocks'),
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'src/helpers'),
str(work / 'test.cpp'), '-o', str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
if __name__ == '__main__':
unittest.main()
+7 -2
View File
@@ -22,6 +22,7 @@ HARNESS = r'''
#include <string>
#include <vector>
#include <helpers/GuestLoginSettings.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/ClientLoginPersistence.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/ClientPathPersistence.h>
@@ -119,7 +120,11 @@ struct MyMesh {
ClientACL acl;
Clock clock;
RNG rng;
struct Prefs { char password[16] = "admin"; char guest_password[16] = ""; } _prefs;
struct Prefs {
char password[16] = "admin";
char guest_password[16] = "";
uint8_t discovery_responses = 3;
} _prefs;
unsigned long dirty_contacts_expiry = 0;
uint8_t contacts_save_failures = 0, reply_data[160]{};
int matching_peer_indexes[MAX_CLIENTS]{};
@@ -573,7 +578,7 @@ class RepeaterGuestAccessTests(unittest.TestCase):
"static bool isFilterMgrAllowed(",
))
dispatch = extract_braced(source, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
call = dispatch.index("if (handleGuestCommand(command, reply, 157)")
call = dispatch.index("handleGuestCommand(command, reply, 157)")
guard_start = dispatch.rindex("if (sender && !sender->isAdmin())", 0, call)
guard = extract_braced(dispatch[guard_start:], "if (sender && !sender->isAdmin())")
# Exercise the actual authorization branch and setter, excluding unrelated