@@ -0,0 +1,451 @@
# 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 ;
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 ;
}