feat(api): system/info sagt, ob der sACN-Empfaenger laeuft

Ein toter Empfangs-Task war von aussen nicht von "es sendet gerade niemand" zu
unterscheiden: beides meldete nur active=false. "multicast" half nicht weiter -
es zeigt den gemerkten Erfolg des LETZTEN Beitritts und bleibt true, auch wenn
der Socket laengst zu ist.

Genau dieser Fall trat am Montana-Black-Shield auf (2026-09-26, direkt nach
Migration und Onboarding) und hat eine halbe Stunde Suche gekostet, an deren
Ende die Ursache immer noch offen war. nl_sacn_running() gab es da schon, es
wurde nur nirgends ausgeliefert.

Zwei Tests: der bestehende prueft running=true mit, ein neuer haelt den toten
Task fest.
This commit is contained in:
Michael Sauer
2026-09-27 02:48:48 +02:00
parent 7fd279721f
commit 543123a60d
4 changed files with 24 additions and 0 deletions
+6
View File
@@ -275,6 +275,12 @@ static void handle_info(const nl_api_ctx_t *ctx, nl_api_response_t *resp)
* der Abnahme nicht zu unterscheiden, ob die Lampe nicht beitritt oder das WLAN die * der Abnahme nicht zu unterscheiden, ob die Lampe nicht beitritt oder das WLAN die
* Gruppe nicht weiterreicht - Unicast laeuft in beiden Faellen. */ * Gruppe nicht weiterreicht - Unicast laeuft in beiden Faellen. */
cJSON_AddBoolToObject(sa, "multicast", in.sacn_joined); cJSON_AddBoolToObject(sa, "multicast", in.sacn_joined);
/* Ob der Empfangs-Task ueberhaupt laeuft. Ohne dieses Feld war ein toter Task von
* "es sendet gerade niemand" nicht zu unterscheiden - beides meldete nur
* active=false, und "multicast" bleibt vom letzten gelungenen Beitritt true, auch
* wenn der Socket laengst zu ist. Das hat beim Montana-Black-Shield eine halbe
* Stunde Suche gekostet. */
cJSON_AddBoolToObject(sa, "running", in.sacn_running);
} }
reply_json(resp, 200, o); reply_json(resp, 200, o);
} }
+1
View File
@@ -26,6 +26,7 @@ typedef struct {
const char *last_ota_error; /* NULL until OTA exists */ const char *last_ota_error; /* NULL until OTA exists */
bool sacn_active; /* true, solange die Lampe ferngesteuert wird */ bool sacn_active; /* true, solange die Lampe ferngesteuert wird */
bool sacn_joined; /* Multicast-Gruppe des Universe beigetreten */ bool sacn_joined; /* Multicast-Gruppe des Universe beigetreten */
bool sacn_running; /* Empfangs-Task lebt und haelt einen Socket */
} nl_sys_info_t; } nl_sys_info_t;
/* Everything the API needs from the application; implemented by src/sys/nl_app_api.c. */ /* Everything the API needs from the application; implemented by src/sys/nl_app_api.c. */
+1
View File
@@ -97,6 +97,7 @@ static void get_info(nl_sys_info_t *out)
nl_app_unlock(); nl_app_unlock();
/* Ausserhalb der Sperre: nl_sacn haelt den Wert selbst, nl_dmx ist nicht beteiligt. */ /* Ausserhalb der Sperre: nl_sacn haelt den Wert selbst, nl_dmx ist nicht beteiligt. */
out->sacn_joined = nl_sacn_joined(); out->sacn_joined = nl_sacn_joined();
out->sacn_running = nl_sacn_running();
nl_wifi_status(&w); nl_wifi_status(&w);
out->wifi_mode = w.mode; out->wifi_mode = w.mode;
memcpy(out->ssid, w.ssid, sizeof out->ssid); memcpy(out->ssid, w.ssid, sizeof out->ssid);
+16
View File
@@ -466,12 +466,27 @@ static void test_info_exposes_sacn_state(void)
{ {
fake_info.sacn_active = true; fake_info.sacn_active = true;
fake_info.sacn_joined = true; fake_info.sacn_joined = true;
fake_info.sacn_running = true;
fake_settings.sacn_universe = 5; fake_settings.sacn_universe = 5;
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL); call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active"))); TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active")));
TEST_ASSERT_EQUAL_INT(5, (int)cJSON_GetNumberValue(sacn_field("universe"))); TEST_ASSERT_EQUAL_INT(5, (int)cJSON_GetNumberValue(sacn_field("universe")));
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("multicast"))); TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("multicast")));
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("running")));
}
/* Ein toter Empfangs-Task sah von aussen genauso aus wie "es sendet gerade niemand":
* beides meldete nur active=false. Beim Montana-Black-Shield (2026-09-26) hat das eine
* halbe Stunde Suche gekostet - "multicast" half nicht weiter, weil es den gemerkten
* Erfolg des LETZTEN Beitritts zeigt und auch dann true bleibt, wenn der Socket laengst
* zu ist. */
static void test_info_reports_a_dead_sacn_task(void)
{
fake_info.sacn_running = false;
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_FALSE(cJSON_IsTrue(sacn_field("running")));
} }
/* --- system/command --- */ /* --- system/command --- */
@@ -610,6 +625,7 @@ int main(void)
RUN_TEST(test_put_sacn_start_slot_must_leave_room_for_all_slots); RUN_TEST(test_put_sacn_start_slot_must_leave_room_for_all_slots);
RUN_TEST(test_put_sacn_start_slot_limit_follows_the_board); RUN_TEST(test_put_sacn_start_slot_limit_follows_the_board);
RUN_TEST(test_info_exposes_sacn_state); RUN_TEST(test_info_exposes_sacn_state);
RUN_TEST(test_info_reports_a_dead_sacn_task);
RUN_TEST(test_commands); RUN_TEST(test_commands);
return UNITY_END(); return UNITY_END();
} }