Compare commits

...

1 Commits

Author SHA1 Message Date
clement 0027970907 fix(gamebook): scroll long passages + DELETE /game/file
CI / platformio (push) Failing after 5m21s
CI / platformio (pull_request) Failing after 5m25s
Context:
Two issues surfaced with the longer kid stories: (1) long passages were
clipped on screen — the body label wrapped in a fixed 196px band and
the tail was simply cut off; (2) old orphan WAVs (demo + archived
adventures) piled up on the SD with no way to remove them, since the
master had POST /game/file but no delete.

Approach:
Make the gamebook body label scroll vertically so the whole passage
goes by, and add a DELETE verb to /game/file mirroring the POST routing
so the host can clean stale assets off the SD.

Changes:
- display_ui: gamebook body label LV_LABEL_LONG_WRAP ->
  LV_LABEL_LONG_SCROLL_CIRCULAR; body buffer 512 -> 1100 so a full
  ~1000-char passage is held and scrolled, not truncated.
- game_endpoint: new DELETE /game/file?path=sd/<…>|apps/<…> (same
  whitelist/traversal guard as POST; unlink + 200/404).
- ota_server: max_uri_handlers 22 -> 24 for the extra verb.

Impact:
Long passages are fully readable on the LCD (they scroll), and the SD
gamebook folder can be cleaned remotely. Hardware-validated: 121 orphan
files deleted, library still opens 6 stories.
2026-06-19 16:30:11 +02:00
3 changed files with 54 additions and 5 deletions
@@ -256,7 +256,7 @@ static lv_obj_t *s_gb_body; // full passage text, wrapped
static lv_obj_t *s_gb_menu; // choice lines, bottom
static volatile bool s_gamebook_open = false;
static char s_gb_title_buf[64];
static char s_gb_body_buf[512];
static char s_gb_body_buf[1100];
static char s_gb_menu_buf[160];
// ── Gamebook library (tile grid) ────────────────────────────────────────────
@@ -837,7 +837,9 @@ static void build_gamebook_screen(void) {
s_gb_body = lv_label_create(s_scr_gamebook);
lv_obj_set_style_text_color(s_gb_body, lv_color_hex(COL_VALUE), 0);
lv_obj_set_style_text_font(s_gb_body, &lv_font_montserrat_14, 0);
lv_label_set_long_mode(s_gb_body, LV_LABEL_LONG_WRAP);
// Wrap to width AND scroll vertically so long passages are never clipped:
// the whole text scrolls slowly through the band while the WAV narrates it.
lv_label_set_long_mode(s_gb_body, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_size(s_gb_body, DUI_HOR_RES - 24, 196);
lv_label_set_text(s_gb_body, "");
lv_obj_align(s_gb_body, LV_ALIGN_TOP_LEFT, 12, 44);
@@ -1173,6 +1173,43 @@ static esp_err_t handle_file_post(httpd_req_t *req) {
return send_json(req, "200 OK", resp);
}
// ─── DELETE /game/file?path=sd/<…>|apps/<…> — remove a staged file ───────────
// Same path routing/whitelist as the POST handler. Lets the host clean stale
// assets (e.g. orphan gamebook WAVs) off the SD without reflashing.
static esp_err_t handle_file_delete(httpd_req_t *req) {
char query[160], path_param[96];
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK ||
httpd_query_key_value(query, "path", path_param,
sizeof(path_param)) != ESP_OK) {
return send_error(req, "400 Bad Request", "missing path param");
}
const bool to_sd = (strncmp(path_param, "sd/", 3) == 0);
const bool to_apps = (strncmp(path_param, "apps/", 5) == 0);
if ((!to_sd && !to_apps) || strstr(path_param, "..") ||
path_param[strlen(path_param) - 1] == '/') {
return send_error(req, "403 Forbidden", "path must be under apps/ or sd/");
}
char full[192];
if (to_sd) {
if (!sd_storage_ready()) {
return send_error(req, "503 Service Unavailable", "sd_not_mounted");
}
snprintf(full, sizeof(full), "/sdcard/%s", path_param + 3);
} else {
if (mount_storage_lazy() != ESP_OK) {
return send_error(req, "503 Service Unavailable", "storage_unavailable");
}
snprintf(full, sizeof(full), "/littlefs/%s", path_param);
}
if (unlink(full) != 0) {
return send_error(req, "404 Not Found", "remove failed");
}
ESP_LOGI(TAG, "file removed: %s", full);
char resp[192];
snprintf(resp, sizeof(resp), "{\"status\":\"ok\",\"removed\":\"%s\"}", path_param);
return send_json(req, "200 OK", resp);
}
void game_endpoint_get_scene(scene_binding_t *out) {
if (!out) return;
// Lock-free snapshot, same class as get_puzzle_status: written on the
@@ -1244,6 +1281,12 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
.handler = handle_file_post,
.user_ctx = NULL,
};
static const httpd_uri_t uri_file_delete = {
.uri = "/game/file",
.method = HTTP_DELETE,
.handler = handle_file_delete,
.user_ctx = NULL,
};
static const httpd_uri_t uri_espnow_cmd_post = {
.uri = "/game/espnow/cmd",
.method = HTTP_POST,
@@ -1326,6 +1369,10 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
if (err != ESP_OK) {
ESP_LOGW(TAG, "register POST /game/step: %s", esp_err_to_name(err));
}
err = httpd_register_uri_handler(server, &uri_file_delete);
if (err != ESP_OK) {
ESP_LOGW(TAG, "register DELETE /game/file: %s", esp_err_to_name(err));
}
err = httpd_register_uri_handler(server, &uri_file_post);
if (err != ESP_OK) {
ESP_LOGW(TAG, "register /game/file: %s", esp_err_to_name(err));
+3 -3
View File
@@ -310,9 +310,9 @@ esp_err_t ota_server_init(void) {
// HTTP server config
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = OTA_SERVER_PORT;
config.max_uri_handlers = 22; // ota + voice_hook + game (incl. /game/step,
// /game/puzzle_state, /game/file, relay,
// /game/gamebook)
config.max_uri_handlers = 24; // ota + voice_hook + game (incl. /game/step,
// /game/puzzle_state, /game/file POST+DELETE,
// relay, /game/gamebook)
// + headroom
config.uri_match_fn = httpd_uri_match_wildcard;
config.stack_size = 8192;