#include #include #include #include #include #include #include #include #include #include #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; BundleHandle result_bundle = tt_bundle_alloc(); if (result_bundle) { tt_bundle_put_bool(result_bundle, "success", success); tt_bundle_put_string(result_bundle, "path", G.path); tt_bundle_put_int32(result_bundle, "bytes", G.total); if (!success) { tt_lvgl_lock(portMAX_DELAY); const char *status = G.lbl_status ? lv_label_get_text(G.lbl_status) : "failed"; char status_copy[128]; strncpy(status_copy, status, sizeof(status_copy)-1); status_copy[sizeof(status_copy)-1]=0; tt_lvgl_unlock(); tt_bundle_put_string(result_bundle, "error", status_copy); } tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, result_bundle); } else { tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, NULL); } } 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", path); update_ui_status("File exists"); return false; } 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; }