Skip to content

Commit 9a0bdb4

Browse files
committed
Fix portal-start flow: OOM, detecting screen, auto-detect, disconnect handling
Found on hardware while testing v0.4.0: - OOM crash on Start: reserve the FuriString from the file size before reading the ~38.5 KB portal, so byte-by-byte appends don't realloc (the geometric growth's ~2x peak was blowing the heap). - Board check moved into the Live scene as a "Detecting board..." screen so the (blocking) check doesn't freeze on a blank frame; then "Starting portal..." or "No board detected". - The No-board screen keeps watching for the beacon and auto-continues the start when the board is plugged in (~1-3s), no Back + Start needed. - Losing the board link ends the session so the menu returns to "Start Portal" (updates even while on the menu), instead of still offering Stop/Dashboard. - Clear stale link_lost at the start of each run so it can't flash "Board disconnected" during startup. Bump fap_version to 0.4.1; CHANGELOG.
1 parent d9c0fac commit 9a0bdb4

8 files changed

Lines changed: 107 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
All notable changes to Flytrap are documented here.
44

5+
## [0.4.1] — 2026-07-17
6+
7+
Fixes for the portal-start flow, all found on hardware.
8+
9+
### Fixed
10+
- **Out-of-memory crash on Start.** The ~38.5 KB portal was read byte-by-byte into a
11+
growing `FuriString`, whose reallocations briefly held two buffers (~2x peak) and blew
12+
the heap. The buffer is now reserved once from the file size, so the peak is ~1x.
13+
- **"Detecting board…" screen.** Start paints a status screen before the (possibly
14+
blocking) board check, so it no longer looks frozen; then **Starting portal…**, or
15+
**No board detected** if nothing answers.
16+
- **Auto-detect on the No-board screen.** Plugging the board in while "No board detected"
17+
is shown now auto-continues the start within ~1–3 s (no Back + Start needed).
18+
- **Session ends when the board is unplugged.** Losing the link now ends the session, so
19+
the menu returns to **Start Portal** instead of still offering Stop/Dashboard/Console
20+
(updates even while sitting on the menu).
21+
- **No false "Board disconnected" flash** when starting right after a disconnect — stale
22+
link state is cleared at the start of each run.
23+
524
## [0.4.0] — 2026-07-17
625

726
A single offline social portal, a live clients view, and board-liveness handling.

flipper/flytrap/application.fam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ App(
88
fap_icon="icons/flytrap_10px.png",
99
fap_category="GPIO",
1010
fap_author="Tarik Caramanico",
11-
fap_version="0.4",
11+
fap_version="0.4.1",
1212
fap_description="Captive portal (evil portal) for the ESP32-S2 WiFi dev board. Authorized use only.",
1313
)

