fix: send socket events only from loop()

Every socket event is formatted into one static buffer by one shared
formatter.  setConnected() runs on the WiFi/Ethernet event task and sent
memStatus, wifiStrength and ethernet, then ran sockEmit.loop(), while
loop() could be building its own event.  _safecat() checks the room and
then writes at _len, so another task moving _len in between could write
past the end of g_response.

setConnected() now sends nothing.  loop() broadcasts the status every
STATUS_EMIT_MS (30 s): memStatus, then ethernet, or wifiStrength when it
moved 3 dBm or changed channel.  A client that connects still gets all
of it at once from initClients(), so needsBroadcast and the 1.5 s tick
go, and so does the 1.5 s memStatus for the config panel.

SocketEmitter records loop()'s task in begin().  Off it, beginEmit()
logs and hands out a formatter that writes nothing, and endEmit(),
endEmitRoom() and loop() do nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-21 15:27:21 +02:00
parent 474c7c0f42
commit 25d548ef07
11 changed files with 153 additions and 117 deletions

View file

@ -195,6 +195,39 @@ static void test_setBuffer_resets_length() {
TEST_ASSERT_EQUAL_STRING("", storage);
}
/*********************************************************************
* discard
********************************************************************/
static void test_discard_writes_nothing() {
// What a socket event built off loop() goes into. The buffer the formatter
// had before must not see a byte of it.
fmt.beginObject();
fmt.addElem("kept", true);
char before[STORAGE_SIZE];
memcpy(before, storage, sizeof(before));
fmt.discard();
fmt.beginObject();
fmt.addElem("ssid", "Home \"WiFi\"");
fmt.addElem("strength", (int32_t)-70);
fmt.beginArray("pulses");
fmt.addElem((uint32_t)2416);
fmt.endArray();
fmt.addElem("big", (uint64_t)18446744073709551615ULL);
fmt.endObject();
TEST_ASSERT_EQUAL_MEMORY(before, storage, sizeof(before));
TEST_ASSERT_EQUAL_UINT32(0, fmt.length());
TEST_ASSERT_TRUE(fmt.overflowed());
}
static void test_setBuffer_after_discard_writes_again() {
fmt.discard();
fmt.addElem("dropped");
useBuffer(BUFF_SIZE);
fmt.beginObject();
fmt.endObject();
TEST_ASSERT_FALSE(fmt.overflowed());
TEST_ASSERT_EQUAL_STRING("{}", storage);
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
@ -216,5 +249,7 @@ int main(int argc, char **argv) {
RUN_TEST(test_length_matches_what_was_written);
RUN_TEST(test_refused_append_leaves_length_alone);
RUN_TEST(test_setBuffer_resets_length);
RUN_TEST(test_discard_writes_nothing);
RUN_TEST(test_setBuffer_after_discard_writes_again);
return UNITY_END();
}

View file

@ -181,63 +181,73 @@ static void test_parseUTCTime_ignores_local_zone() {
* rssiNeedsEmit
********************************************************************/
static void test_rssiNeedsEmit_channel_change_always_emits() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-70, -70, 6, 11, 0, 3, 10000));
TEST_ASSERT_TRUE(rssiNeedsEmit(-70, -70, 6, 11, 3));
}
static void test_rssiNeedsEmit_small_change_is_ignored() {
TEST_ASSERT_FALSE(rssiNeedsEmit(-72, -70, 6, 6, 60000, 3, 10000));
TEST_ASSERT_FALSE(rssiNeedsEmit(-72, -70, 6, 6, 3));
}
static void test_rssiNeedsEmit_real_change_after_interval_emits() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 10000, 3, 10000));
}
static void test_rssiNeedsEmit_real_change_too_soon_waits() {
TEST_ASSERT_FALSE(rssiNeedsEmit(-73, -70, 6, 6, 5000, 3, 10000));
static void test_rssiNeedsEmit_real_change_emits() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 3));
}
static void test_rssiNeedsEmit_either_direction() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-67, -70, 6, 6, 10000, 3, 10000));
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 10000, 3, 10000));
TEST_ASSERT_TRUE(rssiNeedsEmit(-67, -70, 6, 6, 3));
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 3));
}
/*********************************************************************
* RssiBroadcast
********************************************************************/
static RssiBroadcast broadcastAt(int rssi, int channel, uint32_t at) {
static RssiBroadcast broadcastOf(int rssi, int channel) {
RssiBroadcast b;
b.sent(true, rssi, channel, at);
b.sent(true, rssi, channel);
return b;
}
static void test_rssiBroadcast_first_status_is_due() {
// Nothing broadcast since boot, so the first status tick after connecting
// sends the signal.
RssiBroadcast b;
TEST_ASSERT_TRUE(b.due(-60, 6, 3));
}
static void test_rssiBroadcast_client_emit_keeps_baseline() {
// A new client gets the current RSSI right as it drops 5 dBm. The clients
// already connected must still get the change.
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(false, -65, 6, 20000);
RssiBroadcast b = broadcastOf(-60, 6);
b.sent(false, -65, 6);
TEST_ASSERT_EQUAL_INT(-60, b.rssi);
TEST_ASSERT_EQUAL_UINT32(0, b.at);
TEST_ASSERT_TRUE(b.due(-65, 6, 20000, 3, 10000));
TEST_ASSERT_TRUE(b.due(-65, 6, 3));
}
static void test_rssiBroadcast_broadcast_moves_baseline() {
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(true, -65, 6, 20000);
RssiBroadcast b = broadcastOf(-60, 6);
b.sent(true, -65, 6);
TEST_ASSERT_EQUAL_INT(-65, b.rssi);
TEST_ASSERT_EQUAL_UINT32(20000, b.at);
// Measured from the new baseline and too soon after it.
TEST_ASSERT_FALSE(b.due(-66, 6, 40000, 3, 10000));
TEST_ASSERT_FALSE(b.due(-70, 6, 25000, 3, 10000));
TEST_ASSERT_TRUE(b.due(-70, 6, 30000, 3, 10000));
// Measured from the new baseline.
TEST_ASSERT_FALSE(b.due(-66, 6, 3));
TEST_ASSERT_TRUE(b.due(-70, 6, 3));
}
static void test_rssiBroadcast_channel_change_is_due() {
RssiBroadcast b = broadcastAt(-60, 6, 1000);
TEST_ASSERT_TRUE(b.due(-60, 11, 1000, 3, 10000));
RssiBroadcast b = broadcastOf(-60, 6);
TEST_ASSERT_TRUE(b.due(-60, 11, 3));
}
static void test_rssiBroadcast_client_emit_on_disconnect_keeps_baseline() {
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(false, -100, -1, 5000);
RssiBroadcast b = broadcastOf(-60, 6);
b.sent(false, -100, -1);
TEST_ASSERT_EQUAL_INT(6, b.channel);
TEST_ASSERT_FALSE(b.due(-60, 6, 5000, 3, 10000));
TEST_ASSERT_FALSE(b.due(-60, 6, 3));
}
static void test_rssiBroadcast_hold_off_across_millis_wrap() {
RssiBroadcast b = broadcastAt(-60, 6, 0xFFFFF000u);
TEST_ASSERT_FALSE(b.due(-70, 6, 0x00001000u, 3, 10000)); // 8192ms later
TEST_ASSERT_TRUE(b.due(-70, 6, 0x00002000u, 3, 10000)); // 12288ms later
/*********************************************************************
* emitAllowed
********************************************************************/
static int loopTask, wifiEventTask;
static void test_emitAllowed_on_the_loop_task() {
TEST_ASSERT_TRUE(emitAllowed(&loopTask, &loopTask));
}
static void test_emitAllowed_refuses_another_task() {
TEST_ASSERT_FALSE(emitAllowed(&wifiEventTask, &loopTask));
}
static void test_emitAllowed_before_the_owner_is_known() {
// setup() before SocketEmitter::begin(), which is loop()'s task anyway.
TEST_ASSERT_TRUE(emitAllowed(&loopTask, nullptr));
}
/*********************************************************************
@ -336,22 +346,6 @@ static void test_wifiAuthModeName_unknown_past_the_shared_values() {
TEST_ASSERT_EQUAL_STRING("UNKNOWN", wifiAuthModeName(255));
}
/*********************************************************************
* 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));
}
/*********************************************************************
* LoopStats
********************************************************************/
@ -437,14 +431,16 @@ int main(int argc, char **argv) {
RUN_TEST(test_parseUTCTime_ignores_local_zone);
RUN_TEST(test_rssiNeedsEmit_channel_change_always_emits);
RUN_TEST(test_rssiNeedsEmit_small_change_is_ignored);
RUN_TEST(test_rssiNeedsEmit_real_change_after_interval_emits);
RUN_TEST(test_rssiNeedsEmit_real_change_too_soon_waits);
RUN_TEST(test_rssiNeedsEmit_real_change_emits);
RUN_TEST(test_rssiNeedsEmit_either_direction);
RUN_TEST(test_rssiBroadcast_first_status_is_due);
RUN_TEST(test_rssiBroadcast_client_emit_keeps_baseline);
RUN_TEST(test_rssiBroadcast_broadcast_moves_baseline);
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_emitAllowed_on_the_loop_task);
RUN_TEST(test_emitAllowed_refuses_another_task);
RUN_TEST(test_emitAllowed_before_the_owner_is_known);
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);
@ -458,9 +454,6 @@ int main(int argc, char **argv) {
RUN_TEST(test_formatBacktrace_stops_when_the_buffer_is_full);
RUN_TEST(test_wifiAuthModeName_matches_the_idf_enum);
RUN_TEST(test_wifiAuthModeName_unknown_past_the_shared_values);
RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);
RUN_TEST(test_heapEmitTarget_broadcasts_once_per_interval);
RUN_TEST(test_heapEmitTarget_room_in_between);
RUN_TEST(test_loopStats_rate_is_passes_per_second);
RUN_TEST(test_loopStats_empty_window_has_no_rate);
RUN_TEST(test_loopStats_keeps_the_longest_pass);