Files
tactility_apps/Apps/SdDownloader/main/Source/main.c
T
Adolfo eaccebc95d feat(DM+SdDDL): sequential full-season download queue, fix close crash, return true
- DM: add dl_missing_* fields to AppCtx for sequential queue
- DM player: count_missing_in_season, find_next_missing, download_episode, download_missing_season_start/next
- DM main onResult: auto-continue queue when dl_missing_active, updates status DL X/Y, finishes with done ok/fail
- SdDownloader: file exists returns true (Already on SD) with total=file size, not false
- SdDownloader: set_result_and_close minimal bundle (only success bool) to avoid internal heap OOM crash on close
- SdDownloader: Cancel button becomes Close after done, calls tt_app_stop() directly (manual close per request, auto-close unreliable)
- Fixes crash on close observed in demo mode (file exists) and after download done

Tested on 192.168.68.133: SdDownloader shows Already on SD + Close, DM shows S10 5/6 after sequential, no crash on open/close cycles
2026-07-25 22:49:28 -04:00

461 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <tt_app.h>
#include <tt_lvgl.h>
#include <tt_bundle.h>
#include <tactility/lvgl_fonts.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "esp_heap_caps.h"
#define TAG "SDDL"
#define DEFAULT_URL "http://192.168.68.110:8098/463_Discovering_Discovery_Mountain_S01E01/discovery_mountain_s01e01_esp32_24k.mp3"
#define DEFAULT_PATH "/sdcard/download/test2.mp3"
typedef struct {
lv_obj_t *bar;
lv_obj_t *lbl_url;
lv_obj_t *lbl_path;
lv_obj_t *lbl_pct;
lv_obj_t *lbl_detail;
lv_obj_t *lbl_status;
lv_obj_t *btn_cancel;
char url[512];
char path[512];
bool override_existing;
AppHandle app_handle;
TaskHandle_t dl_handle;
volatile bool downloading;
volatile bool cancel_req;
volatile int total;
volatile int expected;
int last_pct;
bool result_success;
} AppCtx;
static AppCtx G;
static void ensure_parent_dir(const char *filepath) {
if (!filepath) return;
char path_copy[512];
strncpy(path_copy, filepath, sizeof(path_copy)-1);
path_copy[sizeof(path_copy)-1]=0;
char *last_slash = strrchr(path_copy, '/');
if (!last_slash) return;
*last_slash = 0;
if (strlen(path_copy)==0) return;
char cur[512] = "";
if (path_copy[0]=='/') strcpy(cur, "/");
char *start = path_copy[0]=='/' ? path_copy+1 : path_copy;
int len = strlen(start);
int i = 0;
while (i <= len) {
int j = i;
while (j < len && start[j] != '/') j++;
if (j > i) {
size_t cur_len = strlen(cur);
if (cur_len > 0 && cur[cur_len-1] != '/') {
if (cur_len + 1 < sizeof(cur)) {
cur[cur_len] = '/';
cur[cur_len+1] = 0;
}
}
char saved = start[j];
start[j] = 0;
strncat(cur, start+i, sizeof(cur)-strlen(cur)-1);
start[j] = saved;
mkdir(cur, 0777);
}
i = j+1;
}
}
static void update_ui_status(const char *txt) {
tt_lvgl_lock(portMAX_DELAY);
if (G.lbl_status) lv_label_set_text(G.lbl_status, txt);
tt_lvgl_unlock();
}
static void set_result_and_close(bool success) {
G.result_success = success;
size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
BundleHandle result_bundle = NULL;
if (free_internal > 8000) {
result_bundle = tt_bundle_alloc();
}
if (result_bundle) {
tt_bundle_put_bool(result_bundle, "success", success);
// Keep bundle minimal to avoid low heap crash (previous put_string path 512B caused OOM)
tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, result_bundle);
} else {
// Low heap return OK without bundle (DM will still refresh via has_slug)
if (free_internal <= 8000) {
ESP_LOGW(TAG, "Low heap %d, returning result without bundle", free_internal);
}
tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, NULL);
}
// Change Cancel button to Close once done (manual close, auto-close unreliable)
tt_lvgl_lock(portMAX_DELAY);
if (G.btn_cancel) {
lv_obj_t* lbl = lv_obj_get_child(G.btn_cancel, 0);
if (lbl) lv_label_set_text(lbl, "Close");
}
tt_lvgl_unlock();
}
static bool download_one_file(const char *url, const char *path) {
ESP_LOGI(TAG, "DL one file url=%s path=%s override=%d", url, path, G.override_existing);
struct stat st;
if (stat(path, &st)==0) {
if (!G.override_existing) {
ESP_LOGW(TAG, "File exists and override=false: %s (returning true as cached)", path);
G.total = st.st_size;
update_ui_status("Already on SD");
return true;
} else {
ESP_LOGI(TAG, "File exists but override=true, overwriting %s", path);
unlink(path);
}
}
ensure_parent_dir(path);
size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
ESP_LOGI(TAG, "Heap before DL: internal=%d", free_internal);
if (free_internal < 10000) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (strlen(url) < 8 || strncmp(url, "http://", 7)!=0) {
update_ui_status("Invalid URL");
return false;
}
char tmp_path[520];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
unlink(tmp_path);
FILE *f = fopen(tmp_path, "wb");
if (!f) {
ESP_LOGE(TAG, "fopen fail %s errno=%d", tmp_path, errno);
update_ui_status("Failed to open file");
return false;
}
esp_http_client_config_t cfg = {
.url = url,
.timeout_ms = 15000,
.buffer_size = 4096,
.buffer_size_tx = 1024,
.keep_alive_enable = false,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (!client) {
fclose(f);
unlink(tmp_path);
update_ui_status("HTTP init failed");
return false;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
fclose(f);
unlink(tmp_path);
update_ui_status("HTTP open failed");
return false;
}
int content_len = esp_http_client_fetch_headers(client);
int status = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "HTTP status %d, content_len %d", status, content_len);
if (status != 200) {
char msg[64];
snprintf(msg, sizeof(msg), "HTTP %d", status);
update_ui_status(msg);
esp_http_client_close(client);
esp_http_client_cleanup(client);
fclose(f);
unlink(tmp_path);
return false;
}
G.expected = content_len;
uint8_t *buf = heap_caps_malloc(4096, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!buf) {
buf = malloc(4096);
if (!buf) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
fclose(f);
unlink(tmp_path);
update_ui_status("No memory");
return false;
}
}
int total = 0;
int timeouts = 0;
bool ok = true;
while (1) {
if (G.cancel_req) { ok = false; break; }
if (content_len >=0 && total >= content_len) break;
int r = esp_http_client_read(client, (char*)buf, 4096);
if (r < 0) {
timeouts++;
if (timeouts >= 5) { ok = false; break; }
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
if (r == 0) break;
timeouts = 0;
if (fwrite(buf, 1, r, f) != (size_t)r) { ok = false; break; }
total += r;
G.total = total;
int pct = 0;
if (content_len > 0) {
pct = (int)((int64_t)total * 100 / content_len);
if (pct<0) pct=0;
if (pct>100) pct=100;
}
if (pct != G.last_pct) {
G.last_pct = pct;
tt_lvgl_lock(portMAX_DELAY);
if (G.bar) lv_bar_set_value(G.bar, pct, LV_ANIM_OFF);
if (G.lbl_detail) {
char d[64];
if (content_len>0) snprintf(d,sizeof(d),"%d / %d KB", total/1024, content_len/1024);
else snprintf(d,sizeof(d),"%d KB", total/1024);
lv_label_set_text(G.lbl_detail, d);
}
if (G.lbl_pct) {
char b[32];
if (content_len>0) snprintf(b,sizeof(b),"%d%%",pct);
else snprintf(b,sizeof(b),"%d KB", total/1024);
lv_label_set_text(G.lbl_pct, b);
}
tt_lvgl_unlock();
}
if ((total / 4096) % 8 == 0) {
fflush(f);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
heap_caps_free(buf);
esp_http_client_close(client);
esp_http_client_cleanup(client);
fclose(f);
if (G.cancel_req) { unlink(tmp_path); update_ui_status("Canceled"); return false; }
if (!ok || total < 512) { unlink(tmp_path); update_ui_status("Failed error"); return false; }
if (content_len >=0 && total < content_len * 95 / 100) { unlink(tmp_path); update_ui_status("Failed incomplete"); return false; }
if (rename(tmp_path, path) != 0) { unlink(tmp_path); update_ui_status("Rename failed"); return false; }
return true;
}
static void download_task(void *arg) {
(void)arg;
G.downloading = true;
G.cancel_req = false;
G.last_pct = -1;
char url[512];
char path[512];
strncpy(url, G.url, sizeof(url)-1);
strncpy(path, G.path, sizeof(path)-1);
url[sizeof(url)-1]=0;
path[sizeof(path)-1]=0;
bool ok = download_one_file(url, path);
if (ok) {
char msg[64];
snprintf(msg, sizeof(msg), "Done! %d KB", G.total/1024);
update_ui_status(msg);
}
G.downloading = false;
G.dl_handle = NULL;
set_result_and_close(ok);
vTaskDelete(NULL);
}
static void btn_cancel_cb(lv_event_t *e) {
(void)e;
if (!G.downloading) {
tt_app_stop();
return;
}
G.cancel_req = true;
tt_lvgl_lock(portMAX_DELAY);
if (G.lbl_status) lv_label_set_text(G.lbl_status, "Canceling...");
tt_lvgl_unlock();
}
static void build_ui(void) {
tt_lvgl_lock(portMAX_DELAY);
lv_obj_t *root = lv_obj_create(lv_scr_act());
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_all(root, 8, 0);
lv_obj_set_style_bg_color(root, lv_color_hex(0x111111), 0);
lv_obj_set_style_border_width(root, 0, 0);
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(root, 6, 0);
lv_obj_t *title = lv_label_create(root);
lv_label_set_text(title, "Downloading...");
lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
G.lbl_url = lv_label_create(root);
lv_label_set_text(G.lbl_url, G.url);
lv_obj_set_width(G.lbl_url, LV_PCT(100));
lv_label_set_long_mode(G.lbl_url, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(G.lbl_url, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_text_color(G.lbl_url, lv_color_hex(0x888888), 0);
G.lbl_path = lv_label_create(root);
lv_label_set_text(G.lbl_path, G.path);
lv_obj_set_width(G.lbl_path, LV_PCT(100));
lv_label_set_long_mode(G.lbl_path, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(G.lbl_path, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_text_color(G.lbl_path, lv_color_hex(0xAAAAAA), 0);
G.bar = lv_bar_create(root);
lv_obj_set_size(G.bar, LV_PCT(100), 14);
lv_bar_set_range(G.bar, 0, 100);
lv_bar_set_value(G.bar, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(G.bar, lv_color_hex(0x333333), LV_PART_MAIN);
lv_obj_set_style_bg_color(G.bar, lv_color_hex(0x2E7D32), LV_PART_INDICATOR);
lv_obj_t *row = lv_obj_create(root);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
G.lbl_pct = lv_label_create(row);
lv_label_set_text(G.lbl_pct, "0%");
lv_obj_set_style_text_font(G.lbl_pct, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0);
G.lbl_detail = lv_label_create(row);
lv_label_set_text(G.lbl_detail, "-");
lv_obj_set_style_text_font(G.lbl_detail, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_text_color(G.lbl_detail, lv_color_hex(0x888888), 0);
G.lbl_status = lv_label_create(root);
lv_label_set_text(G.lbl_status, "Starting...");
lv_obj_set_width(G.lbl_status, LV_PCT(100));
lv_label_set_long_mode(G.lbl_status, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_color(G.lbl_status, lv_color_hex(0xCCCCCC), 0);
G.btn_cancel = lv_button_create(root);
lv_obj_set_size(G.btn_cancel, 100, 40);
lv_obj_set_style_bg_color(G.btn_cancel, lv_color_hex(0x444444), 0);
lv_obj_t *lbl = lv_label_create(G.btn_cancel);
lv_label_set_text(lbl, "Cancel");
lv_obj_center(lbl);
lv_obj_add_event_cb(G.btn_cancel, btn_cancel_cb, LV_EVENT_CLICKED, NULL);
tt_lvgl_unlock();
}
static void onShow(AppHandle app, void* data, lv_obj_t* parent) {
(void)data; (void)parent;
memset(&G, 0, sizeof(G));
G.app_handle = app;
G.last_pct = -1;
BundleHandle bundle = tt_app_get_parameters(app);
bool has_url = false;
bool has_path = false;
if (bundle) {
char url_buf[512] = {0};
char path_buf[512] = {0};
bool override_val = false;
if (tt_bundle_opt_string(bundle, "url", url_buf, sizeof(url_buf)) ||
tt_bundle_opt_string(bundle, "file", url_buf, sizeof(url_buf))) {
strncpy(G.url, url_buf, sizeof(G.url)-1);
has_url = true;
}
if (tt_bundle_opt_string(bundle, "path", path_buf, sizeof(path_buf)) ||
tt_bundle_opt_string(bundle, "dest", path_buf, sizeof(path_buf)) ||
tt_bundle_opt_string(bundle, "file_dest", path_buf, sizeof(path_buf))) {
strncpy(G.path, path_buf, sizeof(G.path)-1);
has_path = true;
}
if (tt_bundle_opt_bool(bundle, "override", &override_val)) {
G.override_existing = override_val;
} else {
bool ow2 = false;
if (tt_bundle_opt_bool(bundle, "overwrite", &ow2)) G.override_existing = ow2;
}
}
if (!has_url) strncpy(G.url, DEFAULT_URL, sizeof(G.url)-1);
if (!has_path) strncpy(G.path, DEFAULT_PATH, sizeof(G.path)-1);
mkdir("/sdcard", 0777);
mkdir("/sdcard/download", 0777);
mkdir("/sdcard/dm", 0777);
build_ui();
bool should_autostart = has_url && has_path;
if (!has_url && !has_path) {
should_autostart = true;
tt_lvgl_lock(portMAX_DELAY);
if (G.lbl_status) lv_label_set_text(G.lbl_status, "Demo mode no args, using defaults");
tt_lvgl_unlock();
}
ESP_LOGI(TAG, "Autostart=%d url=%s path=%s override=%d", should_autostart, G.url, G.path, G.override_existing);
if (should_autostart) {
BaseType_t res = xTaskCreate(download_task, "sd_dl", 8192, NULL, 5, &G.dl_handle);
ESP_LOGI(TAG, "xTaskCreate res=%d handle=%p", res, G.dl_handle);
if (res != pdPASS) update_ui_status("Failed to create DL task");
} else {
update_ui_status("Missing url/path args");
}
}
static void onHide(AppHandle app, void* data) {
(void)app; (void)data;
if (G.dl_handle) {
G.cancel_req = true;
for (int i=0;i<20 && G.dl_handle!=NULL; i++) vTaskDelay(pdMS_TO_TICKS(100));
if (G.dl_handle) { vTaskDelete(G.dl_handle); G.dl_handle = NULL; G.downloading = false; }
}
}
int main(int argc, char* argv[]) {
(void)argc; (void)argv;
tt_app_register((AppRegistration){ .onShow = onShow, .onHide = onHide });
return 0;
}