From 0027970907411e20ea39672fd88f72c470714019 Mon Sep 17 00:00:00 2001 From: clement Date: Fri, 19 Jun 2026 16:30:11 +0200 Subject: [PATCH] fix(gamebook): scroll long passages + DELETE /game/file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/display_ui/display_ui.cpp | 6 ++- .../components/game_endpoint/game_endpoint.c | 47 +++++++++++++++++++ idf_zacus/components/ota_server/ota_server.c | 6 +-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/idf_zacus/components/display_ui/display_ui.cpp b/idf_zacus/components/display_ui/display_ui.cpp index 6ac4354..e5e393c 100644 --- a/idf_zacus/components/display_ui/display_ui.cpp +++ b/idf_zacus/components/display_ui/display_ui.cpp @@ -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); diff --git a/idf_zacus/components/game_endpoint/game_endpoint.c b/idf_zacus/components/game_endpoint/game_endpoint.c index 13ae0c6..809cb43 100644 --- a/idf_zacus/components/game_endpoint/game_endpoint.c +++ b/idf_zacus/components/game_endpoint/game_endpoint.c @@ -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)); diff --git a/idf_zacus/components/ota_server/ota_server.c b/idf_zacus/components/ota_server/ota_server.c index 341fb28..f31da95 100644 --- a/idf_zacus/components/ota_server/ota_server.c +++ b/idf_zacus/components/ota_server/ota_server.c @@ -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; -- 2.52.0