Skip to content

Commit 6c2d1da

Browse files
tarikbcclaude
andcommitted
Detect a disconnected board via heartbeat (stop falsely showing "Broadcasting")
The dashboard trusted the last status token forever, so unplugging the ESP left it claiming "Broadcasting". Add a liveness link: - Firmware: emit a "PING" beacon ~every 2s from loop(). - Flipper: stamp last_rx_tick on any RX (PING or event); a 1s ViewDispatcher tick flags link_lost when nothing has arrived for FLYTRAP_LINK_TIMEOUT_MS (5s). The dashboard then shows "Board disconnected" (hollow indicator) instead of "Broadcasting", and recovers automatically when the board returns (its reboot re-runs the handshake). PING is kept out of the console so it doesn't spam it. Note: both halves must be updated together — the new app expects the beacon, so an old firmware without PING would read as a lost link when idle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 58f5db3 commit 6c2d1da

5 files changed

Lines changed: 53 additions & 2 deletions

File tree

esp32/flytrap-fw/flytrap-fw.ino

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// BYE mac=<mac> (a station left the AP)
1212
// IP mac=<mac> ip=<ip> (DHCP assigned a station its IP)
1313
// CRED <urlencoded> (all submitted form fields, one line per submission)
14+
// PING (~every 2s liveness beacon so the Flipper detects unplug)
1415
//
1516
// Authorized testing only.
1617

@@ -252,4 +253,14 @@ void setup() {
252253
void loop() {
253254
if(portalRunning) dnsServer.processNextRequest();
254255
pumpSerial();
256+
257+
// Heartbeat: a periodic liveness beacon so the Flipper can tell the board is
258+
// still attached (it shows "Broadcasting" until told otherwise). If the board
259+
// is unplugged, these stop and the Flipper flags the link as lost.
260+
static uint32_t lastPing = 0;
261+
uint32_t now = millis();
262+
if(now - lastPing >= 2000) {
263+
lastPing = now;
264+
emitLine("PING");
265+
}
255266
}

flipper/flytrap/flytrap.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ static bool flytrap_back_event_callback(void* context) {
2424
return scene_manager_handle_back_event(app->scene_manager);
2525
}
2626

27+
// Periodic tick: with no RX to wake us, this is how we notice the board went
28+
// away. If we haven't heard a PING (or any event) within the timeout, flag the
29+
// link as lost and redraw so the dashboard stops claiming "Broadcasting".
30+
static void flytrap_tick_callback(void* context) {
31+
FlytrapApp* app = context;
32+
if(!app->session_active) return;
33+
bool stale = (furi_get_tick() - app->last_rx_tick) > FLYTRAP_LINK_TIMEOUT_MS;
34+
if(stale && !app->link_lost) {
35+
app->link_lost = true;
36+
scene_manager_handle_custom_event(app->scene_manager, FlytrapEventRefreshView);
37+
}
38+
}
39+
2740
// Runs on the UART worker thread: just wake the GUI to drain/parse RX bytes.
2841
// The context is fixed for the app's lifetime, so this never races a toggle.
2942
static void flytrap_uart_notify(void* ctx) {
@@ -48,6 +61,7 @@ static FlytrapApp* flytrap_app_alloc(void) {
4861
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
4962
view_dispatcher_set_custom_event_callback(app->view_dispatcher, flytrap_custom_event_callback);
5063
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, flytrap_back_event_callback);
64+
view_dispatcher_set_tick_event_callback(app->view_dispatcher, flytrap_tick_callback, 1000);
5165
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
5266

5367
app->submenu = submenu_alloc();

flipper/flytrap/flytrap_i.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#define FLYTRAP_CLIENT_SLOTS (16)
2626
#define FLYTRAP_CAP_KV_SIZE (160)
2727
#define FLYTRAP_LINE_MAX (512)
28+
#define FLYTRAP_LINK_TIMEOUT_MS (5000) // no PING/data for this long -> board unplugged
2829
#define FLYTRAP_SESSION_BUF_MAX (4096) // scrollable captures/raw buffers
2930

3031
#define FLYTRAP_DATA_DIR EXT_PATH("apps_data/flytrap")
@@ -100,6 +101,11 @@ typedef struct FlytrapApp {
100101

101102
FlytrapTextViewMode textview_mode;
102103

104+
// Board liveness: the ESP beacons "PING" ~every 2s; if we hear nothing for
105+
// FLYTRAP_LINK_TIMEOUT_MS the board is likely unplugged and we flag the link.
106+
uint32_t last_rx_tick;
107+
bool link_lost;
108+
103109
// Handshake / lifecycle flags
104110
bool portal_running;
105111
bool pending_setap;

flipper/flytrap/helpers/flytrap_session.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ static void append_raw(FlytrapApp* app, const char* line) {
116116
}
117117

118118
static void process_line(FlytrapApp* app, const char* line) {
119+
// Liveness beacon: consumed for the freshness timer (handled by the caller
120+
// updating last_rx_tick); kept out of the console so it doesn't spam it.
121+
if(strcmp(line, "PING") == 0) return;
122+
119123
append_raw(app, line); // everything is visible in the raw console
120124

121125
if(strncmp(line, "STATUS ", 7) == 0) {
@@ -228,6 +232,8 @@ void flytrap_session_start(FlytrapApp* app) {
228232
app->client_count = 0;
229233
app->clients_rev = 0;
230234
app->selected_client = 0;
235+
app->last_rx_tick = furi_get_tick();
236+
app->link_lost = false;
231237
app->portal_running = false;
232238
app->pending_setap = false;
233239
app->need_restart = false;
@@ -258,8 +264,15 @@ void flytrap_session_stop(FlytrapApp* app) {
258264
void flytrap_session_rx(FlytrapApp* app) {
259265
uint8_t buf[128];
260266
size_t n;
267+
bool got = false;
261268
while((n = flytrap_uart_rx(app->uart, buf, sizeof(buf))) > 0) {
262269
feed(app, buf, n);
270+
got = true;
271+
}
272+
if(got) {
273+
// Any traffic (PING or event) means the board is alive.
274+
app->last_rx_tick = furi_get_tick();
275+
app->link_lost = false;
263276
}
264277
if(app->need_restart) {
265278
// ESP rebooted mid-session; redo the handshake (keep buffers). Only clear

flipper/flytrap/scenes/flytrap_scene_live.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ static void flytrap_dash_refresh(FlytrapApp* app) {
4141
app->widget, 0, 18, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(tmp));
4242

4343
// Status: a filled dot when broadcasting (ring otherwise) + a polished label.
44+
// A lost board link overrides whatever the last status token said.
4445
bool live = false;
45-
const char* state = flytrap_state_label(furi_string_get_cstr(app->status), &live);
46+
const char* state;
47+
if(app->link_lost) {
48+
state = "Board disconnected";
49+
live = false;
50+
} else {
51+
state = flytrap_state_label(furi_string_get_cstr(app->status), &live);
52+
}
4653
widget_add_circle_element(app->widget, 4, 31, 3, live);
4754
widget_add_string_element(app->widget, 12, 28, AlignLeft, AlignTop, FontSecondary, state);
4855

@@ -83,7 +90,7 @@ static void flytrap_live_render(FlytrapApp* app) {
8390
bool live = false;
8491
flytrap_state_label(furi_string_get_cstr(app->status), &live);
8592
bool err = strstr(furi_string_get_cstr(app->status), "err") != NULL;
86-
if(live || err) {
93+
if(live || err || app->link_lost) {
8794
flytrap_dash_refresh(app);
8895
} else {
8996
flytrap_loading_render(app);

0 commit comments

Comments
 (0)