From 8b070cf0c9031f938b1989a1f8f18ca243f9e8bd Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira <54593998+thomazbandeira@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:01:37 -0300 Subject: [PATCH 1/4] test: add native Config validation coverage --- src/politician_compat.h | 4 +++- test/native/test_config.cpp | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 test/native/test_config.cpp diff --git a/src/politician_compat.h b/src/politician_compat.h index 922bc9e..6fc3a3a 100644 --- a/src/politician_compat.h +++ b/src/politician_compat.h @@ -1,6 +1,8 @@ #pragma once -#ifdef ARDUINO +#if defined(POLITICIAN_HOST_TEST) + #include +#elif defined(ARDUINO) #include #include #else diff --git a/test/native/test_config.cpp b/test/native/test_config.cpp new file mode 100644 index 0000000..fb2422c --- /dev/null +++ b/test/native/test_config.cpp @@ -0,0 +1,35 @@ +#include +#include + +#define POLITICIAN_HOST_TEST +#include "PoliticianTypes.h" + +using namespace politician; + +int main() { + Config defaults; + const char *warnings[16] = {}; + assert(validateConfig(defaults, warnings, 16) == 0); + + Config invalid; + invalid.hop_min_dwell_ms = 400; + invalid.hop_max_dwell_ms = 200; + invalid.fish_timeout_ms = 100; + invalid.csa_wait_ms = 100; + invalid.hop_dwell_ms = 0; + invalid.deauth_burst_count = 0; + invalid.csa_beacon_count = 0; + invalid.probe_aggr_interval_s = 0; + invalid.min_rssi = -10; + assert(validateConfig(invalid, warnings, 16) == 8); + + Config overflow; + overflow.hop_min_dwell_ms = UINT16_MAX; + assert(validateConfig(overflow, warnings, 16) == 2); + + Config noSmartHopping; + noSmartHopping.smart_hopping = false; + noSmartHopping.hop_min_dwell_ms = UINT16_MAX; + assert(validateConfig(noSmartHopping, warnings, 16) == 0); + return 0; +} \ No newline at end of file From aa6b7a39a90b42393672d1e3fd4524e0c9f5b432 Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira <54593998+thomazbandeira@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:01:39 -0300 Subject: [PATCH 2/4] docs: document lifecycle callbacks and regulatory scope --- README.md | 28 +++++++++++++++++++++++++ docs/delivery/ChangeSummary.md | 32 +++++++++++++++++++++++++++++ docs/delivery/ImplementationPlan.md | 16 +++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 docs/delivery/ChangeSummary.md create mode 100644 docs/delivery/ImplementationPlan.md diff --git a/README.md b/README.md index e31aab3..9196da8 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,14 @@ engine.begin(cfg); // begins normally; begin() clamps automatically `validateConfig()` is a free inline function in the `politician` namespace — zero allocations, zero dependencies. +`begin()` rejects a smart-hopping configuration whose minimum dwell cannot be +represented after the `min + 50 ms` correction. Calling `begin()` on an already +initialized engine stops the previous runtime first, so rebegin is deterministic. +Calling `stop()` is safe more than once; it disables capture, unregisters the +instance, and releases the worker resources. The destructor also stops an active +engine. WiFi driver ownership remains process-wide and is reused by later +instances. + ### Configuration ```cpp @@ -291,6 +299,26 @@ void setPacketLogger(PacketCb cb); // Raw promiscuous-mode frames void setLogger(LogCb cb); // Redirect internal log output ``` +Callbacks run on the engine worker task while the engine state is protected. +They should do short, non-blocking work and must not call blocking Politician +methods or retain references to callback records. In particular, +`EspNowRecord::payload` points into the current receive buffer and is valid only +for the duration of the callback; copy it when it must outlive the callback. + +ESP-NOW capture is enabled by default unless `POLITICIAN_NO_ESPNOW` is defined. +The parser accepts Espressif vendor-specific action frames and reports the +source, destination, channel, RSSI, timestamp, and payload through +`setEspNowCallback()`. + +### Regulatory domain + +Regulatory-domain selection is intentionally not implemented yet. The current +channel helper contains common 2.4 GHz and 5 GHz channels but has no authoritative +country source, DFS/CAC state, or API for selecting a country. A future change +should add an explicit country/domain configuration backed by the ESP-IDF WiFi +country API, filter channels from that result, and define behavior for DFS and +unsupported regions before exposing it publicly. + ### Engine Control ```cpp diff --git a/docs/delivery/ChangeSummary.md b/docs/delivery/ChangeSummary.md new file mode 100644 index 0000000..496f696 --- /dev/null +++ b/docs/delivery/ChangeSummary.md @@ -0,0 +1,32 @@ +# Change Summary + +## Changes + +- Added `EspNowCapture` to the example build matrix and restored its example. +- Added the ESP-NOW callback record/parser contract. +- Added deterministic host-side `Config` validation tests. +- Added idempotent stop/rebegin cleanup for per-instance task, ring buffer, and + mutex resources. +- Documented lifecycle and callback contracts. + +## Breaking Changes + +None. Existing public methods and callback signatures remain compatible. + +## Migration Scope + +None. ESP-NOW is available by default and can be excluded with +`POLITICIAN_NO_ESPNOW`. + +## Rollback Strategy + +Revert the commits in reverse order. No database, persistent storage, or +external state migration is introduced. + +## Known Limitations + +- PlatformIO is not installed in the local environment, so ESP32 builds were + not run locally. +- Regulatory-domain selection is documented as a proposal only. +- The host test covers public configuration validation, not ESP32 FreeRTOS + scheduling or WiFi-driver behavior. \ No newline at end of file diff --git a/docs/delivery/ImplementationPlan.md b/docs/delivery/ImplementationPlan.md new file mode 100644 index 0000000..5ec7b4d --- /dev/null +++ b/docs/delivery/ImplementationPlan.md @@ -0,0 +1,16 @@ +# Implementation Plan + +Base: `upstream/main` +Branch: `feat/core-reliability` + +1. Add the existing ESP-NOW capture example to the PlatformIO CI matrix. +2. Add deterministic host-side coverage for public `Config` validation without + including Arduino or ESP32 runtime headers. +3. Preserve the public API while making `begin()` validation reject an unsafe + dwell correction and making stop/rebegin release per-instance runtime + resources. +4. Document lifecycle, callback lifetime/threading constraints, compatibility, + and the regulatory-domain proposal. + +Regulatory-domain behavior remains intentionally out of scope until a country +source and DFS policy can be defined safely. \ No newline at end of file From 78836474756aad0e8f755d9dd9c5d70e35f6675d Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira <54593998+thomazbandeira@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:58:07 -0300 Subject: [PATCH 3/4] docs: finalize delivery documentation for core reliability --- docs/delivery/ChangeSummary.md | 16 +++++++++++++++- docs/delivery/ImplementationPlan.md | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/delivery/ChangeSummary.md b/docs/delivery/ChangeSummary.md index 496f696..39b8db5 100644 --- a/docs/delivery/ChangeSummary.md +++ b/docs/delivery/ChangeSummary.md @@ -7,6 +7,14 @@ - Added deterministic host-side `Config` validation tests. - Added idempotent stop/rebegin cleanup for per-instance task, ring buffer, and mutex resources. +- Made `begin()` transactional after instance registration, including Wi-Fi + driver teardown when that attempt initialized Wi-Fi. +- Added an ISR-safe shutdown barrier: registry removal and shutdown marking are + serialized, in-flight callbacks drain, and the worker exits before its + ringbuffer and mutex are destroyed. +- Removed the duplicate `[env:esp32]` section from `EspNowCapture`. +- Masked ESP-NOW MACs and disabled payload printing by default in the example; + complete payload diagnostics are compile-time opt-in. - Documented lifecycle and callback contracts. ## Breaking Changes @@ -29,4 +37,10 @@ external state migration is introduced. not run locally. - Regulatory-domain selection is documented as a proposal only. - The host test covers public configuration validation, not ESP32 FreeRTOS - scheduling or WiFi-driver behavior. \ No newline at end of file + scheduling or WiFi-driver behavior. +- PlatformIO is unavailable in the local environment; the native test was + compiled and executed directly with Clang, while the ESP32 example build was + not run locally. +- `stop()` must not be called from a Politician callback running on the worker + task, because waiting for that same worker would deadlock. Callbacks should + signal application state and let the main/application task perform stop. \ No newline at end of file diff --git a/docs/delivery/ImplementationPlan.md b/docs/delivery/ImplementationPlan.md index 5ec7b4d..7a4401a 100644 --- a/docs/delivery/ImplementationPlan.md +++ b/docs/delivery/ImplementationPlan.md @@ -12,5 +12,15 @@ Branch: `feat/core-reliability` 4. Document lifecycle, callback lifetime/threading constraints, compatibility, and the regulatory-domain proposal. +Security remediation completed on the current branch: + +5. Make `begin()` roll back registry, Wi-Fi, mutex, ringbuffer, and worker + resources on every post-registration failure. +6. Make `stop()` mark shutdown and unregister under an ISR-safe critical + section, wait for in-flight callbacks, then wait for the worker before + deleting the ringbuffer and mutex. +7. Keep the ESP-NOW example's MAC output masked and payload output disabled by + default. Full payload diagnostics require `ESP_NOW_DIAGNOSTICS=1`. + Regulatory-domain behavior remains intentionally out of scope until a country source and DFS policy can be defined safely. \ No newline at end of file From 8b75bf3c49ffd129b9302ab8708af09788fdf495 Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira <54593998+thomazbandeira@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:37:48 -0300 Subject: [PATCH 4/4] fix(core): ensure thread-safe worker shutdown and promiscuous callback dispatch --- src/Politician.cpp | 161 ++++++++++++++++++++++++++++++++++++------ src/Politician.h | 13 ++++ src/PoliticianTypes.h | 2 + 3 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/Politician.cpp b/src/Politician.cpp index 6b22c83..d0c672b 100644 --- a/src/Politician.cpp +++ b/src/Politician.cpp @@ -11,6 +11,7 @@ namespace politician { // ─── Static members ─────────────────────────────────────────────────────────── Politician *Politician::_instances[POLITICIAN_MAX_INSTANCES] = {}; bool Politician::_wifiInitialized = false; +portMUX_TYPE Politician::_instanceMux = portMUX_INITIALIZER_UNLOCKED; // Default 2.4GHz hopping sequence (channels 1-13) const uint8_t Politician::HOP_SEQ[] = {1, 6, 11, 2, 7, 3, 8, 4, 9, 5, 10, 12, 13}; @@ -79,6 +80,10 @@ Politician::Politician() #endif } +Politician::~Politician() { + stop(); +} + // ─── Logging ───────────────────────────────────────────────────────────────── void Politician::_log(const char *fmt, ...) { #ifndef POLITICIAN_NO_LOGGING @@ -98,10 +103,43 @@ void Politician::_log(const char *fmt, ...) { // ─── begin() ───────────────────────────────────────────────────────────────── Error Politician::begin(const Config &cfg) { + if (cfg.smart_hopping && cfg.hop_min_dwell_ms > UINT16_MAX - 50) { + return ERR_INVALID_ARG; + } + + if (_initialized) stop(); + _cfg = cfg; + bool wifiDriverStarted = false; + + auto rollback = [&]() { + stop(); + if (wifiDriverStarted) { + bool otherActive = false; + portENTER_CRITICAL(&_instanceMux); + for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) { + if (_instances[i] != nullptr && _instances[i] != this) { + otherActive = true; + break; + } + } + if (!otherActive) { + _wifiInitialized = false; + } + portEXIT_CRITICAL(&_instanceMux); + + if (!otherActive) { + esp_wifi_set_promiscuous(false); + esp_wifi_set_promiscuous_rx_cb(nullptr); + esp_wifi_stop(); + esp_wifi_deinit(); + } + } + }; // ── Register this instance in the shared instance registry ─────────────── bool registered = false; + portENTER_CRITICAL(&_instanceMux); for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) { if (_instances[i] == this) { registered = true; break; } // already registered (re-begin) } @@ -114,16 +152,18 @@ Error Politician::begin(const Config &cfg) { } } if (!registered) { + portEXIT_CRITICAL(&_instanceMux); _log("[WiFi] ERR: all %d instance slots occupied\n", POLITICIAN_MAX_INSTANCES); return ERR_MAX_INSTANCES; } } + portEXIT_CRITICAL(&_instanceMux); // Validate and clamp critical config values if (_cfg.smart_hopping && _cfg.hop_min_dwell_ms >= _cfg.hop_max_dwell_ms) { _log("[Config] WARNING: hop_min_dwell_ms (%u) >= hop_max_dwell_ms (%u); clamping max to min+50ms\n", _cfg.hop_min_dwell_ms, _cfg.hop_max_dwell_ms); - _cfg.hop_max_dwell_ms = _cfg.hop_min_dwell_ms + 50; + _cfg.hop_max_dwell_ms = static_cast(_cfg.hop_min_dwell_ms + 50); } if (_cfg.fish_timeout_ms < 500) { _log("[Config] WARNING: fish_timeout_ms (%u) < 500ms; clamping to 500ms\n", _cfg.fish_timeout_ms); @@ -138,24 +178,30 @@ Error Politician::begin(const Config &cfg) { #endif // ── WiFi driver init — only the first instance does this ───────────────── - if (!_wifiInitialized) { + portENTER_CRITICAL(&_instanceMux); + bool needWifiInit = !_wifiInitialized; + if (needWifiInit) { + _wifiInitialized = true; + } + portEXIT_CRITICAL(&_instanceMux); + + if (needWifiInit) { wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); - if (esp_wifi_init(&wifi_cfg) != ESP_OK) return ERR_WIFI_INIT; - if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK) return ERR_WIFI_INIT; + if (esp_wifi_init(&wifi_cfg) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } + wifiDriverStarted = true; + if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } - if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) return ERR_WIFI_INIT; - if (esp_wifi_start() != ESP_OK) return ERR_WIFI_INIT; + if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } + if (esp_wifi_start() != ESP_OK) { rollback(); return ERR_WIFI_INIT; } esp_log_level_set("wifi", ESP_LOG_NONE); wifi_promiscuous_filter_t filt = { .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA }; - if (esp_wifi_set_promiscuous_filter(&filt) != ESP_OK) return ERR_WIFI_INIT; - if (esp_wifi_set_promiscuous(true) != ESP_OK) return ERR_WIFI_INIT; - if (esp_wifi_set_promiscuous_rx_cb(&_promiscuousCb) != ESP_OK) return ERR_WIFI_INIT; - - _wifiInitialized = true; + if (esp_wifi_set_promiscuous_filter(&filt) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } + if (esp_wifi_set_promiscuous(true) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } + if (esp_wifi_set_promiscuous_rx_cb(&_promiscuousCb) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } } wifi_config_t ap_cfg = {}; @@ -169,30 +215,34 @@ Error Politician::begin(const Config &cfg) { ap_cfg.ap.authmode = WIFI_AUTH_OPEN; ap_cfg.ap.channel = 1; ap_cfg.ap.beacon_interval = 1000; - esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); + if (esp_wifi_set_config(WIFI_IF_AP, &ap_cfg) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } esp_wifi_get_mac(WIFI_IF_STA, _ownStaMac); _log("[WiFi] STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", _ownStaMac[0], _ownStaMac[1], _ownStaMac[2], _ownStaMac[3], _ownStaMac[4], _ownStaMac[5]); - if (esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) return ERR_WIFI_INIT; + if (esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) { rollback(); return ERR_WIFI_INIT; } // Initialize Thread Safety if (!_lock) { _lock = xSemaphoreCreateRecursiveMutex(); - if (!_lock) return ERR_WIFI_INIT; + if (!_lock) { rollback(); return ERR_WIFI_INIT; } } // Initialize Async Processing Core (Ringbuffer + Task) if (!_rb) { _rb = xRingbufferCreate(16384, RINGBUF_TYPE_NOSPLIT); - if (!_rb) return ERR_WIFI_INIT; + if (!_rb) { rollback(); return ERR_WIFI_INIT; } } + _shutdownRequested = false; + _workerExited = false; + _isrUsers = 0; + if (!_task) { xTaskCreatePinnedToCore(_workerTask, "pol_worker", 4096, this, 5, &_task, 0); - if (!_task) return ERR_WIFI_INIT; + if (!_task) { rollback(); return ERR_WIFI_INIT; } } _initialized = true; @@ -278,6 +328,8 @@ void Politician::stopHopping() { } void Politician::stop() { + _shutdownRequested = true; + if (_lock && xSemaphoreTakeRecursive(_lock, pdMS_TO_TICKS(100)) == pdTRUE) { if (_fishState != FISH_IDLE) { esp_wifi_disconnect(); @@ -292,13 +344,53 @@ void Politician::stop() { _active = false; xSemaphoreGiveRecursive(_lock); } - // Deregister from the shared instance registry so the ISR skips this instance. + + // Deregister from the shared instance registry under _instanceMux so the ISR skips this instance. + portENTER_CRITICAL(&_instanceMux); for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) { if (_instances[i] == this) { _instances[i] = nullptr; break; } } + portEXIT_CRITICAL(&_instanceMux); + + while (_isrUsers != 0) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + + if (_task && xTaskGetCurrentTaskHandle() == _task) { + _log("[WiFi] stop() called by worker; cleanup deferred until callback returns\n"); + _initialized = false; + return; + } + + if (_task) { + ulTaskNotifyTake(pdTRUE, 0); + + portENTER_CRITICAL(&_instanceMux); + bool needsWait = !_workerExited; + if (needsWait) { + _stopWaiter = xTaskGetCurrentTaskHandle(); + } + portEXIT_CRITICAL(&_instanceMux); + + if (needsWait) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + _task = nullptr; + _stopWaiter = nullptr; + } + + if (_rb) { + vRingbufferDelete(_rb); + _rb = nullptr; + } + if (_lock) { + vSemaphoreDelete(_lock); + _lock = nullptr; + } + _initialized = false; _log("[WiFi] Engine stopped\n"); } @@ -901,20 +993,37 @@ void IRAM_ATTR Politician::_promiscuousCb(void *buf, wifi_promiscuous_pkt_type_t const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf; uint16_t total_len = sizeof(wifi_pkt_rx_ctrl_t) + pkt->rx_ctrl.sig_len; + Politician *targets[POLITICIAN_MAX_INSTANCES]; + uint8_t count = 0; + + portENTER_CRITICAL_ISR(&_instanceMux); for (uint8_t i = 0; i < POLITICIAN_MAX_INSTANCES; i++) { Politician *inst = _instances[i]; - if (!inst || !inst->_active || !inst->_rb) continue; + if (inst && inst->_active && inst->_rb) { + inst->_isrUsers++; + targets[count++] = inst; + } + } + portEXIT_CRITICAL_ISR(&_instanceMux); + + for (uint8_t i = 0; i < count; i++) { + Politician *inst = targets[i]; if (xRingbufferSendFromISR(inst->_rb, buf, total_len, NULL) != pdTRUE) { inst->_stats.dropped++; } + portENTER_CRITICAL_ISR(&_instanceMux); + if (inst->_isrUsers > 0) { + inst->_isrUsers--; + } + portEXIT_CRITICAL_ISR(&_instanceMux); } } void Politician::_workerTask(void *pvParameters) { Politician *self = (Politician *)pvParameters; - while (true) { + while (!self->_shutdownRequested) { size_t size = 0; - wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)xRingbufferReceive(self->_rb, &size, portMAX_DELAY); + wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)xRingbufferReceive(self->_rb, &size, pdMS_TO_TICKS(20)); if (pkt) { // Infer frame type from 802.11 Frame Control field uint16_t fc = pkt->payload[0] | (pkt->payload[1] << 8); @@ -935,6 +1044,18 @@ void Politician::_workerTask(void *pvParameters) { vRingbufferReturnItem(self->_rb, (void *)pkt); } } + + portENTER_CRITICAL(&_instanceMux); + self->_workerExited = true; + self->_task = nullptr; + TaskHandle_t waiter = self->_stopWaiter; + self->_stopWaiter = nullptr; + portEXIT_CRITICAL(&_instanceMux); + + if (waiter) { + xTaskNotifyGive(waiter); + } + vTaskDelete(NULL); } void Politician::_handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type) { diff --git a/src/Politician.h b/src/Politician.h index 771682e..01e8cf4 100644 --- a/src/Politician.h +++ b/src/Politician.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "PoliticianTypes.h" namespace politician { @@ -105,6 +106,11 @@ class Politician { public: Politician(); + /** + * @brief Destructor. Ensures the engine is stopped and resources released. + */ + ~Politician(); + /** * @brief Initializes the WiFi driver in promiscuous mode. * @param cfg Optional configuration struct. @@ -159,6 +165,8 @@ class Politician { * @brief Full engine teardown. Aborts any in-progress attack, clears the * target, stops hopping, and disables frame processing in one call. * Use this instead of combining stopHopping() + clearTarget() + setActive(false). + * @note If called by the worker or one of its callbacks, shutdown is marked and + * resource cleanup is deferred; call stop() again after the callback returns. */ void stop(); @@ -535,12 +543,17 @@ class Politician { static void _workerTask(void *pvParameters); /** Per-process instance registry; populated by begin(), cleared by stop(). */ static Politician *_instances[POLITICIAN_MAX_INSTANCES]; + static portMUX_TYPE _instanceMux; /** Set to true after the first successful begin() so subsequent calls skip WiFi driver init. */ static bool _wifiInitialized; RingbufHandle_t _rb = nullptr; TaskHandle_t _task = nullptr; + TaskHandle_t _stopWaiter = nullptr; SemaphoreHandle_t _lock = nullptr; + volatile bool _shutdownRequested = true; + volatile bool _workerExited = true; + volatile uint32_t _isrUsers = 0; void _handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type); void _handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload, uint16_t len, int8_t rssi); diff --git a/src/PoliticianTypes.h b/src/PoliticianTypes.h index 9415769..0f9855e 100644 --- a/src/PoliticianTypes.h +++ b/src/PoliticianTypes.h @@ -167,6 +167,8 @@ inline int validateConfig(const Config &cfg, const char **out, uint8_t maxOut) { auto w = [&](const char *msg) { if (n < maxOut) out[n++] = msg; }; if (cfg.smart_hopping && cfg.hop_min_dwell_ms >= cfg.hop_max_dwell_ms) w("hop_min_dwell_ms >= hop_max_dwell_ms — max will be clamped to min+50ms"); + if (cfg.smart_hopping && cfg.hop_min_dwell_ms > UINT16_MAX - 50) + w("hop_min_dwell_ms is too large for the min+50ms clamp"); if (cfg.fish_timeout_ms < 500) w("fish_timeout_ms < 500 — will be clamped to 500ms"); if (cfg.csa_wait_ms < 1000)