flipper/flytrap/flytrap.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,25 @@ static bool flytrap_back_event_callback(void* context) {
3131
// link as lost and redraw so the dashboard stops claiming "Broadcasting".
3232
static void flytrap_tick_callback(void* context) {
3333
FlytrapApp* app = context;
34+
// On the "No board detected" screen: idle RX keeps last_rx_tick current, so a
35+
// fresh stamp means the board was just plugged in — resume the start flow.
36+
if(app->awaiting_board) {
37+
if(furi_get_tick() - app->last_rx_tick < 2500) {
38+
app->awaiting_board = false;
39+
scene_manager_handle_custom_event(app->scene_manager, FlytrapEventDetectBoard);
40+
}
41+
return;
42+
}
3443
if(!app->session_active) return;
3544
bool stale = (furi_get_tick() - app->last_rx_tick) > FLYTRAP_LINK_TIMEOUT_MS;
3645
if(stale && !app->link_lost) {
3746
app->link_lost = true;
47+
// The board is gone, so nothing is broadcasting — end the session so the
48+
// menu returns to "Start Portal" instead of offering Stop/Dashboard. The
49+
// dashboard still shows "Board disconnected" (link_lost) until the user
50+
// leaves; restarting re-detects the board and re-runs the handshake.
51+
app->session_active = false;
52+
app->portal_running = false;
3853
scene_manager_handle_custom_event(app->scene_manager, FlytrapEventRefreshView);
3954
}
4055
}

flipper/flytrap/flytrap_custom_event.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ typedef enum {
2020
// Client-detail paging.
2121
FlytrapEventClientPrev = 108,
2222
FlytrapEventClientNext = 109,
23-
// Posted by the loading scene once it has drawn, to kick off the (blocking)
24-
// portal start without freezing on a blank screen.
25-
FlytrapEventBeginSend = 110,
23+
// Start flow, each posted after the prior screen paints so a blocking step
24+
// never freezes on a blank frame: detect the board, then send the portal.
25+
FlytrapEventDetectBoard = 110,
26+
FlytrapEventBeginSend = 111,
2627
} FlytrapCustomEvent;

flipper/flytrap/flytrap_i.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@ typedef struct FlytrapApp {
105105
// FLYTRAP_LINK_TIMEOUT_MS the board is likely unplugged and we flag the link.
106106
uint32_t last_rx_tick;
107107
bool link_lost;
108+
bool awaiting_board; // on the "No board detected" screen, watching for the beacon
108109

109110
// Handshake / lifecycle flags
110111
bool portal_running;
112+
bool menu_shows_active; // session_active the main menu was last built for
111113
bool pending_setap;
112114
bool session_active; // portal owned (persists across menu/sub-views)
113115
bool need_restart; // ESP reported "boot" mid-session -> resend

flipper/flytrap/helpers/flytrap_storage.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ bool flytrap_storage_read_file(const char* path, FuriString* out, size_t cap) {
7373
size_t total = 0;
7474

7575
if(storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
76+
// Reserve the whole buffer up front. Without this, appending byte-by-byte
77+
// reallocs the string geometrically, and each grow briefly holds both the
78+
// old and new buffers — a ~2x peak that OOMs the Flipper on a large (38 KB)
79+
// portal. The +128 leaves room for {{SSID}} expansion done by the caller.
80+
uint64_t fsize = storage_file_size(file);
81+
size_t reserve = (fsize < cap ? (size_t)fsize : cap) + 128;
82+
furi_string_reserve(out, reserve);
7683
uint8_t buf[257];
7784
while(total < cap) {
7885
size_t want = cap - total;

flipper/flytrap/scenes/flytrap_scene_live.c

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,45 +67,53 @@ static void flytrap_dash_refresh(FlytrapApp* app) {
6767
furi_string_free(tmp);
6868
}
6969

70-
// Shown while the portal is coming up — the ESP handshake plus the ~3s it takes
71-
// to stream the portal HTML over UART (which blocks the UI), so the user sees
72-
// progress instead of a frozen dashboard.
73-
static void flytrap_loading_render(FlytrapApp* app) {
70+
// A centered status card (title + divider + two lines). Used for the detecting /
71+
// no-board / starting screens so a blocking step never shows a blank frame.
72+
static void flytrap_status_screen(FlytrapApp* app, const char* l1, const char* l2) {
7473
widget_reset(app->widget);
7574
widget_add_string_element(app->widget, 64, 6, AlignCenter, AlignTop, FontPrimary, "Flytrap");
7675
widget_add_line_element(app->widget, 0, 20, 127, 20);
77-
widget_add_string_element(
78-
app->widget, 64, 25, AlignCenter, AlignTop, FontSecondary, "Starting portal...");
79-
FuriString* tmp = furi_string_alloc();
80-
furi_string_printf(tmp, "SSID: %s", furi_string_get_cstr(app->ssid));
81-
widget_add_string_element(
82-
app->widget, 64, 38, AlignCenter, AlignTop, FontSecondary, furi_string_get_cstr(tmp));
83-
widget_add_string_element(
84-
app->widget, 64, 51, AlignCenter, AlignTop, FontSecondary, "Bringing up Wi-Fi");
85-
furi_string_free(tmp);
76+
widget_add_string_element(app->widget, 64, 27, AlignCenter, AlignTop, FontSecondary, l1);
77+
if(l2 && l2[0])
78+
widget_add_string_element(app->widget, 64, 42, AlignCenter, AlignTop, FontSecondary, l2);
8679
}
8780

88-
// Loading screen until the portal is broadcasting (or errors), then the dashboard.
81+
// Render the screen for the current start state: detecting / no-board / starting,
82+
// then the dashboard once broadcasting (or on error / a lost board link).
8983
static void flytrap_live_render(FlytrapApp* app) {
84+
const char* s = furi_string_get_cstr(app->status);
85+
if(strcmp(s, "detecting") == 0) {
86+
flytrap_status_screen(app, "Detecting board...", "");
87+
return;
88+
}
89+
if(strcmp(s, "noboard") == 0) {
90+
flytrap_status_screen(app, "No board detected", "Attach the ESP32 board");
91+
return;
92+
}
9093
bool live = false;
91-
flytrap_state_label(furi_string_get_cstr(app->status), &live);
92-
bool err = strstr(furi_string_get_cstr(app->status), "err") != NULL;
94+
flytrap_state_label(s, &live);
95+
bool err = strstr(s, "err") != NULL;
9396
if(live || err || app->link_lost) {
9497
flytrap_dash_refresh(app);
9598
} else {
96-
flytrap_loading_render(app);
99+
FuriString* tmp = furi_string_alloc();
100+
furi_string_printf(tmp, "SSID: %s", furi_string_get_cstr(app->ssid));
101+
flytrap_status_screen(app, "Starting portal...", furi_string_get_cstr(tmp));
102+
furi_string_free(tmp);
97103
}
98104
}
99105

100106
void flytrap_scene_live_on_enter(void* context) {
101107
FlytrapApp* app = context;
102108
if(!app->session_active) {
103-
// Fresh start: paint the loading screen first, then kick off the blocking
104-
// send on the next event loop pass so it doesn't freeze on a blank frame.
105-
furi_string_set(app->status, "starting");
106-
flytrap_loading_render(app);
109+
// Fresh start: paint "Detecting board..." first, then run the (possibly
110+
// blocking) check on the next loop pass so it doesn't freeze on a blank frame.
111+
app->awaiting_board = false;
112+
app->link_lost = false; // clear any stale disconnect from a prior session
113+
furi_string_set(app->status, "detecting");
114+
flytrap_live_render(app);
107115
view_dispatcher_switch_to_view(app->view_dispatcher, FlytrapViewWidget);
108-
view_dispatcher_send_custom_event(app->view_dispatcher, FlytrapEventBeginSend);
116+
view_dispatcher_send_custom_event(app->view_dispatcher, FlytrapEventDetectBoard);
109117
} else {
110118
flytrap_live_render(app);
111119
view_dispatcher_switch_to_view(app->view_dispatcher, FlytrapViewWidget);
@@ -116,8 +124,21 @@ bool flytrap_scene_live_on_event(void* context, SceneManagerEvent event) {
116124
FlytrapApp* app = context;
117125
if(event.type != SceneManagerEventTypeCustom) return false;
118126
switch(event.event) {
127+
case FlytrapEventDetectBoard:
128+
if(flytrap_board_present(app, 2500)) {
129+
furi_string_set(app->status, "starting");
130+
flytrap_live_render(app); // paint "Starting..." before the blocking send
131+
view_dispatcher_send_custom_event(app->view_dispatcher, FlytrapEventBeginSend);
132+
} else {
133+
// Keep watching: the tick resumes this flow when the board's beacon
134+
// shows up, so plugging it in auto-continues without a Back + Start.
135+
app->awaiting_board = true;
136+
furi_string_set(app->status, "noboard");
137+
flytrap_live_render(app);
138+
}
139+
return true;
119140
case FlytrapEventBeginSend:
120-
flytrap_session_start(app); // blocks while the portal streams; loading stays up
141+
flytrap_session_start(app); // blocks while the portal streams; "Starting..." stays
121142
flytrap_live_render(app);
122143
return true;
123144
case FlytrapEventRefreshView:
@@ -135,6 +156,7 @@ bool flytrap_scene_live_on_event(void* context, SceneManagerEvent event) {
135156
}
136157

137158
void flytrap_scene_live_on_exit(void* context) {
138-
UNUSED(context);
159+
FlytrapApp* app = context;
160+
app->awaiting_board = false; // stop watching once we leave the start screen
139161
// Session persists across the menu/sub-views; stopped via menu "Stop" or app exit.
140162
}

flipper/flytrap/scenes/flytrap_scene_main_menu.c

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ static void flytrap_menu_build(FlytrapApp* app) {
104104
submenu_add_item(app->submenu, "View Logs", MenuViewLogs, flytrap_menu_callback, app);
105105
submenu_add_item(app->submenu, "Settings", MenuSettings, flytrap_menu_callback, app);
106106
submenu_add_item(app->submenu, "About", MenuAbout, flytrap_menu_callback, app);
107+
108+
app->menu_shows_active = app->session_active; // remember what this build reflects
107109
}
108110

109111
void flytrap_scene_main_menu_on_enter(void* context) {
@@ -117,7 +119,15 @@ void flytrap_scene_main_menu_on_enter(void* context) {
117119
bool flytrap_scene_main_menu_on_event(void* context, SceneManagerEvent event) {
118120
FlytrapApp* app = context;
119121
if(event.type != SceneManagerEventTypeCustom) return false;
120-
if(event.event == FlytrapEventRefreshView) return true; // ignore live RX at the menu
122+
if(event.event == FlytrapEventRefreshView) {
123+
// Live RX is otherwise irrelevant at the menu, but the session can end
124+
// under us (board unplugged -> link lost) — rebuild so Start/Stop match.
125+
if(app->session_active != app->menu_shows_active) {
126+
flytrap_menu_build(app);
127+
view_dispatcher_switch_to_view(app->view_dispatcher, FlytrapViewSubmenu);
128+
}
129+
return true;
130+
}
121131

122132
scene_manager_set_scene_state(app->scene_manager, FlytrapSceneMainMenu, event.event);
123133

@@ -128,17 +138,9 @@ bool flytrap_scene_main_menu_on_event(void* context, SceneManagerEvent event) {
128138
} else if(furi_string_empty(app->portal_path)) {
129139
flytrap_show_message(app, "Flytrap", "Select a portal first.");
130140
view_dispatcher_switch_to_view(app->view_dispatcher, FlytrapViewSubmenu);
131-
} else if(!flytrap_board_present(app, 2500)) {
132-
// No point entering the loading screen if no board will answer the
133-
// handshake — it would just hang. Tell the user instead.
134-
flytrap_show_message(
135-
app,
136-
"No board detected",
137-
"Attach the ESP32 WiFi dev\nboard to the GPIO header,\nthen try again.");
138-
view_dispatcher_switch_to_view(app->view_dispatcher, FlytrapViewSubmenu);
139141
} else {
140-
// The Live scene shows a loading screen and then starts the session,
141-
// so the (blocking) portal upload doesn't freeze on a blank frame.
142+
// The Live scene walks the start flow (detect board → send portal),
143+
// painting a status screen before each blocking step.
142144
scene_manager_next_scene(app->scene_manager, FlytrapSceneLive);
143145
}
144146
return true;

0 commit comments

Comments
 (0)