From 293e013aa2621badfb97e9f701f67f851c4f1b7e Mon Sep 17 00:00:00 2001 From: cjkas Date: Tue, 13 Oct 2026 15:37:12 +0200 Subject: [PATCH] test: cover the roam threshold and who gets memStatus Move the roam check into worthRoaming() and the memStatus routing into heapEmitTarget() in Utils so the native tests reach them. worthRoaming() roams for a gain of at least ROAM_MIN_GAIN_DB, as the comment and the 6 dB commit said. The old check needed one dB more. Co-Authored-By: Claude Opus 5.5 --- src/ESPNetwork.cpp | 37 ++++++++++++++------------------- src/ESPNetwork.h | 2 +- src/Utils.cpp | 8 +++++++ src/Utils.h | 9 ++++++++ test/test_utils/test_utils.cpp | 38 ++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/ESPNetwork.cpp b/src/ESPNetwork.cpp index d25abde..afc39ed 100644 --- a/src/ESPNetwork.cpp +++ b/src/ESPNetwork.cpp @@ -533,9 +533,9 @@ uint32_t ESPNetwork::getChipId() { return chipId; } bool ESPNetwork::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) { - // The new AP must be at least ROAM_MIN_GAIN_DB stronger. const bool connected = this->connected(); - int32_t strength = connected ? WiFi.RSSI() + ROAM_MIN_GAIN_DB : -127; + const int32_t currentRssi = connected ? WiFi.RSSI() : 0; + int32_t strength = -127; int32_t chan = -1; memset(bssid, 0x00, 6); esp_task_wdt_delete(NULL); @@ -552,7 +552,7 @@ bool ESPNetwork::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *chann ESP_LOGI(TAG, "AP %02X:%02X:%02X:%02X:%02X:%02X ch %ld %ld dBm%s", b[0], b[1], b[2], b[3], b[4], b[5], (long)WiFi.channel(i), (long)WiFi.RSSI(i), (cur && memcmp(b, cur, 6) == 0) ? " (connected)" : ""); } - if(WiFi.RSSI(i) > strength) { + if(worthRoaming(connected, currentRssi, WiFi.RSSI(i), ROAM_MIN_GAIN_DB) && WiFi.RSSI(i) > strength) { strength = WiFi.RSSI(i); memcpy(bssid, WiFi.BSSID(i), 6); *channel = chan = WiFi.channel(i); @@ -671,22 +671,17 @@ void ESPNetwork::emitHeap(uint8_t num) { uint32_t freeHeap = ESP.getFreeHeap(); uint32_t maxHeap = ESP.getMaxAllocHeap(); uint32_t minHeap = ESP.getMinFreeHeap(); - const bool bBroadcast = num == 255 && millis() - _lastHeapEmit >= HEAP_EMIT_MS; - const bool bRoomEmit = sockEmit.activeClients(0) > 0; - if(num != 255 || bBroadcast || bRoomEmit) { - JsonSockEvent *json = sockEmit.beginEmit("memStatus"); - json->beginObject(); - json->addElem("max", maxHeap); - json->addElem("free", freeHeap); - json->addElem("min", minHeap); - json->addElem("total", ESP.getHeapSize()); - json->addElem("uptime", (uint64_t)millis()); - json->endObject(); - if(bBroadcast) { - sockEmit.endEmit(num); - _lastHeapEmit = millis(); - } - else if(num != 255) sockEmit.endEmit(num); - else sockEmit.endEmitRoom(0); - } + const heap_emit_t target = heapEmitTarget(num, millis() - _lastHeapEmit, HEAP_EMIT_MS, sockEmit.activeClients(0) > 0); + if(target == heap_emit_t::none) return; + JsonSockEvent *json = sockEmit.beginEmit("memStatus"); + json->beginObject(); + json->addElem("max", maxHeap); + json->addElem("free", freeHeap); + json->addElem("min", minHeap); + json->addElem("total", ESP.getHeapSize()); + json->addElem("uptime", (uint64_t)millis()); + json->endObject(); + if(target == heap_emit_t::room) sockEmit.endEmitRoom(0); + else sockEmit.endEmit(num); + if(target == heap_emit_t::broadcast) _lastHeapEmit = millis(); } diff --git a/src/ESPNetwork.h b/src/ESPNetwork.h index e90d0ed..32df40d 100644 --- a/src/ESPNetwork.h +++ b/src/ESPNetwork.h @@ -16,7 +16,7 @@ // How often memStatus is broadcast. It is also the one event that is always // sent, so it must stay well under Home Assistant's 60s receive timeout. #define HEAP_EMIT_MS 30000 -// A roaming scan only moves to another AP this much stronger. +// A roaming scan only moves to another AP at least this much stronger. #define ROAM_MIN_GAIN_DB 6 class ESPNetwork { protected: diff --git a/src/Utils.cpp b/src/Utils.cpp index becb358..0d1691b 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -144,3 +144,11 @@ void RssiBroadcast::sent(bool broadcast, int rssi, int channel, uint32_t now) { this->channel = channel; this->at = now; } +bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain) { + return !connected || apRssi >= currentRssi + minGain; +} +heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients) { + if(num != 255) return heap_emit_t::client; + if(sinceLastMs >= intervalMs) return heap_emit_t::broadcast; + return roomHasClients ? heap_emit_t::room : heap_emit_t::none; +} diff --git a/src/Utils.h b/src/Utils.h index ebe5597..b721c5f 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -69,6 +69,15 @@ struct RssiBroadcast { bool due(int rssi, int channel, uint32_t now, int minDelta, uint32_t minMs) const; void sent(bool broadcast, int rssi, int channel, uint32_t now); }; +// Whether an AP a scan found for our SSID is worth connecting to. While +// connected it has to be at least minGain dBm stronger than the current AP so +// a small swing doesn't make the device hop between APs. +bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain); +// Who gets a memStatus event. +enum class heap_emit_t : uint8_t { none, client, broadcast, room }; +// num is the socket client asked for, 255 for everyone. A broadcast goes out +// every intervalMs; in between only the clients in room 0 get one. +heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients); class Timestamp { char _timeBuffer[128]; diff --git a/test/test_utils/test_utils.cpp b/test/test_utils/test_utils.cpp index 7d483bd..80e76cd 100644 --- a/test/test_utils/test_utils.cpp +++ b/test/test_utils/test_utils.cpp @@ -240,6 +240,38 @@ static void test_rssiBroadcast_hold_off_across_millis_wrap() { TEST_ASSERT_TRUE(b.due(-70, 6, 0x00002000u, 3, 10000)); // 12288ms later } +/********************************************************************* + * worthRoaming + ********************************************************************/ +static void test_worthRoaming_needs_the_full_gain() { + TEST_ASSERT_FALSE(worthRoaming(true, -70, -65, 6)); + TEST_ASSERT_TRUE(worthRoaming(true, -70, -64, 6)); + TEST_ASSERT_TRUE(worthRoaming(true, -70, -50, 6)); +} +static void test_worthRoaming_never_to_a_weaker_ap() { + // The scan also lists the AP we are on. + TEST_ASSERT_FALSE(worthRoaming(true, -70, -70, 6)); + TEST_ASSERT_FALSE(worthRoaming(true, -70, -80, 6)); +} +static void test_worthRoaming_any_ap_while_disconnected() { + TEST_ASSERT_TRUE(worthRoaming(false, 0, -90, 6)); +} + +/********************************************************************* + * heapEmitTarget + ********************************************************************/ +static void test_heapEmitTarget_one_client_always_gets_it() { + TEST_ASSERT_TRUE(heap_emit_t::client == heapEmitTarget(3, 0, 30000, false)); + TEST_ASSERT_TRUE(heap_emit_t::client == heapEmitTarget(3, 60000, 30000, true)); +} +static void test_heapEmitTarget_broadcasts_once_per_interval() { + TEST_ASSERT_TRUE(heap_emit_t::broadcast == heapEmitTarget(255, 30000, 30000, false)); + TEST_ASSERT_TRUE(heap_emit_t::broadcast == heapEmitTarget(255, 30000, 30000, true)); + TEST_ASSERT_TRUE(heap_emit_t::none == heapEmitTarget(255, 29999, 30000, false)); +} +static void test_heapEmitTarget_room_in_between() { + TEST_ASSERT_TRUE(heap_emit_t::room == heapEmitTarget(255, 1000, 30000, true)); +} int main(int argc, char **argv) { (void)argc; (void)argv; @@ -276,5 +308,11 @@ int main(int argc, char **argv) { RUN_TEST(test_rssiBroadcast_channel_change_is_due); RUN_TEST(test_rssiBroadcast_client_emit_on_disconnect_keeps_baseline); RUN_TEST(test_rssiBroadcast_hold_off_across_millis_wrap); + RUN_TEST(test_worthRoaming_needs_the_full_gain); + RUN_TEST(test_worthRoaming_never_to_a_weaker_ap); + RUN_TEST(test_worthRoaming_any_ap_while_disconnected); + RUN_TEST(test_heapEmitTarget_one_client_always_gets_it); + RUN_TEST(test_heapEmitTarget_broadcasts_once_per_interval); + RUN_TEST(test_heapEmitTarget_room_in_between); return UNITY_END(); }