diff --git a/.gitignore b/.gitignore index f2503a86..606857ab 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ pixelpilot_config.h .vscode/ build/ build_sim/ +build-sim/ .apt_cache/ output/ debian-*-generic-arm64.tar.xz diff --git a/CMakeLists.txt b/CMakeLists.txt index 54db9a1c..b7e806bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,46 +39,24 @@ include_directories(${PNG_INCLUDE_DIR}) # Simulator set(SIMULATOR_SOURCES src/simulator.c + src/gsmenu/column_stack.h + src/gsmenu/column_stack.c + src/gsmenu/colmenu.h + src/gsmenu/colmenu.c + src/gsmenu/colmenu_pages.h + src/gsmenu/colmenu_pages.c src/gsmenu/executor.h src/gsmenu/executor.c - src/gsmenu/air_telemetry.h - src/gsmenu/air_telemetry.c src/gsmenu/air_txprofiles.h src/gsmenu/air_txprofiles.c - src/gsmenu/air_camera.h - src/gsmenu/air_camera.c - src/gsmenu/air_actions.h - src/gsmenu/air_actions.c - src/gsmenu/gs_wifi.h - src/gsmenu/gs_wifi.c - src/gsmenu/ui.h src/gsmenu/helper.h src/gsmenu/helper.c - src/gsmenu/gs_main.c - src/gsmenu/gs_main.h - src/gsmenu/gs_dvr.h - src/gsmenu/gs_dvr.c src/gsmenu/gs_dvrplayer.h src/gsmenu/gs_dvrplayer.c - src/gsmenu/gs_wfbng.h - src/gsmenu/gs_wfbng.c - src/gsmenu/gs_apfpv.h - src/gsmenu/gs_apfpv.c - src/gsmenu/gs_system.h - src/gsmenu/gs_system.c - src/gsmenu/gs_actions.h - src/gsmenu/gs_actions.c src/gsmenu/gs_scripts.h src/gsmenu/gs_scripts.c src/gsmenu/gs_connection_checker.h src/gsmenu/gs_connection_checker.c - src/gsmenu/air_wfbng.h - src/gsmenu/air_wfbng.c - src/gsmenu/air_alink.h - src/gsmenu/air_alink.c - src/gsmenu/air_aalink.h - src/gsmenu/air_aalink.c - src/gsmenu/ui.c src/gsmenu/styles.h src/gsmenu/styles.c src/lvosd.h @@ -96,45 +74,24 @@ set(LIB_SOURCE_FILES src/drm.c src/gsmenu/executor.h src/gsmenu/executor.c - src/gsmenu/air_telemetry.c src/gsmenu/air_txprofiles.h src/gsmenu/air_txprofiles.c - src/gsmenu/air_camera.h - src/gsmenu/air_camera.c - src/gsmenu/air_actions.h - src/gsmenu/air_actions.c - src/gsmenu/gs_wifi.h - src/gsmenu/gs_wifi.c - src/gsmenu/ui.h src/gsmenu/helper.h src/gsmenu/helper.c - src/gsmenu/gs_main.c - src/gsmenu/gs_main.h - src/gsmenu/gs_dvr.h - src/gsmenu/gs_dvr.c src/gsmenu/gs_dvrplayer.h src/gsmenu/gs_dvrplayer.c - src/gsmenu/gs_wfbng.h - src/gsmenu/gs_wfbng.c - src/gsmenu/gs_apfpv.h - src/gsmenu/gs_apfpv.c - src/gsmenu/gs_system.h - src/gsmenu/gs_system.c - src/gsmenu/air_wfbng.h - src/gsmenu/air_wfbng.c - src/gsmenu/air_alink.h - src/gsmenu/air_alink.c - src/gsmenu/air_aalink.h - src/gsmenu/air_aalink.c - src/gsmenu/gs_actions.h - src/gsmenu/gs_actions.c src/gsmenu/gs_scripts.h src/gsmenu/gs_scripts.c src/gsmenu/gs_connection_checker.h src/gsmenu/gs_connection_checker.c src/gsmenu/styles.h src/gsmenu/styles.c - src/gsmenu/ui.c + src/gsmenu/column_stack.h + src/gsmenu/column_stack.c + src/gsmenu/colmenu.h + src/gsmenu/colmenu.c + src/gsmenu/colmenu_pages.h + src/gsmenu/colmenu_pages.c src/lvosd.h src/lvosd.c src/menu.h diff --git a/gsmenu.sh b/gsmenu.sh index 78a6b615..44d0f00d 100755 --- a/gsmenu.sh +++ b/gsmenu.sh @@ -394,6 +394,9 @@ case "$@" in echo 1.0 emit_values "0.5 1.0" ;; + "get gs system gs_live_colortrans") + echo 0 + ;; "get gs system rec_fps") echo 60 emit_values "60\n90\n120" @@ -422,6 +425,12 @@ case "$@" in echo -n "10000" emit_values "5000\n10000\n15000\n20000\n25000\n30000\n35000\n40000\n45000\n50000" ;; + "get gs system rec_enabled"*) + echo 0 + ;; + "get gs system dvr_osd"*) + echo 0 + ;; "set gs system rx_codec"*) : ;; "set gs system rx_mode"*) : ;; @@ -429,6 +438,7 @@ case "$@" in "set gs system connector"*) : ;; "set gs system resolution"*) : ;; "set gs system video_scale"*) : ;; + "set gs system gs_live_colortrans"*) : ;; "set gs system rec_fps"*) : ;; "set gs system rec_enabled"*) : ;; "set gs system dvr_mode"*) : ;; @@ -485,6 +495,7 @@ case "$@" in "set gs wifi connect"*) : ;; "set gs wifi disconnect"*) : ;; + "set gs wifi forget"*) : ;; # integrator: forget/remove the saved network ($4 = SSID) "set gs wifi wlan"*) : ;; "set gs wifi hotspot"*) : ;; diff --git a/src/gsmenu/air_aalink.c b/src/gsmenu/air_aalink.c deleted file mode 100644 index 1d354489..00000000 --- a/src/gsmenu/air_aalink.c +++ /dev/null @@ -1,74 +0,0 @@ -#include -#include -#include -#include "executor.h" -#include "styles.h" -#include "lvgl/lvgl.h" -#include "helper.h" -#include "../input.h" - -extern lv_group_t * default_group; -extern lv_indev_t * indev_drv; -extern gsmenu_control_mode_t control_mode; - -lv_obj_t * ap_fpv_channel; -lv_obj_t * txPower; -lv_obj_t * mcsShift; -lv_obj_t * temp; -lv_obj_t * throughput; -lv_obj_t * osdscale; -lv_obj_t * osdlevel; -lv_obj_t * osdsignalbars; -lv_obj_t * mcssource; - -void create_air_aalink_menu(lv_obj_t * parent) { - - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "aalink"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "WiFi Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - ap_fpv_channel = create_dropdown(cont,LV_SYMBOL_SETTINGS, "Channel","","channel",menu_page_data,false); - - create_text(parent, NULL, "AALink Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - txPower = create_slider(cont,LV_SYMBOL_SETTINGS,"VTX Power Output","SCALE_TX_POWER",menu_page_data,false,1); - mcsShift = create_slider(cont,LV_SYMBOL_SETTINGS,"Link resilience (dB)","THRESH_SHIFT",menu_page_data,false,0); - osdscale = create_slider(cont,LV_SYMBOL_SETTINGS,"OSD Size","OSD_SCALE",menu_page_data,false,1); - osdlevel = create_dropdown(cont,LV_SYMBOL_SETTINGS,"OSD Level","","OSD_LEVEL", menu_page_data, false); - osdsignalbars = create_switch(cont,LV_SYMBOL_SETTINGS,"OSD Signal Bars","SHOW_SIGNAL_BARS", menu_page_data, false); - throughput = create_slider(cont,LV_SYMBOL_SETTINGS,"Maximum Throughput (%)","THROUGHPUT_PCT",menu_page_data,false,0); - temp = create_slider(cont,LV_SYMBOL_SETTINGS,"Temp Throttle Threshold (°C)","HIGH_TEMP",menu_page_data,false,0); - mcssource = create_dropdown(cont,LV_SYMBOL_SETTINGS, "LQ Consideration","","MCS_SOURCE",menu_page_data,false); - - - add_entry_to_menu_page(menu_page_data,"Loading Channel ...", ap_fpv_channel, reload_dropdown_value ); - add_entry_to_menu_page(menu_page_data,"Loading VTX Power Output ...", txPower, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading Link resilience (dB) ...", mcsShift, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading OSD Size ...", osdscale, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading OSD Level ...", osdlevel, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading OSD Signal Bars ...", osdsignalbars, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading Maximum Throughput ...", throughput, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading Temp Throttle Threshold (°C)", temp, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading LQ Consideration ...", mcssource, reload_dropdown_value); - - lv_group_set_default(default_group); -} \ No newline at end of file diff --git a/src/gsmenu/air_aalink.h b/src/gsmenu/air_aalink.h deleted file mode 100644 index cfca1cfa..00000000 --- a/src/gsmenu/air_aalink.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_air_aalink_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/air_actions.c b/src/gsmenu/air_actions.c deleted file mode 100644 index 5eea9f12..00000000 --- a/src/gsmenu/air_actions.c +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include - -#include "lvgl/lvgl.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" -#include "helper.h" -#include "air_actions.h" - -extern lv_group_t * default_group; -lv_obj_t * air_reboot; -lv_obj_t * air_custom_action; - -void create_air_actions_menu(lv_obj_t * parent) { - - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "actions"); - menu_page_data->page_load_callback = NULL; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - - air_reboot = create_button(section, "Reboot"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(air_reboot,0,&lv_button_class),generic_button_callback,LV_EVENT_CLICKED,menu_page_data); - - for (size_t i = 0; i < airactions_count; i++) { - air_custom_action = create_button(section, airactions[i].label); - lv_obj_add_event_cb(lv_obj_get_child_by_type(air_custom_action,0,&lv_button_class),custom_actions_cb,LV_EVENT_CLICKED,&airactions[i]); - } - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/air_actions.h b/src/gsmenu/air_actions.h deleted file mode 100644 index 3c0d50fa..00000000 --- a/src/gsmenu/air_actions.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include -#include "../menu.h" -extern MenuAction airactions[MAX_ACTIONS]; -extern size_t airactions_count; - -void create_air_actions_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/air_alink.c b/src/gsmenu/air_alink.c deleted file mode 100644 index 4ac941e8..00000000 --- a/src/gsmenu/air_alink.c +++ /dev/null @@ -1,134 +0,0 @@ -#include -#include -#include -#include "executor.h" -#include "styles.h" -#include "lvgl/lvgl.h" -#include "helper.h" -#include "../input.h" - -extern lv_group_t * default_group; -extern lv_indev_t * indev_drv; -extern gsmenu_control_mode_t control_mode; - -extern lv_obj_t * txprofiles_screen; -extern lv_group_t * tx_profile_group; -lv_obj_t * txprofiles; - -// # set desired power output (0 pitmode, 4 highest power)(scales with MCS) -lv_obj_t * power_level_0_to_4; -// ### if gs heartbeat lost for x ms, set link low (fallback) default 1000 -lv_obj_t * fallback_ms; -// # keep link low for min x s default 1 -lv_obj_t * hold_fallback_mode_s; -// ### limit time between any link change and the next default 200 -lv_obj_t * min_between_changes_ms; -// # wait x seconds before increasing link speed default 3 -lv_obj_t * hold_modes_down_s; - -// ### smooth out rssi/snr readings for link increase / decrease -lv_obj_t * hysteresis_percent; // default 5 -lv_obj_t * hysteresis_percent_down; // default 5 -lv_obj_t * exp_smoothing_factor; // default 0.1 -lv_obj_t * exp_smoothing_factor_down; // default 1.0 - -// ### allow lost GS packet to request new keyframe -lv_obj_t * allow_request_keyframe; // default 1 -// # allow drone driver-tx_dropped to request new keyframe -lv_obj_t * allow_rq_kf_by_tx_d; // default 1 -// # how often to check driver-xtx -lv_obj_t * check_xtx_period_ms; // default 2250 -// # limit time between keyframe requests -lv_obj_t * request_keyframe_interval_ms; // default 1112 -// # request a keyframe at every link change -lv_obj_t * idr_every_change; // default 0 - -// ### enable higher quality in center of image -// roi_focus_mode=0 - -// ### allow dynamic fec -// allow_dynamic_fec=0 -// # by 1 decreasing k, or 0 increasing n -// fec_k_adjust=1 -// # disable when bitrate is <= 4000 -// spike_fix_dynamic_fec=0 - -// ### attempt to help encoder bitrate spikes by strategically lowering FPS when on high resolutions -// allow_spike_fix_fps=0 -// # reduce bitrate on driver-tx dropped for 1 second or until xtx stops -// allow_xtx_reduce_bitrate=1 -// # reduce bitrate to -// xtx_reduce_bitrate_factor=0.8 - -// ### how much info on OSD (0 to 6). 4 = all on one line. 6 = all on multiple lines -lv_obj_t * osd_level; // default 0 -// # make custom text smaller/bigger -lv_obj_t * multiply_font_size_by; // default 1 - -static void txprofiles_callback(lv_event_t * e) -{ - lv_screen_load(txprofiles_screen); - lv_indev_set_group(indev_drv,tx_profile_group); - lv_obj_t * first_obj = find_first_focusable_obj(txprofiles_screen); - lv_group_focus_obj(first_obj); -} - -void create_air_alink_menu(lv_obj_t * parent) { - - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "alink"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - txprofiles = create_button(cont, "TX-Profiles"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(txprofiles,0,&lv_button_class),txprofiles_callback,LV_EVENT_CLICKED,NULL); - - power_level_0_to_4 = create_dropdown(cont,LV_SYMBOL_SETTINGS, "Desired power output (0 pitmode, 4 highest power)","","power_level_0_to_4",menu_page_data,false); - fallback_ms = create_slider(cont,LV_SYMBOL_SETTINGS,"If GS heartbeat lost for x ms, set link low (fallback) (ms)","fallback_ms",menu_page_data,false,0); - hold_fallback_mode_s = create_slider(cont,LV_SYMBOL_SETTINGS,"Keep link low for min x s","hold_fallback_mode_s",menu_page_data,false,0); - min_between_changes_ms = create_slider(cont,LV_SYMBOL_SETTINGS, "Limit time between any link change (ms)","min_between_changes_ms",menu_page_data,false,0); - hold_modes_down_s = create_slider(cont,LV_SYMBOL_SETTINGS, "Wait x seconds before increasing link speed (s)","hold_modes_down_s",menu_page_data,false,0); - hysteresis_percent = create_slider(cont,LV_SYMBOL_SETTINGS, "Hysteresis (%)","hysteresis_percent",menu_page_data,false,0); - hysteresis_percent_down = create_slider(cont,LV_SYMBOL_SETTINGS, "Hysteresis down (%)","hysteresis_percent_down",menu_page_data,false,0); - exp_smoothing_factor = create_slider(cont,LV_SYMBOL_SETTINGS, "exp_smoothing_factor","exp_smoothing_factor",menu_page_data,false,1); - exp_smoothing_factor_down = create_slider(cont,LV_SYMBOL_SETTINGS, "exp_smoothing_factor_down","exp_smoothing_factor_down",menu_page_data,false,1); - allow_request_keyframe = create_switch(cont,LV_SYMBOL_SETTINGS,"Allow lost GS packet to request new keyframe","allow_request_keyframe", menu_page_data,false); - allow_rq_kf_by_tx_d = create_switch(cont,LV_SYMBOL_SETTINGS,"Allow drone driver-tx_dropped to request new keyframe","allow_rq_kf_by_tx_d", menu_page_data,false); - check_xtx_period_ms = create_slider(cont,LV_SYMBOL_SETTINGS, "How often to check driver-xtx","check_xtx_period_ms",menu_page_data,false,0); - request_keyframe_interval_ms = create_slider(cont,LV_SYMBOL_SETTINGS, "Limit time between keyframe requests","request_keyframe_interval_ms",menu_page_data,false,0); - idr_every_change = create_switch(cont,LV_SYMBOL_SETTINGS,"Request a keyframe at every link change","idr_every_change", menu_page_data,false); - osd_level = create_dropdown(cont,LV_SYMBOL_SETTINGS, "OSD Details 4 = all on one line. 6 = all on multiple lines","","osd_level",menu_page_data,false); - multiply_font_size_by = create_slider(cont,LV_SYMBOL_SETTINGS, "Font Size","multiply_font_size_by",menu_page_data,false,1); - - add_entry_to_menu_page(menu_page_data,"Loading power_level_0_to_4 ...", power_level_0_to_4, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading fallback_ms ...", fallback_ms, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading hold_fallback_mode_s ...", hold_fallback_mode_s, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading min_between_changes_ms ...", min_between_changes_ms, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading hold_modes_down_s ...", hold_modes_down_s, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading hysteresis_percent ...", hysteresis_percent, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading hysteresis_percent_down ...", hysteresis_percent_down, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading exp_smoothing_factor ...", exp_smoothing_factor, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading exp_smoothing_factor_down ...", exp_smoothing_factor_down, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading allow_request_keyframe ...", allow_request_keyframe, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading allow_rq_kf_by_tx_d ...", allow_rq_kf_by_tx_d, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading check_xtx_period_ms ...", check_xtx_period_ms, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading request_keyframe_interval_ms ...", request_keyframe_interval_ms, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading idr_every_change ...", idr_every_change, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading osd_level ...", osd_level, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading multiply_font_size_by ...", multiply_font_size_by, reload_slider_value); - - lv_group_set_default(default_group); -} \ No newline at end of file diff --git a/src/gsmenu/air_alink.h b/src/gsmenu/air_alink.h deleted file mode 100644 index 9fb796bc..00000000 --- a/src/gsmenu/air_alink.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_air_alink_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/air_camera.c b/src/gsmenu/air_camera.c deleted file mode 100644 index 6615facb..00000000 --- a/src/gsmenu/air_camera.c +++ /dev/null @@ -1,283 +0,0 @@ -#include -#include -#include -#include "executor.h" -#include "styles.h" -#include "lvgl/lvgl.h" -#include "helper.h" - -extern lv_group_t * default_group; -extern lv_obj_t * menu; -extern lv_indev_t * indev_drv; -extern lv_obj_t * sub_air_camera_page; - -lv_obj_t * mirror; -lv_obj_t * flip; -lv_obj_t * contrast; -lv_obj_t * hue; -lv_obj_t * saturation; -lv_obj_t * luminace; -lv_obj_t * video_mode; -lv_obj_t * size; -lv_obj_t * fps; -lv_obj_t * bitrate; -lv_obj_t * video_codec; -lv_obj_t * gopsize; -lv_obj_t * rc_mode; -lv_obj_t * rec_enable; -lv_obj_t * rec_split; -lv_obj_t * rec_maxusage; -lv_obj_t * exposure; -lv_obj_t * antiflicker; -lv_obj_t * sensor_file; -lv_obj_t * fpv_enable; -lv_obj_t * noiselevel; - - -extern lv_obj_t * rec_fps; -void air_rec_fps_cb(lv_event_t *e) { - char val[100] = ""; - - lv_obj_t *ta = lv_event_get_target(e); - lv_dropdown_get_selected_str(ta, val, sizeof(val) - 1); - - lv_obj_t *obj = lv_obj_get_child_by_type(rec_fps, 0, &lv_dropdown_class); - int index = lv_dropdown_get_option_index(obj,val); - lv_dropdown_set_selected(obj, index); - lv_obj_send_event(obj,LV_EVENT_VALUE_CHANGED,NULL); -} - -// Back handler for camera sub-pages: go up to Camera landing page rather than -// all the way to the main menu. -static void air_camera_sub_back_handler(lv_event_t * e) { - lv_key_t key = lv_event_get_key(e); - if (key == LV_KEY_HOME) { - menu_page_data_t *cam_data = (menu_page_data_t *)lv_obj_get_user_data(sub_air_camera_page); - lv_menu_set_page(menu, sub_air_camera_page); - if (cam_data) lv_indev_set_group(indev_drv, cam_data->indev_group); - } -} - -// Replace generic_back_event_handler on the focusable inner child of a widget -// container with air_camera_sub_back_handler. -static void use_sub_back_handler(lv_obj_t *container) { - lv_obj_t *inner = lv_obj_get_child_by_type(container, 0, &lv_dropdown_class); - if (!inner) inner = lv_obj_get_child_by_type(container, 0, &lv_switch_class); - if (!inner) inner = lv_obj_get_child_by_type(container, 0, &lv_slider_class); - if (!inner) return; - lv_obj_remove_event_cb(inner, generic_back_event_handler); - lv_obj_add_event_cb(inner, air_camera_sub_back_handler, LV_EVENT_KEY, NULL); -} - -static menu_page_data_t * make_camera_page_data(lv_obj_t *parent) { - menu_page_data_t *mpd = malloc(sizeof(menu_page_data_t)); - strcpy(mpd->type, "air"); - strcpy(mpd->page, "camera"); - mpd->page_load_callback = generic_page_load_callback; - mpd->indev_group = lv_group_create(); - mpd->entry_count = 0; - mpd->page_entries = NULL; - lv_group_set_default(mpd->indev_group); - lv_obj_set_user_data(parent, mpd); - return mpd; -} - -void create_air_camera_video_menu(lv_obj_t * parent) { - menu_page_data_t *menu_page_data = make_camera_page_data(parent); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "Video", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - size = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Size", "", "size", menu_page_data, false); - use_sub_back_handler(size); - video_mode = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Video Mode", "", "video_mode", menu_page_data, false); - use_sub_back_handler(video_mode); - fps = create_dropdown(cont, LV_SYMBOL_SETTINGS, "FPS", "", "fps", menu_page_data, false); - // change rec fps when changing camera fps - lv_obj_add_event_cb(lv_obj_get_child_by_type(fps, 0, &lv_dropdown_class), air_rec_fps_cb, LV_EVENT_VALUE_CHANGED, fps); - use_sub_back_handler(fps); - bitrate = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Bitrate", "", "bitrate", menu_page_data, false); - use_sub_back_handler(bitrate); - video_codec = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Codec", "", "codec", menu_page_data, false); - use_sub_back_handler(video_codec); - gopsize = create_slider(cont, LV_SYMBOL_SETTINGS, "Gopsize", "gopsize", menu_page_data, false, 0); - use_sub_back_handler(gopsize); - rc_mode = create_dropdown(cont, LV_SYMBOL_SETTINGS, "RC Mode", "", "rc_mode", menu_page_data, false); - use_sub_back_handler(rc_mode); - - add_entry_to_menu_page(menu_page_data, "Loading size ...", size, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading video_mode ...", video_mode, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading fps ...", fps, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading bitrate ...", bitrate, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading video_codec ...", video_codec, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading gopsize ...", gopsize, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading rc_mode ...", rc_mode, reload_dropdown_value); - - lv_group_set_default(default_group); -} - -void create_air_camera_image_menu(lv_obj_t * parent) { - menu_page_data_t *menu_page_data = make_camera_page_data(parent); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "Image", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - mirror = create_switch(cont, LV_SYMBOL_SETTINGS, "Mirror", "mirror", menu_page_data, false); - use_sub_back_handler(mirror); - flip = create_switch(cont, LV_SYMBOL_SETTINGS, "Flip", "flip", menu_page_data, false); - use_sub_back_handler(flip); - contrast = create_slider(cont, LV_SYMBOL_SETTINGS, "Contrast", "contrast", menu_page_data, false, 0); - use_sub_back_handler(contrast); - hue = create_slider(cont, LV_SYMBOL_SETTINGS, "Hue", "hue", menu_page_data, false, 0); - use_sub_back_handler(hue); - saturation = create_slider(cont, LV_SYMBOL_SETTINGS, "Saturation", "saturation", menu_page_data, false, 0); - use_sub_back_handler(saturation); - luminace = create_slider(cont, LV_SYMBOL_SETTINGS, "Luminance", "luminace", menu_page_data, false, 0); - use_sub_back_handler(luminace); - - add_entry_to_menu_page(menu_page_data, "Loading mirror ...", mirror, reload_switch_value); - add_entry_to_menu_page(menu_page_data, "Loading flip ...", flip, reload_switch_value); - add_entry_to_menu_page(menu_page_data, "Loading contrast ...", contrast, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading hue ...", hue, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading saturation ...", saturation, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading luminace ...", luminace, reload_slider_value); - - lv_group_set_default(default_group); -} - -void create_air_camera_recording_menu(lv_obj_t * parent) { - menu_page_data_t *menu_page_data = make_camera_page_data(parent); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "Recording", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - rec_enable = create_switch(cont, LV_SYMBOL_SETTINGS, "Enabled", "rec_enable", menu_page_data, false); - use_sub_back_handler(rec_enable); - rec_split = create_slider(cont, LV_SYMBOL_SETTINGS, "Split", "rec_split", menu_page_data, false, 0); - use_sub_back_handler(rec_split); - rec_maxusage = create_slider(cont, LV_SYMBOL_SETTINGS, "Maxusage", "rec_maxusage", menu_page_data, false, 0); - use_sub_back_handler(rec_maxusage); - - add_entry_to_menu_page(menu_page_data, "Loading rec_enable ...", rec_enable, reload_switch_value); - add_entry_to_menu_page(menu_page_data, "Loading rec_split ...", rec_split, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading rec_maxusage ...", rec_maxusage, reload_slider_value); - - lv_group_set_default(default_group); -} - -void create_air_camera_isp_menu(lv_obj_t * parent) { - menu_page_data_t *menu_page_data = make_camera_page_data(parent); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "ISP", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - exposure = create_slider(cont, LV_SYMBOL_SETTINGS, "Exposure", "exposure", menu_page_data, false, 0); - use_sub_back_handler(exposure); - antiflicker = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Antiflicker", "", "antiflicker", menu_page_data, false); - use_sub_back_handler(antiflicker); - sensor_file = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Sensor File", "", "sensor_file", menu_page_data, false); - use_sub_back_handler(sensor_file); - - add_entry_to_menu_page(menu_page_data, "Loading exposure ...", exposure, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading antiflicker ...", antiflicker, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading sensor_file ...", sensor_file, reload_dropdown_value); - - lv_group_set_default(default_group); -} - -void create_air_camera_fpv_menu(lv_obj_t * parent) { - menu_page_data_t *menu_page_data = make_camera_page_data(parent); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "FPV", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - fpv_enable = create_switch(cont, LV_SYMBOL_SETTINGS, "Enabled", "fpv_enable", menu_page_data, false); - use_sub_back_handler(fpv_enable); - noiselevel = create_slider(cont, LV_SYMBOL_SETTINGS, "Noiselevel", "noiselevel", menu_page_data, false, 0); - use_sub_back_handler(noiselevel); - - add_entry_to_menu_page(menu_page_data, "Loading fpv_enable ...", fpv_enable, reload_switch_value); - add_entry_to_menu_page(menu_page_data, "Loading noiselevel ...", noiselevel, reload_slider_value); - - lv_group_set_default(default_group); -} - -void create_air_camera_menu(lv_obj_t * parent, - lv_obj_t * video_page, - lv_obj_t * image_page, - lv_obj_t * recording_page, - lv_obj_t * isp_page, - lv_obj_t * fpv_page) { - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "camera"); - menu_page_data->page_load_callback = NULL; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - - lv_obj_t * section; - - create_text(parent, NULL, "Camera", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - lv_obj_t * video_item = create_text(section, LV_SYMBOL_VIDEO, "Video", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, video_item); - lv_menu_set_load_page_event(menu, video_item, video_page); - lv_obj_add_event_cb(video_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * image_item = create_text(section, LV_SYMBOL_IMAGE, "Image", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, image_item); - lv_menu_set_load_page_event(menu, image_item, image_page); - lv_obj_add_event_cb(image_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * recording_item = create_text(section, LV_SYMBOL_VIDEO, "Recording", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, recording_item); - lv_menu_set_load_page_event(menu, recording_item, recording_page); - lv_obj_add_event_cb(recording_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * isp_item = create_text(section, LV_SYMBOL_EYE_OPEN, "ISP", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, isp_item); - lv_menu_set_load_page_event(menu, isp_item, isp_page); - lv_obj_add_event_cb(isp_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * fpv_item = create_text(section, LV_SYMBOL_GPS, "FPV", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, fpv_item); - lv_menu_set_load_page_event(menu, fpv_item, fpv_page); - lv_obj_add_event_cb(fpv_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/air_camera.h b/src/gsmenu/air_camera.h deleted file mode 100644 index e0cac06b..00000000 --- a/src/gsmenu/air_camera.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -void create_air_camera_video_menu(lv_obj_t * parent); -void create_air_camera_image_menu(lv_obj_t * parent); -void create_air_camera_recording_menu(lv_obj_t * parent); -void create_air_camera_isp_menu(lv_obj_t * parent); -void create_air_camera_fpv_menu(lv_obj_t * parent); -void create_air_camera_menu(lv_obj_t * parent, - lv_obj_t * video_page, - lv_obj_t * image_page, - lv_obj_t * recording_page, - lv_obj_t * isp_page, - lv_obj_t * fpv_page); diff --git a/src/gsmenu/air_telemetry.c b/src/gsmenu/air_telemetry.c deleted file mode 100644 index d4946ef9..00000000 --- a/src/gsmenu/air_telemetry.c +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include -#include -#include "executor.h" -#include "styles.h" - -#include "lvgl/lvgl.h" -#include "helper.h" - -extern lv_group_t * default_group; - -#define ENTRIES 4 -lv_obj_t * serial; -lv_obj_t * router; -lv_obj_t * osd_fps; -lv_obj_t * air_gs_rendering; - -lv_obj_t * air_telemetry_msposd_text; -lv_obj_t * air_telemetry_msposd_section; - -void create_air_telemetry_menu(lv_obj_t * parent) { - - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t) + sizeof(PageEntry) * ENTRIES); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "telemetry"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - serial = create_dropdown(cont,LV_SYMBOL_SETTINGS, "Serial Port", "","serial",menu_page_data,false); - router = create_dropdown(cont,LV_SYMBOL_SETTINGS,"Router","","router",menu_page_data,false); - - air_telemetry_msposd_text = create_text(parent, NULL, "MSPOSD Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - air_telemetry_msposd_section = lv_menu_section_create(parent); - lv_obj_add_style(air_telemetry_msposd_section, &style_openipc_section, 0); - cont = lv_menu_cont_create(air_telemetry_msposd_section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - osd_fps = create_slider(cont,LV_SYMBOL_SETTINGS,"OSD FPS","osd_fps",menu_page_data,false,0); - air_gs_rendering = create_switch(cont,LV_SYMBOL_SETTINGS,"GS Rendering","gs_rendering", menu_page_data,false); - - add_entry_to_menu_page(menu_page_data,"Loading serial ...", serial, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading router ...", router, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading osd_fps ...", osd_fps, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading air_gs_rendering ...", air_gs_rendering, reload_switch_value); - - lv_group_set_default(default_group); -} \ No newline at end of file diff --git a/src/gsmenu/air_telemetry.h b/src/gsmenu/air_telemetry.h deleted file mode 100644 index bfa34761..00000000 --- a/src/gsmenu/air_telemetry.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_air_telemetry_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/air_txprofiles.c b/src/gsmenu/air_txprofiles.c index a8eaf8ba..076d3dbd 100644 --- a/src/gsmenu/air_txprofiles.c +++ b/src/gsmenu/air_txprofiles.c @@ -3,7 +3,6 @@ #include "lvgl/lvgl.h" #include "styles.h" -#include "ui.h" #include "helper.h" #include "../input.h" #include "executor.h" @@ -31,6 +30,7 @@ extern lv_indev_t * indev_drv; extern lv_group_t * default_group; extern lv_obj_t * pp_menu_screen; lv_group_t * tx_profile_group; +lv_group_t * txprofiles_return_group; /* set by the menu before launching */ extern lv_obj_t * sub_air_alink_page; lv_obj_t * input_box; @@ -370,8 +370,7 @@ static void txprofiles_back_button_callback(lv_event_t * e) { if(code == LV_EVENT_CLICKED) { lv_screen_load(pp_menu_screen); - menu_page_data_t * menu_page_data = (menu_page_data_t *) lv_obj_get_user_data(sub_air_alink_page); - lv_indev_set_group(indev_drv,menu_page_data->indev_group); + lv_indev_set_group(indev_drv, txprofiles_return_group); control_mode = GSMENU_CONTROL_MODE_NAV; } } @@ -382,6 +381,8 @@ static void table_focus_callback(lv_event_t * e) { static void txprofiles_save_callback() { lv_screen_load(pp_menu_screen); + lv_indev_set_group(indev_drv, txprofiles_return_group); + control_mode = GSMENU_CONTROL_MODE_NAV; } // Function to save table data back to file @@ -569,6 +570,9 @@ void create_table(lv_obj_t * parent) { lv_obj_add_style(table, &style_openipc_dark_background, LV_PART_MAIN); lv_obj_add_style(table, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); lv_obj_set_style_pad_all(table,2,LV_PART_ITEMS); + /* Local style (highest precedence) guarantees readable light cell text on + * the dark table background, incl. the "..." menu indicator in column 0. */ + lv_obj_set_style_text_color(table, lv_color_hex(0xf2f4f8), LV_PART_ITEMS | LV_STATE_DEFAULT); // Add event handler lv_obj_add_event_cb(table, table_event_cb, LV_EVENT_ALL, NULL); @@ -605,7 +609,6 @@ void create_table(lv_obj_t * parent) { lv_obj_set_style_outline_width(kb,0,LV_PART_MAIN | LV_STATE_FOCUS_KEY); lv_obj_add_style(kb, &style_openipc, LV_PART_ITEMS| LV_STATE_FOCUS_KEY); lv_obj_add_style(kb, &style_openipc_dark_background, LV_PART_ITEMS| LV_STATE_DEFAULT); - lv_obj_add_style(kb, &style_openipc_textcolor, LV_PART_ITEMS| LV_STATE_FOCUS_KEY); lv_obj_add_style(kb, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_event_cb(kb, kb_event_cb, LV_EVENT_ALL,kb); diff --git a/src/gsmenu/air_wfbng.c b/src/gsmenu/air_wfbng.c deleted file mode 100644 index c7567f73..00000000 --- a/src/gsmenu/air_wfbng.c +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include -#include - -#include "lvgl/lvgl.h" -#include "helper.h" -#include "air_wfbng.h" -#include "executor.h" -#include "styles.h" - -extern lv_group_t * default_group; - -lv_obj_t * driver_txpower_override; -lv_obj_t * air_channel; -lv_obj_t * air_bandwidth; -lv_obj_t * mcs_index; -lv_obj_t * stbc; -lv_obj_t * ldpc; -lv_obj_t * fec_k; -lv_obj_t * fec_n; -lv_obj_t * mlink; -lv_obj_t * air_adaptivelink; - -void create_air_wfbng_menu(lv_obj_t * parent) { - - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "air"); - strcpy(menu_page_data->page, "wfbng"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - lv_obj_t * cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - - driver_txpower_override = create_dropdown(cont,LV_SYMBOL_SETTINGS,"Power", "","power",menu_page_data,false); - air_channel = create_dropdown(cont,LV_SYMBOL_SETTINGS,"Frequency", "","air_channel",menu_page_data,false); - air_bandwidth = create_dropdown(cont,LV_SYMBOL_SETTINGS, "bandwidth", "","width",menu_page_data,false); - mcs_index = create_slider(cont,LV_SYMBOL_SETTINGS, "MCS Index","mcs_index",menu_page_data,false,0); - stbc = create_switch(cont,LV_SYMBOL_SETTINGS,"STBC","stbc", menu_page_data,false); - ldpc = create_switch(cont,LV_SYMBOL_SETTINGS,"LDPC","ldpc", menu_page_data,false); - fec_k = create_slider(cont,LV_SYMBOL_SETTINGS, "FEC_K","fec_k",menu_page_data,false,0); - fec_n = create_slider(cont,LV_SYMBOL_SETTINGS, "FEC_N","fec_n",menu_page_data,false,0); - mlink = create_dropdown(cont,LV_SYMBOL_SETTINGS,"MLink", "","mlink",menu_page_data,false); - - create_text(parent, NULL, "Adaptive Link", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - air_adaptivelink = create_switch(cont,LV_SYMBOL_SETTINGS,"Enabled","adaptivelink", menu_page_data,false); - - add_entry_to_menu_page(menu_page_data,"Loading driver_txpower_override ...", driver_txpower_override, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading air_channel ...", air_channel, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading air_bandwidth ...", air_bandwidth, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading mcs_index ...", mcs_index, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading stbc ...", stbc, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading ldpc ...", ldpc, reload_switch_value); - add_entry_to_menu_page(menu_page_data,"Loading fec_k ...", fec_k, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading fec_n ...", fec_n, reload_slider_value); - add_entry_to_menu_page(menu_page_data,"Loading mlink ...", mlink, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data,"Loading air_adaptivelink ...", air_adaptivelink, reload_switch_value); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/air_wfbng.h b/src/gsmenu/air_wfbng.h deleted file mode 100644 index fe405d0e..00000000 --- a/src/gsmenu/air_wfbng.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -typedef struct { - int channel; - const char *frequency; -} FrequencyEntry; - -char *get_frequencies_string(FrequencyEntry *entries, int size); - -void create_air_wfbng_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/colmenu.c b/src/gsmenu/colmenu.c new file mode 100644 index 00000000..86c7ec74 --- /dev/null +++ b/src/gsmenu/colmenu.c @@ -0,0 +1,1170 @@ +#include "colmenu.h" +#include "column_stack.h" +#include "styles.h" /* dark theme styles for the keyboard */ +#include "../input.h" /* control_mode for text entry */ +#include "../menu.h" /* menu_is_recording() */ +#include +#include +#include +#include +#include +#include +#include + +extern gsmenu_control_mode_t control_mode; +extern lv_indev_t * indev_drv; + +static colstack_t * g_cs; + +/* True while the command-error dialog is up, so async teardowns (e.g. the text + * overlay close) don't steal the keypad back from its Close button. */ +static bool g_err_active; + +/* First genuine `gsmenu.sh get` failure (non-zero exit that is NOT the "Unknown" + * = unimplemented sentinel), captured for a deferred error dialog. Gets run on the + * page-load worker thread, so the failure is recorded here and shown later on the + * main thread (page_load_poll, or an async flush for a main-thread get). */ +static char g_gerr_cmd[256]; +static char g_gerr_out[512]; +static char g_gerr_err[512]; +static int g_gerr_rc; +static volatile bool g_gerr_pending; +static volatile bool g_prefetching; /* true while the load worker is fetching */ +static char * read_all(const char * path); +static void flush_get_error(void * unused); + +static void report_get_error(const char * cmd, int rc, const char * out, const char * err) +{ + if(__atomic_load_n(&g_gerr_pending, __ATOMIC_ACQUIRE)) return; /* keep the first */ + snprintf(g_gerr_cmd, sizeof(g_gerr_cmd), "%s", cmd ? cmd : ""); + snprintf(g_gerr_out, sizeof(g_gerr_out), "%s", out ? out : ""); + snprintf(g_gerr_err, sizeof(g_gerr_err), "%s", err ? err : ""); + g_gerr_rc = rc; + __atomic_store_n(&g_gerr_pending, true, __ATOMIC_RELEASE); + /* Worker-thread gets are flushed by page_load_poll; a main-thread get defers + * its own flush (lv_async_call is only safe to call from the LVGL thread). */ + if(!g_prefetching) lv_async_call(flush_get_error, NULL); +} + +/* True from when a sub-page enter is requested until its rows are built. The slow + * gsmenu.sh reads happen on a worker thread meanwhile (see the page loader below), + * so stack-mutating callbacks (drone detection) must defer their work. */ +static volatile bool g_loading; +static volatile bool g_pending_close; /* drone lost mid-load: close after build */ +static void close_drone_subtree(void); + +/* Values pre-fetched by the loader worker, consulted by colmenu_get during the + * (main-thread) build so no gsmenu.sh reads happen while creating widgets. */ +typedef struct { char * param; char * value; char * opts; } prefetch_entry_t; +typedef struct { prefetch_entry_t * items; int count; } prefetch_cache_t; +static prefetch_cache_t * g_prefetch; + +struct colmenu_emit { colstack_t * cs; lv_obj_t * body; }; + +/* ── gsmenu.sh backend ─────────────────────────────────────────────────────── */ + +static void strip_nl(char * s) { size_t n = s ? strlen(s) : 0; while(n && (s[n-1]=='\n'||s[n-1]=='\r')) s[--n]='\0'; } + +/* Read `gsmenu.sh get `; returns malloc'd current value + * (NULL if the script doesn't implement it), and *opts = malloc'd option/range + * blob after the 0x1e separator. */ +char * colmenu_get(const char * type, const char * page, const char * param, char ** opts) +{ + if(opts) *opts = NULL; + if(!type || !param) return NULL; + + /* During a page build a loader worker has already fetched the slow reads; serve + * them from the cache so we never shell out from the main (render) thread — + * for static value items and for the gets a dynamic build() makes (the WiFi + * scan). The worker itself runs with g_prefetch == NULL, so it fetches for real. */ + if(g_prefetch) { + for(int i = 0; i < g_prefetch->count; i++) { + if(g_prefetch->items[i].param && strcmp(g_prefetch->items[i].param, param) == 0) { + if(opts) *opts = g_prefetch->items[i].opts ? strdup(g_prefetch->items[i].opts) : NULL; + return g_prefetch->items[i].value ? strdup(g_prefetch->items[i].value) : NULL; + } + } + } + + /* rec_enabled is runtime state (are we recording RIGHT NOW), not config. + * Read it directly from the app instead of the shell script. Return the + * switch's 0/1 convention (the builder does atoi(v)) — NOT "on"/"off", + * which atoi()s to 0 and would leave the switch stuck off while recording. */ + if(strcmp(param, "rec_enabled") == 0) { + return strdup(menu_is_recording() ? "1" : "0"); + } + + char errf[] = "/tmp/gsmenu_gerr_XXXXXX"; + int efd = mkstemp(errf); + if(efd >= 0) close(efd); + char cmd[300]; + printf("Running command: gsmenu.sh get %s %s %s\n", type, page, param); + fflush(stdout); + snprintf(cmd, sizeof(cmd), "gsmenu.sh get %s %s %s 2>%s", type, page, param, errf); + FILE * fp = popen(cmd, "r"); + if(!fp) { unlink(errf); return NULL; } + char buf[1024]; + size_t n = fread(buf, 1, sizeof(buf) - 1, fp); + buf[n] = '\0'; + int status = pclose(fp); + int rc = (status == -1) ? -1 : WEXITSTATUS(status); + bool unknown = (strncmp(buf, "Unknown", 7) == 0); + /* Non-zero exit that isn't the "Unknown"/unimplemented sentinel is a real + * failure — surface its command, stdout and stderr. */ + if(rc != 0 && !unknown) { + char * eout = read_all(errf); + char disp[300]; + snprintf(disp, sizeof(disp), "gsmenu.sh get %s %s %s", type, page, param); + report_get_error(disp, rc, buf, eout); + free(eout); + } + unlink(errf); + if(rc != 0 || n == 0) return NULL; /* Unknown / error / empty → no value */ + char * rs = strchr(buf, '\x1e'); + char * val; + if(rs) { *rs = '\0'; if(opts) { *opts = strdup(rs + 1); strip_nl(*opts); } val = strdup(buf); } + else { val = strdup(buf); } + strip_nl(val); + return val; +} + +/* ── command execution + error reporting ───────────────────────────────────── */ + +static char * read_all(const char * path) +{ + FILE * f = fopen(path, "r"); + if(!f) return NULL; + char * buf = malloc(4096); + size_t n = fread(buf, 1, 4095, f); + buf[n] = '\0'; + fclose(f); + strip_nl(buf); + return buf; +} + +typedef struct { lv_obj_t * backdrop; lv_group_t * grp; lv_group_t * restore; } err_dlg_t; + +/* Deferred teardown — the close arrives from the button's own event, so deleting + * it inline would be a use-after-free. */ +static void err_close_async(void * p) +{ + err_dlg_t * d = p; + lv_indev_set_group(indev_drv, d->restore); + lv_obj_delete(d->backdrop); + lv_group_delete(d->grp); + free(d); +} + +static void err_close_cb(lv_event_t * e) +{ + /* Activate on the ENTER key press, not CLICKED (which fires on release) — the + * release of the ENTER that triggered the failing action would else close it. */ + if(lv_event_get_code(e) == LV_EVENT_KEY && lv_event_get_key(e) != LV_KEY_ENTER) return; + if(!g_err_active) return; /* already closing */ + g_err_active = false; + lv_async_call(err_close_async, lv_event_get_user_data(e)); +} + +/* Modal error dialog showing a failed command's exit code, stdout and stderr. */ +static void show_cmd_error(const char * cmd, int rc, const char * out, const char * err) +{ + err_dlg_t * d = calloc(1, sizeof(*d)); + d->restore = colstack_cur_group(g_cs); + + lv_obj_t * bd = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(bd); + lv_obj_set_size(bd, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(bd, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(bd, LV_OPA_50, 0); + lv_obj_clear_flag(bd, LV_OBJ_FLAG_SCROLLABLE); + d->backdrop = bd; + + lv_obj_t * panel = lv_obj_create(bd); + lv_obj_add_style(panel, &style_openipc_lightdark_background, LV_PART_MAIN); + lv_obj_set_width(panel, 1000); + lv_obj_set_height(panel, LV_SIZE_CONTENT); + lv_obj_set_style_max_height(panel, LV_PCT(80), 0); + lv_obj_set_style_pad_all(panel, 20, 0); + lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER); + lv_obj_center(panel); + + lv_obj_t * title = lv_label_create(panel); + lv_label_set_text(title, "Command failed"); + lv_obj_set_style_text_color(title, lv_color_hex(0xff6b6b), 0); + lv_obj_set_style_pad_bottom(title, 12, 0); + + lv_obj_t * body = lv_label_create(panel); + lv_label_set_long_mode(body, LV_LABEL_LONG_WRAP); + lv_obj_set_width(body, LV_PCT(100)); + lv_obj_set_style_text_color(body, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_pad_bottom(body, 16, 0); + char buf[4096]; + snprintf(buf, sizeof(buf), + "$ %s\nexit status: %d\n\nstdout:\n%s\n\nstderr:\n%s", + cmd, rc, (out && *out) ? out : "(none)", (err && *err) ? err : "(none)"); + lv_label_set_text(body, buf); + + lv_obj_t * btn = lv_button_create(panel); + lv_obj_add_style(btn, &style_openipc, LV_PART_MAIN); + lv_obj_add_style(btn, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_t * bl = lv_label_create(btn); + lv_label_set_text(bl, "Close"); + + d->grp = lv_group_create(); + lv_group_add_obj(d->grp, btn); + lv_indev_set_group(indev_drv, d->grp); + lv_group_focus_obj(btn); + lv_obj_add_event_cb(btn, err_close_cb, LV_EVENT_KEY, d); + g_err_active = true; +} + +/* Async command runner: the (possibly slow) command runs on a worker thread while + * a spinner blocks the menu, matching the old menu's write behaviour. On finish a + * failure shows the error dialog; `done` (optional) runs the caller's follow-up. */ +typedef struct { + char * cmd; + char outf[32]; + char errf[32]; + lv_obj_t * spinner; + lv_group_t * loader_group; + lv_timer_t * poll; + void (*done)(void * ctx, int rc); + void * done_ctx; + volatile int rc; + volatile bool finished; +} exec_ctx_t; + +static void * exec_thread(void * arg) +{ + exec_ctx_t * x = arg; + char full[700]; + snprintf(full, sizeof(full), "%s >%s 2>%s", x->cmd, x->outf, x->errf); + int status = system(full); + x->rc = (status == -1) ? -1 : WEXITSTATUS(status); + __atomic_store_n(&x->finished, true, __ATOMIC_RELEASE); + return NULL; +} + +static void exec_poll(lv_timer_t * t) +{ + exec_ctx_t * x = lv_timer_get_user_data(t); + if(!__atomic_load_n(&x->finished, __ATOMIC_ACQUIRE)) return; + + lv_obj_delete(x->spinner); + int rc = x->rc; + + /* Hand the keypad back to the column and drop the loader group BEFORE the + * follow-up: a follow-up may open its own dialog that captures the current + * group to return to (e.g. the restart notice), and that must be a real + * group, not our about-to-be-deleted temporary one. */ + lv_indev_set_group(indev_drv, colstack_cur_group(g_cs)); + lv_group_delete(x->loader_group); + + /* caller's follow-up (reload / rebuild / restart notice / close overlay) — + * only runs its effect on success; it may claim the keypad for its own UI. */ + if(x->done) x->done(x->done_ctx, rc); + + if(rc != 0) { + char * out = read_all(x->outf); + char * err = read_all(x->errf); + show_cmd_error(x->cmd, rc, out, err); /* grabs the keypad */ + free(out); + free(err); + } + + unlink(x->outf); + unlink(x->errf); + lv_timer_delete(x->poll); + free(x->cmd); + free(x); +} + +static void colmenu_exec_cb(const char * cmd, void (*done)(void * ctx, int rc), void * ctx) +{ + printf("Running command: %s\n", cmd); + fflush(stdout); + + exec_ctx_t * x = calloc(1, sizeof(*x)); + x->cmd = strdup(cmd); + x->done = done; + x->done_ctx = ctx; + strcpy(x->outf, "/tmp/gsmenu_out_XXXXXX"); + strcpy(x->errf, "/tmp/gsmenu_err_XXXXXX"); + int ofd = mkstemp(x->outf); + int efd = mkstemp(x->errf); + if(ofd >= 0) close(ofd); + if(efd >= 0) close(efd); + + x->spinner = openipc_spinner_create(lv_layer_top()); + + /* freeze the menu on an empty group until the command finishes */ + x->loader_group = lv_group_create(); + lv_indev_set_group(indev_drv, x->loader_group); + + x->poll = lv_timer_create(exec_poll, 20, x); + + pthread_t th; + if(pthread_create(&th, NULL, exec_thread, x) != 0) { + /* thread spawn failed — run it synchronously */ + lv_timer_delete(x->poll); + lv_obj_delete(x->spinner); + lv_indev_set_group(indev_drv, colstack_cur_group(g_cs)); + lv_group_delete(x->loader_group); + char full[700]; + snprintf(full, sizeof(full), "%s >%s 2>%s", x->cmd, x->outf, x->errf); + int status = system(full); + int rc = (status == -1) ? -1 : WEXITSTATUS(status); + if(x->done) x->done(x->done_ctx, rc); + if(rc != 0) { + char * out = read_all(x->outf); char * err = read_all(x->errf); + show_cmd_error(x->cmd, rc, out, err); free(out); free(err); + } + unlink(x->outf); unlink(x->errf); free(x->cmd); free(x); + return; + } + pthread_detach(th); +} + +void colmenu_exec(const char * cmd) { colmenu_exec_cb(cmd, NULL, NULL); } + +/* Show a pending `gsmenu.sh get` failure (recorded by report_get_error). Runs on + * the main thread — from page_load_poll after a load, or via lv_async_call. */ +static void flush_get_error(void * unused) +{ + (void)unused; + if(!__atomic_load_n(&g_gerr_pending, __ATOMIC_ACQUIRE)) return; + __atomic_store_n(&g_gerr_pending, false, __ATOMIC_RELEASE); + if(g_err_active) return; /* another error dialog is already up */ + show_cmd_error(g_gerr_cmd, g_gerr_rc, g_gerr_out, g_gerr_err); +} + +/* ── change binding ────────────────────────────────────────────────────────── */ + +typedef struct { + const char * type; + const char * page; + const char * param; + int display_scale; /* multiply slider display value by this (e.g., 100) */ + void (*on_change)(const char * value); + void (*on_live)(const char * value); +} bind_ctx_t; + +/* on_change runs after the async set completes (only on success). */ +typedef struct { void (*on_change)(const char * value); char * value; } setdone_t; + +static void do_set_done(void * ctx, int rc) +{ + setdone_t * s = ctx; + if(rc == 0 && s->on_change) s->on_change(s->value); + free(s->value); + free(s); +} + +static void do_set(void * ctx, const char * value) +{ + bind_ctx_t * b = ctx; + char cmd[320]; + snprintf(cmd, sizeof(cmd), "gsmenu.sh set %s %s %s \"%s\"", + b->type, b->page, b->param, value ? value : ""); + setdone_t * s = malloc(sizeof(*s)); + s->on_change = b->on_change; + s->value = strdup(value ? value : ""); + colmenu_exec_cb(cmd, do_set_done, s); +} + +static void free_bind_cb(lv_event_t * e) { free(lv_event_get_user_data(e)); } + +/* Slider live-preview: apply the effect (on_live) as the slider moves, without + * writing gsmenu.sh — the persist happens once on commit via do_set. */ +static void do_live(void * ctx, const char * value) +{ + bind_ctx_t * b = ctx; + if(b->on_live) b->on_live(value); +} + +static void bind_row(lv_obj_t * row, const colmenu_page_t * pg, const colmenu_item_t * it) +{ + bind_ctx_t * b = malloc(sizeof(bind_ctx_t)); + b->type = pg->type; b->page = pg->page; b->param = it->param; + b->display_scale = it->display_scale; + b->on_change = it->on_change; b->on_live = it->on_live; + colstack_set_binding(row, do_set, b); + colstack_set_row_param(row, it->param); /* for external reflect (see colmenu_reflect_switch) */ + if(it->on_live) colstack_set_slider_live(row, do_live, b); + lv_obj_add_event_cb(row, free_bind_cb, LV_EVENT_DELETE, b); +} + +static int option_index(const char * options, const char * value) +{ + if(!options || !value) return 0; + char * tmp = strdup(options); char * save = NULL; int i = 0; + for(char * t = strtok_r(tmp, "\n", &save); t; t = strtok_r(NULL, "\n", &save), i++) + if(strcmp(t, value) == 0) { free(tmp); return i; } + free(tmp); + return 0; +} + +/* ── page builder ──────────────────────────────────────────────────────────── */ + +static void build_page(colstack_t * cs, lv_obj_t * body, const colmenu_page_t * pg); + +/* A page is worth a loading spinner if entering it hits the backend: dynamic + * pages (may scan/list) or any item that reads a value via gsmenu.sh. Pages made + * only of labels/submenus/actions build instantly, so skip the spinner+thread. */ +static bool page_needs_load(const colmenu_page_t * pg) +{ + if(pg->build) return true; + for(int i = 0; i < pg->count; i++) { + switch(pg->items[i].kind) { + case COLMENU_VALUE: case COLMENU_SWITCH: case COLMENU_SLIDER: + case COLMENU_DROPDOWN: case COLMENU_TEXT: + return true; + default: + break; + } + } + return false; +} + +static bool item_reads_value(colmenu_kind_t kind) +{ + switch(kind) { + case COLMENU_VALUE: case COLMENU_SWITCH: case COLMENU_SLIDER: + case COLMENU_DROPDOWN: case COLMENU_TEXT: + return true; + default: + return false; + } +} + +typedef struct { + colstack_t * cs; + lv_obj_t * body; + const colmenu_page_t * pg; + lv_obj_t * panel; /* loading overlay (label + progress bar) */ + lv_obj_t * bar; + lv_obj_t * label; + int total; /* number of value items to fetch */ + lv_group_t * loader_group; /* empty group: freezes nav while loading */ + lv_group_t * target_group; /* the new column's group to focus when done */ + lv_timer_t * poll; /* main-thread timer that updates + finishes */ + prefetch_cache_t * cache; /* filled by the worker, read by the build */ + int prog_done; /* items fetched so far (worker → poll) */ + const char * prog_label; /* label of the item being fetched (static str) */ + volatile bool cancel; /* set by the Cancel button (poll → abort) */ + volatile bool done; /* set by the worker when the cache is ready */ +} page_load_t; + +static void free_cache(prefetch_cache_t * c) +{ + if(!c) return; + for(int i = 0; i < c->count; i++) { + free(c->items[i].param); free(c->items[i].value); free(c->items[i].opts); + } + free(c->items); free(c); +} + +/* Worker thread: touches NO LVGL — only runs the (slow) gsmenu.sh reads for the + * page's value items into a cache, reporting progress as it goes, then flags + * itself done. The main-thread poll timer draws the progress bar and, once the + * cache is ready, builds the rows — keeping all UI work on one thread. */ +static void * page_load_thread(void * arg) +{ + page_load_t * pl = arg; + const colmenu_page_t * pg = pl->pg; + + prefetch_cache_t * c = calloc(1, sizeof(*c)); + c->items = calloc(pg->count > 0 ? pg->count : 1, sizeof(prefetch_entry_t)); + g_prefetching = true; /* colmenu_get records get failures instead of showing */ + for(int i = 0; i < pg->count; i++) { + const colmenu_item_t * it = &pg->items[i]; + if(!item_reads_value(it->kind) || !it->param) continue; + if(__atomic_load_n(&pl->cancel, __ATOMIC_ACQUIRE)) break; /* user cancelled */ + /* publish what's loading before the (slow) read so the poll can show it */ + __atomic_store_n(&pl->prog_label, it->label ? it->label : it->param, __ATOMIC_RELEASE); + char * opts = NULL; + char * v = colmenu_get(pg->type, pg->page, it->param, &opts); + c->items[c->count].param = strdup(it->param); + c->items[c->count].value = v; + c->items[c->count].opts = opts; + c->count++; + __atomic_store_n(&pl->prog_done, c->count, __ATOMIC_RELEASE); + } + + g_prefetching = false; + pl->cache = c; + /* Release store pairs with the acquire load in page_load_poll so the cache + * write is guaranteed visible before `done` reads true (matters on ARM). */ + __atomic_store_n(&pl->done, true, __ATOMIC_RELEASE); + return NULL; +} + +/* Runs on the main thread every tick: advances the progress bar, and once the + * worker has finished fetching, builds the page from the cache. */ +static void page_load_poll(lv_timer_t * t) +{ + page_load_t * pl = lv_timer_get_user_data(t); + + /* reflect fetch progress */ + int done_n = __atomic_load_n(&pl->prog_done, __ATOMIC_ACQUIRE); + lv_bar_set_value(pl->bar, done_n, LV_ANIM_OFF); + const char * lab = __atomic_load_n(&pl->prog_label, __ATOMIC_ACQUIRE); + if(lab) { + char buf[128]; + snprintf(buf, sizeof(buf), "Loading %s ...", lab); + lv_label_set_text(pl->label, buf); + } + + if(!__atomic_load_n(&pl->done, __ATOMIC_ACQUIRE)) return; + + lv_obj_delete(pl->panel); + g_loading = false; + + if(__atomic_load_n(&pl->cancel, __ATOMIC_ACQUIRE)) { + /* user cancelled: discard the fetched values and back out of the + * half-built column, returning focus to the row that opened it. */ + free_cache(pl->cache); + colstack_pop(pl->cs); + if(g_pending_close) { g_pending_close = false; close_drone_subtree(); } + } else { + /* Attach the keypad to the column BEFORE building so the first row added + * gets LV_STATE_FOCUS_KEY (the visible focus), matching a normal (sync) + * page open. Building while input is elsewhere leaves it merely FOCUSED. */ + lv_indev_set_group(indev_drv, pl->target_group); + g_prefetch = pl->cache; + build_page(pl->cs, pl->body, pl->pg); /* value items hit the cache */ + g_prefetch = NULL; + free_cache(pl->cache); + + if(g_pending_close) { + /* drone was lost mid-load: close the (now built) sub-page. This pops + * the column and its target_group, and re-homes the focus itself. */ + g_pending_close = false; + close_drone_subtree(); + } else { + /* Focusing the first row while the column was still being populated + * queues a SCROLL_ON_FOCUS animation toward a stale target, which then + * drifts the (now full) column off-screen. Cancel it and pin to top. */ + lv_anim_delete(pl->body, NULL); + lv_obj_scroll_to_y(pl->body, 0, LV_ANIM_OFF); + /* if any value's gsmenu.sh get failed during the load, report it now */ + flush_get_error(NULL); + } + } + lv_group_delete(pl->loader_group); + lv_timer_delete(pl->poll); + free(pl); +} + +/* Cancel button: flag the load; page_load_poll aborts once the worker unwinds. + * Fires on the ENTER key press (not CLICKED, which fires on release) so the very + * release of the ENTER that opened this page can't instantly cancel it. */ +static void loader_cancel_cb(lv_event_t * e) +{ + if(lv_event_get_code(e) == LV_EVENT_KEY && lv_event_get_key(e) != LV_KEY_ENTER) return; + page_load_t * pl = lv_event_get_user_data(e); + __atomic_store_n(&pl->cancel, true, __ATOMIC_RELEASE); + lv_label_set_text(pl->label, "Cancelling ..."); +} + +/* ── Dynamic-page prefetch (spinner) ─────────────────────────────────────────── + * A dynamic page that declares a `prefetch` list (e.g. the WiFi Networks scan) + * gets its slow gsmenu.sh reads warmed on a worker thread behind a spinner, then + * build() runs on the main thread reading those values from the cache. Used for + * both opening the page and re-scanning it (both go through submenu_builder). */ +typedef struct { + colstack_t * cs; + lv_obj_t * body; + const colmenu_page_t * pg; + lv_obj_t * spinner; + lv_group_t * loader_group; /* empty group: freezes nav while loading */ + lv_group_t * target_group; /* column's group to focus once built */ + lv_timer_t * poll; + prefetch_cache_t * cache; /* filled by the worker, read by the build */ + volatile bool done; +} dyn_load_t; + +/* Worker: warm the page's prefetch params into a cache (touches no LVGL). */ +static void * dyn_load_thread(void * arg) +{ + dyn_load_t * dl = arg; + const colmenu_page_t * pg = dl->pg; + int n = 0; + for(const char * const * p = pg->prefetch; *p; p++) n++; + + prefetch_cache_t * c = calloc(1, sizeof(*c)); + c->items = calloc(n > 0 ? n : 1, sizeof(prefetch_entry_t)); + g_prefetching = true; /* colmenu_get records get failures instead of showing */ + for(const char * const * p = pg->prefetch; *p; p++) { + char * opts = NULL; + char * v = colmenu_get(pg->type, pg->page, *p, &opts); + c->items[c->count].param = strdup(*p); + c->items[c->count].value = v; + c->items[c->count].opts = opts; + c->count++; + } + g_prefetching = false; + dl->cache = c; + __atomic_store_n(&dl->done, true, __ATOMIC_RELEASE); + return NULL; +} + +/* Main-thread poll: once the worker is done, build the page from the cache. */ +static void dyn_load_poll(lv_timer_t * t) +{ + dyn_load_t * dl = lv_timer_get_user_data(t); + if(!__atomic_load_n(&dl->done, __ATOMIC_ACQUIRE)) return; + + lv_obj_delete(dl->spinner); + g_loading = false; + + /* Attach the keypad to the column BEFORE building so the first row added gets + * the visible focus (matches page_load_poll). */ + lv_indev_set_group(indev_drv, dl->target_group); + lv_group_delete(dl->loader_group); + + g_prefetch = dl->cache; + build_page(dl->cs, dl->body, dl->pg); /* build()'s colmenu_get hits the cache */ + g_prefetch = NULL; + free_cache(dl->cache); + + /* pin the freshly-built column to the top (see page_load_poll) */ + lv_anim_delete(dl->body, NULL); + lv_obj_scroll_to_y(dl->body, 0, LV_ANIM_OFF); + flush_get_error(NULL); /* report any read that failed during the load */ + + lv_timer_delete(dl->poll); + free(dl); +} + +static void dyn_load_start(colstack_t * cs, lv_obj_t * body, const colmenu_page_t * pg) +{ + dyn_load_t * dl = calloc(1, sizeof(*dl)); + dl->cs = cs; dl->body = body; dl->pg = pg; + dl->target_group = colstack_cur_group(cs); + + dl->spinner = openipc_spinner_create(lv_layer_top()); + + /* freeze the menu on an empty group until the scan finishes */ + dl->loader_group = lv_group_create(); + lv_indev_set_group(indev_drv, dl->loader_group); + + g_loading = true; + dl->poll = lv_timer_create(dyn_load_poll, 20, dl); + + pthread_t th; + if(pthread_create(&th, NULL, dyn_load_thread, dl) != 0) { + /* thread spawn failed — fall back to a synchronous build */ + lv_timer_delete(dl->poll); + lv_obj_delete(dl->spinner); + lv_indev_set_group(indev_drv, dl->target_group); + lv_group_delete(dl->loader_group); + g_loading = false; + build_page(cs, body, pg); + free(dl); + return; + } + pthread_detach(th); +} + +static void submenu_builder(colstack_t * cs, lv_obj_t * body, void * user) +{ + const colmenu_page_t * pg = (const colmenu_page_t *)user; + + /* Dynamic pages build their own rows (interleaving reads + widgets) so they + * can't be prefetched item-by-item; a page that declares a `prefetch` list + * (e.g. the WiFi scan) warms those reads behind a spinner first. Trivial pages + * have nothing slow to fetch. Only static pages with value items get the async + * progress bar. */ + if(pg->build) { + if(pg->prefetch) dyn_load_start(cs, body, pg); + else build_page(cs, body, pg); + return; + } + if(!page_needs_load(pg)) { build_page(cs, body, pg); return; } + + int total = 0; + for(int i = 0; i < pg->count; i++) + if(item_reads_value(pg->items[i].kind) && pg->items[i].param) total++; + if(total == 0) { build_page(cs, body, pg); return; } + + page_load_t * pl = calloc(1, sizeof(*pl)); + pl->cs = cs; pl->body = body; pl->pg = pg; pl->total = total; + pl->target_group = colstack_cur_group(cs); /* the just-pushed column's group */ + + /* loading overlay: a caption + progress bar, centred over everything */ + pl->panel = lv_obj_create(lv_layer_top()); + lv_obj_add_style(pl->panel, &style_openipc_lightdark_background, LV_PART_MAIN); + lv_obj_set_size(pl->panel, 460, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(pl->panel, 24, 0); + lv_obj_set_flex_flow(pl->panel, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(pl->panel, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(pl->panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_center(pl->panel); + + pl->label = lv_label_create(pl->panel); + lv_label_set_text(pl->label, "Loading ..."); + lv_obj_set_style_text_color(pl->label, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_pad_bottom(pl->label, 18, 0); + + pl->bar = lv_bar_create(pl->panel); + lv_obj_set_size(pl->bar, 400, 14); + lv_obj_add_style(pl->bar, &style_openipc_dropdown, LV_PART_MAIN); + lv_obj_add_style(pl->bar, &style_openipc, LV_PART_INDICATOR); + lv_bar_set_range(pl->bar, 0, total); + lv_bar_set_value(pl->bar, 0, LV_ANIM_OFF); + + /* input goes to the loader group holding just the Cancel button */ + pl->loader_group = lv_group_create(); + lv_indev_set_group(indev_drv, pl->loader_group); + + lv_obj_t * cancel = lv_button_create(pl->panel); + lv_obj_add_style(cancel, &style_openipc, LV_PART_MAIN); + lv_obj_add_style(cancel, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_set_style_margin_top(cancel, 18, 0); + lv_obj_t * cl = lv_label_create(cancel); + lv_label_set_text(cl, "Cancel"); + lv_group_add_obj(pl->loader_group, cancel); + lv_group_focus_obj(cancel); + lv_obj_add_event_cb(cancel, loader_cancel_cb, LV_EVENT_KEY, pl); + + g_loading = true; + pl->poll = lv_timer_create(page_load_poll, 20, pl); + + pthread_t th; + if(pthread_create(&th, NULL, page_load_thread, pl) != 0) { + /* thread spawn failed — fall back to a synchronous build */ + lv_timer_delete(pl->poll); + lv_obj_delete(pl->panel); + lv_indev_set_group(indev_drv, pl->target_group); + lv_group_delete(pl->loader_group); + g_loading = false; + build_page(cs, body, pg); + free(pl); + return; + } + pthread_detach(th); +} + +static void static_action_cb(void * ctx) +{ + const colmenu_item_t * it = ctx; + if(it->on_activate) it->on_activate(); +} + +static void add_value_row(lv_obj_t * body, const char * icon, const char * label, const char * value) +{ + lv_obj_t * row = lv_obj_create(body); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + /* a card, like the focusable rows but dimmer, so read-only info is still + * clearly legible against the column instead of faint floating text */ + lv_obj_set_style_bg_color(row, lv_color_hex(0x161b24), 0); + lv_obj_set_style_bg_opa(row, 140, 0); + lv_obj_set_style_radius(row, 6, 0); + lv_obj_set_style_margin_bottom(row, 4, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_hor(row, 12, 0); + lv_obj_set_style_pad_ver(row, 10, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + if(icon) { + lv_obj_t * ic = lv_label_create(row); + lv_label_set_text(ic, icon); + lv_obj_set_style_text_color(ic, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_pad_right(ic, 8, 0); + } + lv_obj_t * l = lv_label_create(row); + lv_label_set_text(l, label); + lv_obj_set_style_text_color(l, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_flex_grow(l, 1); + lv_obj_t * v = lv_label_create(row); + lv_label_set_text(v, value ? value : "-"); + lv_obj_set_style_text_color(v, lv_color_hex(0xc7cfdd), 0); /* brighter value */ +} + +/* ── text entry (floating textarea + on-screen keyboard overlay) ───────────── */ + +typedef struct { + const char * type; + const char * page; + const char * param; + void (*on_change)(const char * value); + lv_obj_t * value_lbl; /* row's value label to refresh after save */ + lv_obj_t * ta; /* live only while the overlay is open */ + lv_obj_t * overlay; /* dim backdrop + panel, floated on the top layer */ + lv_group_t * group; /* the keyboard's own input group */ + lv_group_t * prev_group; /* group to restore on close */ + /* prompt mode (colmenu_prompt): a standalone text entry with a custom OK + * handler instead of the gsmenu.sh set — used e.g. for a wifi password. */ + const char * title; /* overlay caption (else param) */ + void (*on_ok)(const char * text, void * user); /* custom OK; NULL = generic set */ + void * user; + bool owned; /* free this ctx on close */ +} text_ctx_t; + +/* Deferred teardown — ok/cancel arrive from the keyboard's own event, so + * deleting it there would be a use-after-free. */ +static void text_close_cb(void * p) +{ + text_ctx_t * t = p; + /* If a command-error dialog popped from the save, leave the keypad with its + * Close button; err_close_cb restores it to the same page group afterwards. */ + if(!g_err_active) lv_indev_set_group(indev_drv, t->prev_group); + if(t->group) { lv_group_delete(t->group); t->group = NULL; } + if(t->overlay) { lv_obj_delete(t->overlay); t->overlay = NULL; } + control_mode = GSMENU_CONTROL_MODE_NAV; + if(t->owned) free(t); /* standalone prompt ctx (row-owned ctx frees itself) */ +} + +/* After the async set: on success update the row's value + fire on_change; either + * way close the keyboard overlay (text_close_cb defers to the error dialog). */ +static void text_set_done(void * ctx, int rc) +{ + text_ctx_t * t = ctx; + if(rc == 0) { + const char * val = lv_textarea_get_text(t->ta); + if(t->value_lbl) lv_label_set_text(t->value_lbl, val); + if(t->on_change) t->on_change(val); + } + lv_async_call(text_close_cb, t); +} + +static void text_ok_cb(lv_event_t * e) +{ + text_ctx_t * t = lv_event_get_user_data(e); + if(t->on_ok) { /* prompt mode: hand the text to the caller */ + t->on_ok(lv_textarea_get_text(t->ta), t->user); + lv_async_call(text_close_cb, t); + return; + } + char cmd[400]; + snprintf(cmd, sizeof(cmd), "gsmenu.sh set %s %s %s \"%s\"", + t->type, t->page, t->param, lv_textarea_get_text(t->ta)); + colmenu_exec_cb(cmd, text_set_done, t); +} +static void text_cancel_cb(lv_event_t * e) +{ + lv_async_call(text_close_cb, lv_event_get_user_data(e)); +} + +static void open_text(void * ctx) +{ + text_ctx_t * t = ctx; + + /* dim backdrop over the whole menu */ + lv_obj_t * overlay = lv_obj_create(lv_layer_top()); + lv_obj_set_size(overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(overlay, LV_OPA_50, 0); + lv_obj_set_style_border_width(overlay, 0, 0); + lv_obj_set_style_pad_all(overlay, 0, 0); + lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE); + t->overlay = overlay; + + /* centred floating panel: title + textarea + full-size keyboard */ + lv_obj_t * panel = lv_obj_create(overlay); + lv_obj_set_width(panel, 780); + lv_obj_set_height(panel, LV_SIZE_CONTENT); + lv_obj_center(panel); + lv_obj_set_style_bg_color(panel, lv_color_hex(0x0b0e14), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_radius(panel, 10, 0); + lv_obj_set_style_border_color(panel, lv_color_hex(0x4c60d8), 0); + lv_obj_set_style_border_width(panel, 2, 0); + lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t * title = lv_label_create(panel); + lv_label_set_text(title, t->title ? t->title : t->param); + lv_obj_set_style_text_color(title, lv_color_hex(0x8a93a6), 0); + + lv_obj_t * ta = lv_textarea_create(panel); + lv_textarea_set_one_line(ta, true); + lv_obj_set_width(ta, LV_PCT(100)); + lv_obj_add_style(ta, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + /* prompt mode starts empty; a bound field preloads its current value */ + char * cur = t->on_ok ? NULL : colmenu_get(t->type, t->page, t->param, NULL); + lv_textarea_set_text(ta, cur ? cur : ""); + free(cur); + t->ta = ta; + + lv_obj_t * kb = lv_keyboard_create(panel); + lv_obj_set_width(kb, LV_PCT(100)); + lv_obj_set_height(kb, 320); + /* match the dark menu theme instead of the bright default keyboard */ + lv_obj_add_style(kb, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_style(kb, &style_openipc_dark_background, LV_PART_ITEMS | LV_STATE_DEFAULT); + lv_obj_add_style(kb, &style_openipc_dark_background, LV_PART_ITEMS | LV_STATE_CHECKED); /* control keys */ + lv_obj_add_style(kb, &style_openipc, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); + lv_obj_add_style(kb, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_keyboard_set_textarea(kb, ta); + lv_obj_add_event_cb(kb, text_ok_cb, LV_EVENT_READY, t); + lv_obj_add_event_cb(kb, text_cancel_cb, LV_EVENT_CANCEL, t); + + /* own group so the keypad drives the floating keyboard (WASD→arrows, + * ENTER→press key); restore the column's group on close. */ + t->prev_group = colstack_cur_group(g_cs); + t->group = lv_group_create(); + lv_group_add_obj(t->group, kb); + lv_indev_set_group(indev_drv, t->group); + lv_group_focus_obj(kb); + control_mode = GSMENU_CONTROL_MODE_KEYBOARD; +} + +/* Standalone on-screen-keyboard prompt: on OK, `on_ok` is called with the entered + * text (nothing is written to gsmenu.sh). Used e.g. for a wifi network password. */ +void colmenu_prompt(const char * title, void (*on_ok)(const char * text, void * user), void * user) +{ + text_ctx_t * t = calloc(1, sizeof(text_ctx_t)); + t->title = title; + t->on_ok = on_ok; + t->user = user; + t->owned = true; + open_text(t); +} + +static void build_page(colstack_t * cs, lv_obj_t * body, const colmenu_page_t * pg) +{ + if(pg->build) { /* dynamic page: emit rows at runtime */ + colmenu_emit_t e = { cs, body }; + pg->build(&e); + colstack_autofit(cs); + colstack_ensure_escapable(cs); /* empty list (only captions) → keep Back working */ + return; + } + for(int i = 0; i < pg->count; i++) { + const colmenu_item_t * it = &pg->items[i]; + + /* Skip items that don't match the current RX mode. */ + if(it->mode_mask) { + int mode_bit = (RXMODE == APFPV) ? COLMENU_MODE_APFPV : COLMENU_MODE_WFB; + if(!(it->mode_mask & mode_bit)) continue; + } + + switch(it->kind) { + case COLMENU_LABEL: + colstack_add_title(cs, body, it->label); + break; + + case COLMENU_VALUE: { + char * v = colmenu_get(pg->type, pg->page, it->param, NULL); + add_value_row(body, it->icon, it->label, v); + free(v); + break; + } + + case COLMENU_SUBMENU: + colstack_add_submenu(cs, body, it->icon, it->label, submenu_builder, (void *)it->sub); + break; + + case COLMENU_SWITCH: { + char * v = colmenu_get(pg->type, pg->page, it->param, NULL); + lv_obj_t * row = colstack_add_switch(cs, body, it->icon, it->label, v && atoi(v)); + free(v); + bind_row(row, pg, it); + break; + } + case COLMENU_SLIDER: { + char * opts = NULL; + char * v = colmenu_get(pg->type, pg->page, it->param, &opts); + double dv = v ? atof(v) : 0, dmin = 0, dmax = 100; + if(opts) sscanf(opts, "%lf %lf", &dmin, &dmax); + double f = pow(10, it->precision); + lv_obj_t * row = colstack_add_slider(cs, body, it->icon, it->label, + (int32_t)(dmin * f), (int32_t)(dmax * f), + (int32_t)(dv * f), it->precision, it->display_scale); + free(v); free(opts); + bind_row(row, pg, it); + break; + } + case COLMENU_DROPDOWN: { + char * opts = NULL; + char * v = colmenu_get(pg->type, pg->page, it->param, &opts); + int sel = option_index(opts, v); + lv_obj_t * row = colstack_add_dropdown(cs, body, it->icon, it->label, + opts ? opts : "", sel); + free(v); free(opts); + bind_row(row, pg, it); + break; + } + case COLMENU_TEXT: { + char * v = colmenu_get(pg->type, pg->page, it->param, NULL); + lv_obj_t * row = colstack_add_action(cs, body, it->icon, it->label); + lv_obj_t * val = lv_label_create(row); + lv_label_set_text(val, v ? v : ""); + lv_obj_set_style_text_color(val, lv_color_hex(0x4c60d8), 0); + free(v); + text_ctx_t * t = calloc(1, sizeof(text_ctx_t)); + t->type = pg->type; t->page = pg->page; t->param = it->param; + t->on_change = it->on_change; t->value_lbl = val; + colstack_set_action(row, open_text, t); + lv_obj_add_event_cb(row, free_bind_cb, LV_EVENT_DELETE, t); + break; + } + case COLMENU_ACTION: { + lv_obj_t * row = colstack_add_action(cs, body, it->icon, it->label); + colstack_set_action(row, static_action_cb, (void *)it); + break; + } + } + } + colstack_autofit(cs); + colstack_ensure_escapable(cs); /* an all-caption static page mustn't strand Back */ +} + +/* ── public ────────────────────────────────────────────────────────────────── */ + +static const colmenu_page_t * g_root; + +/* Drone (air) submenu rows are "gated" — greyed/disabled until the VTX is + * detected. Tracked so the detection timer can toggle them without a rebuild. */ +#define COLMENU_MAX_GATED 16 +static lv_obj_t * g_gated[COLMENU_MAX_GATED]; +static int g_gated_n; +static bool g_drone_detected; + +static bool row_is_gated(lv_obj_t * r) +{ + for(int i = 0; i < g_gated_n; i++) if(g_gated[i] == r) return true; + return false; +} + +/* Drone lost while inside a drone submenu: close those columns and jump focus to + * the first (non-gated) GS row so the user isn't left on stale/disabled pages. */ +static void close_drone_subtree(void) +{ + if(colstack_depth(g_cs) <= 1) return; + if(!row_is_gated(colstack_column_origin(g_cs, 1))) return; /* in a GS subtree */ + + while(colstack_depth(g_cs) > 1) colstack_pop(g_cs); + + lv_obj_t * body = colstack_root(g_cs); + uint32_t n = lv_obj_get_child_count(body); + for(uint32_t i = 0; i < n; i++) { + lv_obj_t * ch = lv_obj_get_child(body, i); + if(lv_obj_check_type(ch, &lv_label_class)) continue; /* section caption */ + if(!row_is_gated(ch)) { lv_group_focus_obj(ch); break; } + } +} + +void colmenu_set_drone_detected(bool detected) +{ + bool was = g_drone_detected; + g_drone_detected = detected; + for(int i = 0; i < g_gated_n; i++) colstack_set_enabled(g_gated[i], detected); + if(was && !detected) { + /* Defer the column pop if a page is mid-load — the loader would build into + * the very column we'd destroy. page_load_poll runs the close on finish. */ + if(g_loading) g_pending_close = true; + else close_drone_subtree(); + } +} +bool colmenu_drone_detected(void) { return g_drone_detected; } + +void colmenu_init(lv_obj_t * parent) { g_cs = colstack_create(parent); } + +void colmenu_show(const colmenu_page_t * root) +{ + g_root = root; + build_page(g_cs, colstack_root(g_cs), root); +} + +/* Reset to the root column and rebuild it — used when the menu structure itself + * changes (e.g. receiver mode switched). Deferred so it's safe to call from a + * row's event/binding. */ +static void rebuild_async(void * p) +{ + (void)p; + while(colstack_depth(g_cs) > 1) colstack_pop(g_cs); + g_gated_n = 0; /* stale rows about to be deleted */ + lv_obj_clean(colstack_root(g_cs)); + if(g_root) build_page(g_cs, colstack_root(g_cs), g_root); +} +void colmenu_rebuild(void) { lv_async_call(rebuild_async, NULL); } + +/* ── emit API (used by dynamic build() functions) ──────────────────────────── */ + +typedef struct { void (*on)(void *); void * ctx; } emit_action_t; + +static void emit_action_invoke(void * p) +{ + emit_action_t * a = p; + if(a->on) a->on(a->ctx); +} +static void emit_action_delete(lv_event_t * e) +{ + emit_action_t * a = lv_event_get_user_data(e); + free(a->ctx); + free(a); +} + +void colmenu_emit_label(colmenu_emit_t * e, const char * text) +{ + colstack_add_title(e->cs, e->body, text); +} + +void colmenu_emit_value(colmenu_emit_t * e, const char * icon, const char * label, const char * value) +{ + add_value_row(e->body, icon, label, value); +} + +/* bind_ctx for dynamically-emitted rows owns its strings (freed on row delete). */ +static void free_dyn_bind_cb(lv_event_t * e) +{ + bind_ctx_t * b = lv_event_get_user_data(e); + free((void *)b->type); free((void *)b->page); free((void *)b->param); + free(b); +} + +void colmenu_emit_switch(colmenu_emit_t * e, const char * icon, const char * label, + const char * type, const char * page, const char * param, + void (*on_change)(const char * value)) +{ + char * v = colmenu_get(type, page, param, NULL); + lv_obj_t * row = colstack_add_switch(e->cs, e->body, icon, label, v && atoi(v)); + free(v); + bind_ctx_t * b = malloc(sizeof(bind_ctx_t)); + b->type = strdup(type); b->page = strdup(page); b->param = strdup(param); + b->on_change = on_change; b->on_live = NULL; + colstack_set_binding(row, do_set, b); + lv_obj_add_event_cb(row, free_dyn_bind_cb, LV_EVENT_DELETE, b); +} + +void colmenu_emit_submenu(colmenu_emit_t * e, const char * icon, const char * label, + const colmenu_page_t * sub) +{ + colstack_add_submenu(e->cs, e->body, icon, label, submenu_builder, (void *)sub); +} + +void colmenu_emit_submenu_gated(colmenu_emit_t * e, const char * icon, const char * label, + const colmenu_page_t * sub) +{ + lv_obj_t * row = colstack_add_submenu(e->cs, e->body, icon, label, submenu_builder, (void *)sub); + if(g_gated_n < COLMENU_MAX_GATED) g_gated[g_gated_n++] = row; + colstack_set_enabled(row, g_drone_detected); +} + +void colmenu_emit_action(colmenu_emit_t * e, const char * icon, const char * label, + void (*on_activate)(void * ctx), void * ctx) +{ + lv_obj_t * row = colstack_add_action(e->cs, e->body, icon, label); + emit_action_t * a = malloc(sizeof(emit_action_t)); + a->on = on_activate; + a->ctx = ctx; + colstack_set_action(row, emit_action_invoke, a); + lv_obj_add_event_cb(row, emit_action_delete, LV_EVENT_DELETE, a); +} + +/* Deferred so it's safe to call from a row's own event (the rebuild deletes it). */ +static void rescan_async(void * p) { (void)p; colstack_rebuild_current(g_cs); } +void colmenu_rescan(void) { lv_async_call(rescan_async, NULL); } + +void colmenu_reflect_switch(const char * param, bool on) +{ + if(g_cs) colstack_reflect_switch(g_cs, param, on); +} + +lv_group_t * colmenu_root_group(void) { return colstack_root_group(g_cs); } +void colmenu_set_root_back(void (*cb)(void * ctx), void * ctx) { colstack_set_root_back(g_cs, cb, ctx); } diff --git a/src/gsmenu/colmenu.h b/src/gsmenu/colmenu.h new file mode 100644 index 00000000..1dabd7c4 --- /dev/null +++ b/src/gsmenu/colmenu.h @@ -0,0 +1,119 @@ +#pragma once +/* + * colmenu — native, descriptor-driven column menu (the target pattern). + * + * A page is DATA: a title, the gsmenu.sh (type,page) namespace, and a table of + * items. One binder reads gsmenu.sh for the current value/options, renders the + * column row, and on change writes gsmenu.sh + calls the item's domain hook. + * + * No lv_menu, no hidden widgets, no child-by-type pokes, no reload threads — + * contrast with the imperative create_gs_system_display_menu() in gs_system.c. + */ +#include "../../lvgl/lvgl.h" + +typedef enum { + COLMENU_LABEL, /* caption / read-only row */ + COLMENU_VALUE, /* read-only "label: " */ + COLMENU_SWITCH, /* bound bool */ + COLMENU_SLIDER, /* bound number (precision decimals) */ + COLMENU_DROPDOWN, /* bound enum → opens a radio column */ + COLMENU_TEXT, /* bound string → opens a textarea+kbd column */ + COLMENU_SUBMENU, /* descends into another page */ + COLMENU_ACTION, /* fires on_activate */ +} colmenu_kind_t; + +struct colmenu_page; + +/* RX mode bitmask for item gating: bit 0=WFB, bit 1=APFPV. 0=show in all modes. */ +typedef enum { + COLMENU_MODE_WFB = (1 << 0), + COLMENU_MODE_APFPV = (1 << 1), + COLMENU_MODE_ALL = (1 << 0) | (1 << 1), +} colmenu_mode_t; + +typedef struct colmenu_item { + colmenu_kind_t kind; + const char * icon; + const char * label; + const char * param; /* gsmenu.sh parameter */ + int precision; /* slider decimals */ + int display_scale; /* SLIDER: multiply displayed value by this (e.g., 100 for pos*100 MB) */ + colmenu_mode_t mode_mask; /* bitmask: which RX modes show this item (0=all, COLMENU_MODE_*) */ + const struct colmenu_page * sub; /* COLMENU_SUBMENU target */ + void (*on_change)(const char * value); /* domain hook after a set (optional)*/ + void (*on_live)(const char * value); /* SLIDER: fired live while dragging */ + void (*on_activate)(void); /* COLMENU_ACTION */ +} colmenu_item_t; + +/* Handle passed to a dynamic page's build() so it can emit rows at runtime. */ +typedef struct colmenu_emit colmenu_emit_t; + +typedef struct colmenu_page { + const char * title; + const char * type; /* gsmenu.sh get/set ... */ + const char * page; + const colmenu_item_t * items; /* static pages: a table */ + int count; + /* Dynamic pages: build the rows at runtime (e.g. from a scan). When set, + * items/count are ignored and build() runs each time the page is opened. */ + void (*build)(colmenu_emit_t * e); + /* Optional (dynamic pages only): NULL-terminated list of gsmenu.sh params to + * warm on a worker thread behind a spinner before build() runs, so a slow read + * (e.g. the WiFi scan) never freezes the UI. build()'s colmenu_get() calls for + * these params are then served from the prefetch cache. */ + const char * const * prefetch; +} colmenu_page_t; + +/* Create the column engine inside `parent`. */ +void colmenu_init(lv_obj_t * parent); +/* Build `root` as the first column. */ +void colmenu_show(const colmenu_page_t * root); + +/* Reset to the root column and rebuild it (e.g. after a mode switch changes the + * page structure). */ +void colmenu_rebuild(void); + +/* Drone (VTX) detection: gates the air pages (greyed when not detected). */ +void colmenu_set_drone_detected(bool detected); +bool colmenu_drone_detected(void); + +/* ── emit API for dynamic build() functions ────────────────────────────────── */ +void colmenu_emit_label (colmenu_emit_t * e, const char * text); +void colmenu_emit_value (colmenu_emit_t * e, const char * icon, const char * label, const char * value); +/* A bound switch in a dynamic page: reads gsmenu.sh get for + * the initial state and writes on/off on toggle (type/page/param are copied). */ +void colmenu_emit_switch (colmenu_emit_t * e, const char * icon, const char * label, + const char * type, const char * page, const char * param, + void (*on_change)(const char * value)); +void colmenu_emit_submenu(colmenu_emit_t * e, const char * icon, const char * label, const colmenu_page_t * sub); +/* Like emit_submenu, but the row is gated on drone (VTX) detection — greyed and + * un-enterable until colmenu_set_drone_detected(true). */ +void colmenu_emit_submenu_gated(colmenu_emit_t * e, const char * icon, const char * label, const colmenu_page_t * sub); +/* ctx is passed to on_activate and free()'d when the row is destroyed. */ +void colmenu_emit_action (colmenu_emit_t * e, const char * icon, const char * label, + void (*on_activate)(void * ctx), void * ctx); + +/* Re-run the current dynamic column's builder (e.g. a wifi re-scan). Deferred. */ +void colmenu_rescan(void); + +/* Reflect an external state change onto a bound switch (found by gsmenu.sh param) + * without firing its on_change — e.g. a hardware button toggling DVR recording + * should move the "Enabled" switch if that page is open. No-op if not shown. */ +void colmenu_reflect_switch(const char * param, bool on); + +/* Open a floating on-screen-keyboard prompt; on OK, `on_ok` gets the typed text + * (nothing is written to gsmenu.sh). For e.g. a wifi password. */ +void colmenu_prompt(const char * title, void (*on_ok)(const char * text, void * user), void * user); + +/* Run a shell command on a worker thread while a spinner blocks the menu (like the + * old menu did for writes). stdout/stderr and the exit code are captured; a + * non-zero exit shows an error dialog with the command output. */ +void colmenu_exec(const char * cmd); + +/* Integration hooks for embedding in the app's menu screen. */ +lv_group_t * colmenu_root_group(void); /* focus on open */ +void colmenu_set_root_back(void (*cb)(void * ctx), void * ctx); /* Back at root */ + +/* Read gsmenu.sh directly (for dynamic builders): malloc'd value or NULL; + * *opts (optional) = malloc'd blob after the 0x1e separator. */ +char * colmenu_get(const char * type, const char * page, const char * param, char ** opts); diff --git a/src/gsmenu/colmenu_pages.c b/src/gsmenu/colmenu_pages.c new file mode 100644 index 00000000..bbd2971f --- /dev/null +++ b/src/gsmenu/colmenu_pages.c @@ -0,0 +1,674 @@ +/* + * colmenu_pages — the full PixelPilot menu tree as data. This replaces the + * lv_menu builders in ui.c / gs_*.c / air_*.c. Every parameter maps to the same + * `gsmenu.sh get/set ` protocol the old menu used. + * + * Parameter pages are plain tables. Actions and WiFi are dynamic (build fns). + * A few device screens (DVR-Player, TX-Profiles) are launched via the app's + * existing screens; see colmenu_pages_open_*. + */ +#include +#include +#include +#include +#include +#include "colmenu.h" +#include "colmenu_pages.h" +#include "helper.h" /* show_restart_notice(), find_first_focusable_obj() */ +#include "../menu.h" /* MenuAction, screens */ +#include "../main.h" /* sig_handler() — graceful shutdown / restart */ +#ifndef USE_SIMULATOR +#include "../drm.h" /* gamma_lut_controller, gamma_lut_enable/disable */ +#endif +#include "gs_scripts.h" /* dynamic /media/dvr/scripts lookup + runner */ + +extern MenuAction airactions[]; extern size_t airactions_count; +extern MenuAction gsactions[]; extern size_t gsactions_count; +extern enum RXMode RXMODE; + +/* Live Colortrans / Video scale apply their effect to the DRM/GL pipeline live, + * as the old gs_system.c widget callbacks did — a gsmenu.sh set alone won't. */ +extern bool enable_live_colortrans; +extern float live_colortrans_gain; +extern float live_colortrans_offset; +#ifndef USE_SIMULATOR +extern gamma_lut_controller lut_ctrl; +void dvr_reenc_notify_colortrans(int enabled); +void drm_set_video_scale(float factor); +/* DVR backend live setters — mirror the old gs_system.c widget callbacks so a + * change takes effect immediately (a gsmenu.sh set alone only applies on restart). */ +void dvr_reenc_set_osd(int enabled); +void dvr_reenc_set_fps(int fps); +void dvr_reenc_set_bitrate(int kbps); +void dvr_reenc_set_codec(int idx); +void dvr_reenc_set_resolution(int idx); +void dvr_set_mode(int mode); +void dvr_set_max_size(int mb); +void dvr_set_raw_fps(int fps); +void dvr_start_all(void); +void dvr_stop_all(void); +#endif + +static void on_live_colortrans(const char * value) +{ + bool on = value && strcmp(value, "on") == 0; + if(on) { +#ifndef USE_SIMULATOR + gamma_lut_enable(&lut_ctrl, live_colortrans_offset, live_colortrans_gain); +#endif + enable_live_colortrans = true; + } else { +#ifndef USE_SIMULATOR + gamma_lut_disable(&lut_ctrl); +#endif + enable_live_colortrans = false; + } + lv_obj_invalidate(lv_screen_active()); +#ifndef USE_SIMULATOR + dvr_reenc_notify_colortrans(on ? 1 : 0); +#endif + printf("Live colortrans %s (offset=%f, gain=%f)\n", + on ? "ENABLED" : "DISABLED", live_colortrans_offset, live_colortrans_gain); + fflush(stdout); +} + +static void on_video_scale(const char * value) +{ + float factor = value ? (float)atof(value) : 1.0f; +#ifndef USE_SIMULATOR + drm_set_video_scale(factor); +#else + printf("drm_set_video_scale(%.2f);\n", factor); + fflush(stdout); +#endif +} + +/* ── GS DVR live hooks ─────────────────────────────────────────────────────── + * These restore the "apply immediately" behaviour the old gs_system.c widget + * callbacks had. Without them a change to a DVR setting only persists to config + * (gsmenu.sh) and takes effect on the next restart. The dropdown value strings + * come straight from gsmenu.sh's option lists. */ +#ifdef USE_SIMULATOR +#define DVR_LIVE(call, fmt, ...) do { printf(fmt "\n", __VA_ARGS__); fflush(stdout); } while(0) +#else +#define DVR_LIVE(call, fmt, ...) do { call; } while(0) +#endif + +static void on_rec_enabled(const char * value) /* start/stop recording now */ +{ + int on = value && strcmp(value, "on") == 0; + if(on) DVR_LIVE(dvr_start_all(), "dvr_start_all()%s", ""); + else DVR_LIVE(dvr_stop_all(), "dvr_stop_all()%s", ""); +} +static void on_dvr_osd(const char * value) /* burn OSD into the re-encode */ +{ + int on = (value && strcmp(value, "on") == 0) ? 1 : 0; + DVR_LIVE(dvr_reenc_set_osd(on), "dvr_reenc_set_osd(%d)", on); +} +static void on_dvr_mode(const char * value) /* raw=0, reencode=1, both=2 */ +{ + int mode = 0; + if(value) { if(!strcmp(value, "reencode")) mode = 1; else if(!strcmp(value, "both")) mode = 2; } + DVR_LIVE(dvr_set_mode(mode), "dvr_set_mode(%d)", mode); +} +static void on_dvr_max_size(const char * value) /* param is in units of 100 MB */ +{ + int mb = (value ? atoi(value) : 0) * 100; + DVR_LIVE(dvr_set_max_size(mb), "dvr_set_max_size(%d)", mb); +} +static void on_rec_fps(const char * value) /* raw recorder framerate */ +{ + int fps = value ? atoi(value) : 0; + if(fps > 0) DVR_LIVE(dvr_set_raw_fps(fps), "dvr_set_raw_fps(%d)", fps); +} +static void on_dvr_reenc_codec(const char * value) /* h264=0, h265=1 */ +{ + int idx = (value && strcmp(value, "h265") == 0) ? 1 : 0; + DVR_LIVE(dvr_reenc_set_codec(idx), "dvr_reenc_set_codec(%d)", idx); +} +static void on_dvr_reenc_resolution(const char * value) /* 720p=0, 1080p=1 */ +{ + int idx = (value && strcmp(value, "1080p") == 0) ? 1 : 0; + DVR_LIVE(dvr_reenc_set_resolution(idx), "dvr_reenc_set_resolution(%d)", idx); +} +static void on_dvr_reenc_fps(const char * value) +{ + int fps = value ? atoi(value) : 0; + if(fps > 0) DVR_LIVE(dvr_reenc_set_fps(fps), "dvr_reenc_set_fps(%d)", fps); +} +static void on_dvr_reenc_bitrate(const char * value) +{ + int kbps = value ? atoi(value) : 0; + if(kbps > 0) DVR_LIVE(dvr_reenc_set_bitrate(kbps), "dvr_reenc_set_bitrate(%d)", kbps); +} + +/* Full-screen editors reached from the menu (built by the app elsewhere). Each + * has its own input group; on exit they restore a group we set before launch. */ +extern lv_indev_t * indev_drv; +extern lv_obj_t * dvr_screen; +extern lv_obj_t * txprofiles_screen; +extern lv_group_t * dvr_group; /* dvr player controls */ +extern lv_group_t * dvr_page_group; /* Stop returns here */ +extern lv_group_t * tx_profile_group; /* txprofiles table */ +extern lv_group_t * txprofiles_return_group; /* txprofiles back/save return */ +extern const char * dvr_template; +void change_playbutton_label(const char * label); +#ifndef USE_SIMULATOR +void switch_pipeline_source(const char * type, const char * path); +#endif + +/* TX-Profiles editor lives in the drone ALink menu. */ +static void open_txprofiles(void) +{ + txprofiles_return_group = lv_indev_get_group(indev_drv); + lv_screen_load(txprofiles_screen); + lv_indev_set_group(indev_drv, tx_profile_group); + lv_obj_t * first = find_first_focusable_obj(txprofiles_screen); + if(first) lv_group_focus_obj(first); + /* swallow the release of the ENTER that opened this, so it doesn't + * immediately click the focused (Back) button */ + lv_indev_wait_release(indev_drv); +} + +static void notify_restart(const char * v) { (void)v; show_restart_notice(); } + +/* Apply the receiver mode: set RXMODE and the env vars the rest of the app reads + * (REMOTE_IP / AIR_FIRMWARE_TYPE). Called both at startup and on a mode change, so + * the env is always in sync with the actual mode (the old gsmenu_toggle_rxmode() + * set these from menu creation onward). */ +static void apply_rx_mode(bool apfpv) +{ + RXMODE = apfpv ? APFPV : WFB; + setenv("REMOTE_IP", apfpv ? "192.168.0.1" : "10.5.0.10", 1); + setenv("AIR_FIRMWARE_TYPE", apfpv ? "apfpv" : "wfb", 1); +} + +/* Receiver mode drives which link pages are shown. Read from the backend, kept + * in the RXMODE global (shared with the rest of the app). */ +static bool mode_is_apfpv(void) +{ + char * v = colmenu_get("gs", "system", "rx_mode", NULL); + bool apfpv = v && strcmp(v, "apfpv") == 0; + free(v); + apply_rx_mode(apfpv); + return apfpv; +} + +/* Switching RX mode: apply the env the rest of the app expects and rebuild the + * menu so the mode-specific pages appear. (Not the old gsmenu_toggle_rxmode(), + * which pokes lv_menu objects that don't exist here.) */ +static void on_rx_mode_change(const char * value) +{ + bool apfpv = value && strcmp(value, "apfpv") == 0; + apply_rx_mode(apfpv); + colmenu_rebuild(); +} + +/* ── Drone / air ───────────────────────────────────────────────────────────── */ + +static const colmenu_item_t air_wfbng_items[] = { + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Power", .param="power" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Frequency", .param="air_channel" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Bandwidth", .param="width" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="MCS Index", .param="mcs_index" }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="STBC", .param="stbc" }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="LDPC", .param="ldpc" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="FEC_K", .param="fec_k" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="FEC_N", .param="fec_n" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="MLink", .param="mlink" }, + { .kind=COLMENU_LABEL, .label="Adaptive Link" }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="Enabled", .param="adaptivelink" }, +}; +static const colmenu_page_t air_wfbng_page = { "WFB-NG", "air", "wfbng", air_wfbng_items, 11 }; + +static const colmenu_item_t air_alink_items[] = { + { .kind=COLMENU_DROPDOWN, .label="Desired power output (0 pit, 4 highest)", .param="power_level_0_to_4" }, + { .kind=COLMENU_SLIDER, .label="GS heartbeat lost fallback (ms)", .param="fallback_ms" }, + { .kind=COLMENU_SLIDER, .label="Keep link low for min (s)", .param="hold_fallback_mode_s" }, + { .kind=COLMENU_SLIDER, .label="Limit time between link changes (ms)", .param="min_between_changes_ms" }, + { .kind=COLMENU_SLIDER, .label="Wait before increasing link speed (s)", .param="hold_modes_down_s" }, + { .kind=COLMENU_SLIDER, .label="Hysteresis (%)", .param="hysteresis_percent" }, + { .kind=COLMENU_SLIDER, .label="Hysteresis down (%)", .param="hysteresis_percent_down" }, + { .kind=COLMENU_SLIDER, .label="exp_smoothing_factor", .precision=1, .param="exp_smoothing_factor" }, + { .kind=COLMENU_SLIDER, .label="exp_smoothing_factor_down", .precision=1, .param="exp_smoothing_factor_down" }, + { .kind=COLMENU_SWITCH, .label="Lost GS packet requests keyframe", .param="allow_request_keyframe" }, + { .kind=COLMENU_SWITCH, .label="tx_dropped requests keyframe", .param="allow_rq_kf_by_tx_d" }, + { .kind=COLMENU_SLIDER, .label="How often to check driver-xtx", .param="check_xtx_period_ms" }, + { .kind=COLMENU_SLIDER, .label="Limit between keyframe requests", .param="request_keyframe_interval_ms" }, + { .kind=COLMENU_SWITCH, .label="Keyframe at every link change", .param="idr_every_change" }, + { .kind=COLMENU_DROPDOWN, .label="OSD Details", .param="osd_level" }, + { .kind=COLMENU_SLIDER, .label="Font Size", .precision=1, .param="multiply_font_size_by" }, + { .kind=COLMENU_ACTION, .icon=LV_SYMBOL_LIST, .label="TX-Profiles", .on_activate=open_txprofiles }, +}; +static const colmenu_page_t air_alink_page = { "ALink", "air", "alink", air_alink_items, 17 }; + +static const colmenu_item_t air_aalink_items[] = { + { .kind=COLMENU_LABEL, .label="WiFi Settings" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Channel", .param="channel" }, + { .kind=COLMENU_LABEL, .label="AALink Settings" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="VTX Power Output", .precision=1, .param="SCALE_TX_POWER" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="Link resilience (dB)", .param="THRESH_SHIFT" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="OSD Size", .precision=1, .param="OSD_SCALE" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="OSD Level", .param="OSD_LEVEL" }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="OSD Signal Bars", .param="SHOW_SIGNAL_BARS" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="Max Throughput (%)", .param="THROUGHPUT_PCT" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="Temp Throttle (°C)", .param="HIGH_TEMP" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="LQ Consideration", .param="MCS_SOURCE" }, +}; +static const colmenu_page_t air_aalink_page = { "AALink", "air", "aalink", air_aalink_items, 11 }; + +/* Camera sub-pages (all type=air page=camera) */ +static const colmenu_item_t cam_video_items[] = { + /* Size and FPS are WFB-specific; APFPV direct connection has fixed camera modes. */ + { .kind=COLMENU_DROPDOWN, .label="Size", .param="size", .mode_mask=COLMENU_MODE_WFB }, + { .kind=COLMENU_DROPDOWN, .label="Video Mode", .param="video_mode", .mode_mask=COLMENU_MODE_APFPV }, + { .kind=COLMENU_DROPDOWN, .label="FPS", .param="fps", .mode_mask=COLMENU_MODE_WFB }, + { .kind=COLMENU_DROPDOWN, .label="Bitrate", .param="bitrate" }, + { .kind=COLMENU_DROPDOWN, .label="Codec", .param="codec" }, + { .kind=COLMENU_SLIDER, .label="Gopsize", .param="gopsize" }, + { .kind=COLMENU_DROPDOWN, .label="RC Mode", .param="rc_mode" }, +}; +static const colmenu_page_t cam_video_page = { "Video", "air", "camera", cam_video_items, 7 }; +static const colmenu_item_t cam_image_items[] = { + { .kind=COLMENU_SWITCH, .label="Mirror", .param="mirror" }, + { .kind=COLMENU_SWITCH, .label="Flip", .param="flip" }, + { .kind=COLMENU_SLIDER, .label="Contrast", .param="contrast" }, + { .kind=COLMENU_SLIDER, .label="Hue", .param="hue" }, + { .kind=COLMENU_SLIDER, .label="Saturation", .param="saturation" }, + { .kind=COLMENU_SLIDER, .label="Luminance", .param="luminace" }, +}; +static const colmenu_page_t cam_image_page = { "Image", "air", "camera", cam_image_items, 6 }; +static const colmenu_item_t cam_rec_items[] = { + { .kind=COLMENU_SWITCH, .label="Enabled", .param="rec_enable" }, + { .kind=COLMENU_SLIDER, .label="Split", .param="rec_split" }, + { .kind=COLMENU_SLIDER, .label="Maxusage", .param="rec_maxusage" }, +}; +static const colmenu_page_t cam_rec_page = { "Recording", "air", "camera", cam_rec_items, 3 }; +static const colmenu_item_t cam_isp_items[] = { + { .kind=COLMENU_SLIDER, .label="Exposure", .param="exposure" }, + { .kind=COLMENU_DROPDOWN, .label="Antiflicker", .param="antiflicker" }, + { .kind=COLMENU_DROPDOWN, .label="Sensor File", .param="sensor_file" }, +}; +static const colmenu_page_t cam_isp_page = { "ISP", "air", "camera", cam_isp_items, 3 }; +static const colmenu_item_t cam_fpv_items[] = { + { .kind=COLMENU_SWITCH, .label="Enabled", .param="fpv_enable" }, + { .kind=COLMENU_SLIDER, .label="Noiselevel", .param="noiselevel" }, +}; +static const colmenu_page_t cam_fpv_page = { "FPV", "air", "camera", cam_fpv_items, 2 }; +static const colmenu_item_t camera_items[] = { + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="Video", .sub=&cam_video_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_IMAGE, .label="Image", .sub=&cam_image_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="Recording", .sub=&cam_rec_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_EYE_OPEN, .label="ISP", .sub=&cam_isp_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_GPS, .label="FPV", .sub=&cam_fpv_page }, +}; +static const colmenu_page_t camera_page = { "Camera", "air", "camera", camera_items, 5 }; + +static const colmenu_item_t air_tel_items[] = { + { .kind=COLMENU_DROPDOWN, .label="Serial Port", .param="serial" }, + /* Router and MSPOSD are WFB-specific; not used in direct APFPV WiFi mode. */ + { .kind=COLMENU_DROPDOWN, .label="Router", .param="router", .mode_mask=COLMENU_MODE_WFB }, + { .kind=COLMENU_LABEL, .label="MSPOSD Settings", .mode_mask=COLMENU_MODE_WFB }, + { .kind=COLMENU_SLIDER, .label="OSD FPS", .param="osd_fps", .mode_mask=COLMENU_MODE_WFB }, + { .kind=COLMENU_SWITCH, .label="GS Rendering", .param="gs_rendering" }, +}; +static const colmenu_page_t air_tel_page = { "Telemetry", "air", "telemetry", air_tel_items, 5 }; + +/* ── Actions (dynamic: Reboot + script-provided actions) ───────────────────── */ + +#ifdef USE_SIMULATOR +/* The simulator links simulator.c instead of main.cpp, so sig_handler() is + * undefined there; provide a local one that just exits with the given code. */ +void sig_handler(int code) { exit(code); } +#endif + +/* Restart / Exit PixelPilot go through the app's own graceful-shutdown path: + * sig_handler() sets return_value = code and trips signal_flag, so main() returns + * that code. The pixelpilot.sh launcher loop restarts on any exit code except + * 2/143/137 — hence exit(1) restarts PixelPilot and exit(2) quits it for good. */ +static void run_restart_pp(void * c) { (void)c; sig_handler(1); } +static void run_exit_pp (void * c) { (void)c; sig_handler(2); } + +static void run_reboot_air(void * c) { (void)c; colmenu_exec("gsmenu.sh button air actions Reboot"); } +static void run_reboot_gs (void * c) { (void)c; colmenu_exec("gsmenu.sh button gs actions Reboot"); } +static void run_action(void * ctx) { colmenu_exec((const char *)ctx); } + +static void build_air_actions(colmenu_emit_t * e) +{ + colmenu_emit_action(e, LV_SYMBOL_POWER, "Reboot", run_reboot_air, NULL); + for(size_t i = 0; i < airactions_count; i++) + colmenu_emit_action(e, LV_SYMBOL_PLAY, airactions[i].label, run_action, strdup(airactions[i].action)); +} +static void run_custom_script(void * ctx) { gs_scripts_run((const char *)ctx); } + +static void build_gs_actions(colmenu_emit_t * e) +{ + colmenu_emit_action(e, LV_SYMBOL_REFRESH, "Restart PixelPilot", run_restart_pp, NULL); + colmenu_emit_action(e, LV_SYMBOL_CLOSE, "Exit PixelPilot", run_exit_pp, NULL); + colmenu_emit_action(e, LV_SYMBOL_POWER, "Reboot", run_reboot_gs, NULL); + for(size_t i = 0; i < gsactions_count; i++) + colmenu_emit_action(e, LV_SYMBOL_PLAY, gsactions[i].label, run_action, strdup(gsactions[i].action)); + + /* dynamic custom scripts scanned from /media/dvr/scripts */ + char * scripts[128]; + int n = gs_scripts_collect(scripts, 128); + for(int i = 0; i < n; i++) /* names are strdup'd; ownership passes to the row ctx */ + colmenu_emit_action(e, LV_SYMBOL_DIRECTORY, scripts[i], run_custom_script, scripts[i]); +} +static const colmenu_page_t air_actions_page = { .title="Actions", .type="air", .page="actions", .build=build_air_actions }; +static const colmenu_page_t gs_actions_page = { .title="Actions", .type="gs", .page="actions", .build=build_gs_actions }; + +/* ── GS ────────────────────────────────────────────────────────────────────── */ + +static const colmenu_item_t gs_wfbng_items[] = { + { .kind=COLMENU_LABEL, .label="WFB-NG" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Channel", .param="gs_channel" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Bandwidth", .param="bandwidth" }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="TXPower (%)", .param="txpower" }, + { .kind=COLMENU_LABEL, .label="Adaptive Link" }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="Enabled", .param="adaptivelink" }, +}; +static const colmenu_page_t gs_wfbng_page = { "WFB-NG", "gs", "wfbng", gs_wfbng_items, 6 }; + +/* System → Receiver / Display / DVR */ +static const colmenu_item_t sys_receiver_items[] = { + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Codec", .param="rx_codec" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="RX Mode", .param="rx_mode", .on_change=on_rx_mode_change }, +}; +static const colmenu_page_t sys_receiver_page = { "Receiver", "gs", "system", sys_receiver_items, 2 }; +static const colmenu_item_t sys_display_items[] = { + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="GS Rendering", .param="gs_rendering" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Connector", .param="connector" }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Resolution", .param="resolution", .on_change=notify_restart }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="Video scale factor", .precision=2, .param="video_scale", .on_change=on_video_scale, .on_live=on_video_scale }, + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="Live Colortrans", .param="gs_live_colortrans", .on_change=on_live_colortrans }, +}; +static const colmenu_page_t sys_display_page = { "Display", "gs", "system", sys_display_items, 5 }; +static const colmenu_item_t sys_dvr_items[] = { + { .kind=COLMENU_SWITCH, .label="Enabled", .param="rec_enabled", .on_change=on_rec_enabled }, + { .kind=COLMENU_DROPDOWN, .label="Mode", .param="dvr_mode", .on_change=on_dvr_mode }, + { .kind=COLMENU_SLIDER, .label="Max file size (MB)", .param="dvr_max_size", .on_change=on_dvr_max_size, .display_scale=100 }, + { .kind=COLMENU_DROPDOWN, .label="Raw FPS", .param="rec_fps", .on_change=on_rec_fps }, + { .kind=COLMENU_DROPDOWN, .label="Codec", .param="dvr_reenc_codec", .on_change=on_dvr_reenc_codec }, + { .kind=COLMENU_DROPDOWN, .label="Resolution", .param="dvr_reenc_resolution", .on_change=on_dvr_reenc_resolution }, + { .kind=COLMENU_DROPDOWN, .label="Re-encode FPS", .param="dvr_reenc_fps", .on_change=on_dvr_reenc_fps }, + { .kind=COLMENU_DROPDOWN, .label="Bitrate (kbps)", .param="dvr_reenc_bitrate", .on_change=on_dvr_reenc_bitrate }, + { .kind=COLMENU_SWITCH, .label="Record OSD in DVR", .param="dvr_osd", .on_change=on_dvr_osd }, +}; +static const colmenu_page_t sys_dvr_page = { "DVR", "gs", "system", sys_dvr_items, 9 }; +static const colmenu_item_t system_items[] = { + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_WIFI, .label="Receiver", .sub=&sys_receiver_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_IMAGE, .label="Display", .sub=&sys_display_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="DVR", .sub=&sys_dvr_page }, +}; +static const colmenu_page_t system_page = { "System", "gs", "system", system_items, 3 }; + +/* WiFi. The WiFi page shows the live connection (get gs wifi ssid) — entering the + * connected network gives Disconnect / Forget. "Networks" lists only AVAILABLE + * networks to join: open ones and those whose password the GS already saved + * connect straight away; other secured ones prompt for a password. There is no + * standalone "set password" — the password is passed at connect time. */ + +static char g_wifi_ssid[128]; /* SSID awaiting a typed password */ +static char g_connected_ssid[128]; /* the network the connected-submenu acts on */ + +static void wifi_connect_with_password(const char * password, void * user) +{ + (void)user; + char cmd[512]; + snprintf(cmd, sizeof(cmd), "gsmenu.sh set gs wifi connect \"%s\" \"%s\"", g_wifi_ssid, password); + colmenu_exec(cmd); +} + +typedef struct { char ssid[96]; bool secured; char saved[128]; } wifi_net_t; +static void connect_network(void * ctx) +{ + wifi_net_t * net = ctx; + if(!net->secured || net->saved[0]) { /* open, or password already known */ + char cmd[512]; + if(net->saved[0]) + snprintf(cmd, sizeof(cmd), "gsmenu.sh set gs wifi connect \"%s\" \"%s\"", net->ssid, net->saved); + else + snprintf(cmd, sizeof(cmd), "gsmenu.sh set gs wifi connect \"%s\"", net->ssid); + colmenu_exec(cmd); + } else { /* secured, unknown → ask */ + snprintf(g_wifi_ssid, sizeof(g_wifi_ssid), "%s", net->ssid); + char title[160]; + snprintf(title, sizeof(title), "Password for %s", net->ssid); + colmenu_prompt(title, wifi_connect_with_password, NULL); + } +} +static void wifi_rescan(void * ctx) { (void)ctx; colmenu_rescan(); } + +/* Saved-networks blob is "SSID:password" per line ('\:' escapes a colon in the + * SSID). Copy the password for `ssid` into `out` (empty if not saved). */ +static void wifi_saved_password(const char * blob, const char * ssid, char * out, size_t outsz) +{ + out[0] = '\0'; + if(!blob || !*blob) return; + char * dup = strdup(blob), * save = NULL; + for(char * line = strtok_r(dup, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { + if(line[0] == '\0') continue; + int len = (int)strlen(line), colon = -1; + for(int j = 0; j < len; j++) + if(line[j] == ':' && (j == 0 || line[j-1] != '\\')) { colon = j; break; } + if(colon < 0) continue; + const char * pwd = line + colon + 1; + line[colon] = '\0'; + char name[96]; int si = 0, di = 0; + while(line[si] && di < (int)sizeof(name) - 1) { + if(line[si] == '\\' && line[si+1] == ':') { name[di++] = ':'; si += 2; } + else name[di++] = line[si++]; + } + name[di] = '\0'; + if(strcmp(name, ssid) == 0) { snprintf(out, outsz, "%s", pwd); break; } + } + free(dup); +} + +/* Scan line format from `gsmenu.sh get gs wifi networks`: SSID:SECURITY:SIGNAL + * (literal ':' in the SSID escaped as '\:'; SECURITY "--" = open, else secured). */ +static void build_networks(colmenu_emit_t * e) +{ + char * saved = colmenu_get("gs", "wifi", "savednetworks", NULL); + char * out = colmenu_get("gs", "wifi", "networks", NULL); + int n = 0; + if(out && *out) { + char * save = NULL; + for(char * line = strtok_r(out, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { + if(line[0] == '\0') continue; + /* split on the last two UNescaped colons: SSID : SECURITY : SIGNAL */ + int len = (int)strlen(line), last = -1, prev = -1; + for(int i = 0; i < len; i++) + if(line[i] == ':' && (i == 0 || line[i-1] != '\\')) { prev = last; last = i; } + if(last < 0 || prev < 0) continue; + const char * security = line + prev + 1; + line[last] = '\0'; + line[prev] = '\0'; + char ssid[96]; int si = 0, di = 0; /* unescape '\:' → ':' */ + while(line[si] && di < (int)sizeof(ssid) - 1) { + if(line[si] == '\\' && line[si+1] == ':') { ssid[di++] = ':'; si += 2; } + else ssid[di++] = line[si++]; + } + ssid[di] = '\0'; + if(ssid[0] == '\0') continue; + + wifi_net_t * net = malloc(sizeof(*net)); + snprintf(net->ssid, sizeof(net->ssid), "%s", ssid); + net->secured = (security[0] != '\0' && strcmp(security, "--") != 0); + wifi_saved_password(saved, ssid, net->saved, sizeof(net->saved)); + + const char * icon = !net->secured ? LV_SYMBOL_WIFI /* open */ + : net->saved[0] ? LV_SYMBOL_OK /* password known */ + : LV_SYMBOL_EYE_CLOSE; /* needs password */ + colmenu_emit_action(e, icon, ssid, connect_network, net); + n++; + } + } + free(out); + free(saved); + if(n == 0) colmenu_emit_label(e, "No networks found"); + /* Always present + focusable — re-scan, and keeps an empty list navigable. */ + colmenu_emit_action(e, LV_SYMBOL_REFRESH, "Rescan", wifi_rescan, NULL); +} +/* `networks` triggers the (slow) iw scan; warm both it and the saved list on a + * worker thread behind a spinner so opening/rescanning doesn't freeze the UI. */ +static const char * const networks_prefetch[] = { "networks", "savednetworks", NULL }; +static const colmenu_page_t networks_page = { .title="Networks", .type="gs", .page="wifi", .build=build_networks, .prefetch=networks_prefetch }; + +/* Connected-network submenu: Disconnect + Forget. (Forget is best-effort — the + * backend may not implement `set gs wifi forget`.) */ +static void wifi_disconnect(void * ctx) { (void)ctx; colmenu_exec("gsmenu.sh set gs wifi disconnect"); } +static void wifi_forget(void * ctx) +{ + (void)ctx; + char cmd[256]; + snprintf(cmd, sizeof(cmd), "gsmenu.sh set gs wifi forget \"%s\"", g_connected_ssid); + colmenu_exec(cmd); +} +static void build_connected(colmenu_emit_t * e) +{ + colmenu_emit_value(e, LV_SYMBOL_WIFI, "Network", g_connected_ssid); + colmenu_emit_action(e, LV_SYMBOL_CLOSE, "Disconnect", wifi_disconnect, NULL); + colmenu_emit_action(e, LV_SYMBOL_TRASH, "Forget", wifi_forget, NULL); +} +static const colmenu_page_t wifi_connected_page = { .title="Connected", .type="gs", .page="wifi", .build=build_connected }; + +/* WiFi main page (dynamic — reflects the live connection state). */ +static void build_wifi(colmenu_emit_t * e) +{ + char * ip = colmenu_get("gs", "wifi", "IP", NULL); + colmenu_emit_value(e, LV_SYMBOL_SETTINGS, "IP", (ip && *ip) ? ip : "-"); + free(ip); + + char * ssid = colmenu_get("gs", "wifi", "ssid", NULL); + if(ssid && *ssid) { /* connected → enter for actions */ + snprintf(g_connected_ssid, sizeof(g_connected_ssid), "%s", ssid); + char lbl[160]; + snprintf(lbl, sizeof(lbl), LV_SYMBOL_OK " %s", ssid); + colmenu_emit_submenu(e, LV_SYMBOL_WIFI, lbl, &wifi_connected_page); + } else { + colmenu_emit_value(e, LV_SYMBOL_WIFI, "Not connected", ""); + } + free(ssid); + + colmenu_emit_submenu(e, LV_SYMBOL_WIFI, "Networks", &networks_page); + colmenu_emit_switch (e, LV_SYMBOL_WIFI, "Hotspot", "gs", "wifi", "hotspot", NULL); +} +static const colmenu_page_t wifi_page = { .title="WiFi", .type="gs", .page="wifi", .build=build_wifi }; + +/* DVR-Player: a recordings browser (like the old create_gs_dvr_menu); selecting + * a file starts playback on dvr_screen, and the player's Stop button returns. */ +static void play_recording(void * ctx) +{ + dvr_page_group = lv_indev_get_group(indev_drv); /* Stop returns to this list */ +#ifndef USE_SIMULATOR + switch_pipeline_source("file", (const char *)ctx); +#endif + lv_screen_load(dvr_screen); + lv_indev_set_group(indev_drv, dvr_group); + lv_indev_wait_release(indev_drv); /* don't let the ENTER release hit a control */ + change_playbutton_label(LV_SYMBOL_PAUSE); +} +static void build_dvr(colmenu_emit_t * e) +{ + char dir[256] = "./"; + if(dvr_template) { + const char * sl = strrchr(dvr_template, '/'); + if(sl) { size_t n = sl - dvr_template + 1; if(n < sizeof(dir)) { memcpy(dir, dvr_template, n); dir[n] = '\0'; } } + } + colmenu_emit_label(e, "Recordings"); + int n = 0; + DIR * d = opendir(dir); + if(d) { + struct dirent * ent; + while((ent = readdir(d))) { + char * ext = strrchr(ent->d_name, '.'); + if(ext && strcmp(ext, ".mp4") == 0) { + char full[512]; + snprintf(full, sizeof(full), "%s%s", dir, ent->d_name); + colmenu_emit_action(e, LV_SYMBOL_VIDEO, ent->d_name, play_recording, strdup(full)); + n++; + } + } + closedir(d); + } + if(n == 0) colmenu_emit_label(e, "No recordings found"); +} +static const colmenu_page_t dvr_page = { .title="DVR-Player", .type="gs", .page="dvr", .build=build_dvr }; + +/* GS APFPV connection page (shown in APFPV mode) */ +static void reset_apfpv_action(void) { colmenu_exec("gsmenu.sh set gs apfpv reset"); } + +/* Per-wlx-adapter enable + connection status, listed dynamically from the live + * interface set (the old apfpv page did this inline). Enable toggles the adapter + * (gsmenu.sh set gs apfpv on/off); status is read-only ("status "). */ +static void build_apfpv_adapters(colmenu_emit_t * e) +{ + FILE * fp = popen("ip -o link show | awk -F': ' '{print $2}' | grep '^wlx' 2>/dev/null", "r"); + int n = 0; + if(fp) { + char line[128]; + while(fgets(line, sizeof(line), fp)) { + line[strcspn(line, " \t\r\n")] = '\0'; /* trim to bare iface name */ + if(line[0] == '\0') continue; + n++; + char lbl[160]; + snprintf(lbl, sizeof(lbl), "Enable %s", line); + colmenu_emit_switch(e, LV_SYMBOL_WIFI, lbl, "gs", "apfpv", line, NULL); + + char sparam[160]; + snprintf(sparam, sizeof(sparam), "status %s", line); + char * st = colmenu_get("gs", "apfpv", sparam, NULL); + colmenu_emit_value(e, LV_SYMBOL_SETTINGS, "Status", st ? st : "-"); + free(st); + } + pclose(fp); + } + if(n == 0) colmenu_emit_label(e, "No wlx adapters found"); +} +static const colmenu_page_t apfpv_adapters_page = { .title="Adapters", .type="gs", .page="apfpv", .build=build_apfpv_adapters }; + +static const colmenu_item_t gs_apfpv_items[] = { + { .kind=COLMENU_TEXT, .icon=LV_SYMBOL_WIFI, .label="SSID", .param="ssid" }, + { .kind=COLMENU_TEXT, .icon=LV_SYMBOL_EYE_CLOSE, .label="Password", .param="password" }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_LIST, .label="Adapters", .sub=&apfpv_adapters_page }, + { .kind=COLMENU_ACTION, .icon=LV_SYMBOL_REFRESH, .label="Reset APFPV", .on_activate=reset_apfpv_action }, +}; +static const colmenu_page_t gs_apfpv_page = { "APFPV", "gs", "apfpv", gs_apfpv_items, 4 }; + +/* ── root (mode-dependent) ──────────────────────────────────────────────────── */ + +static void build_root(colmenu_emit_t * e) +{ + bool apfpv = (RXMODE == APFPV); /* RXMODE is set from the backend at open, + and updated live by on_rx_mode_change */ + + /* Drone (air) pages are gated on VTX detection — greyed until the drone is + * seen (mirrors the old check_connection_timer behaviour). */ + colmenu_emit_label(e, "Drone Settings"); + if(apfpv) { + colmenu_emit_submenu_gated(e, LV_SYMBOL_WIFI, "AALink", &air_aalink_page); + } else { + colmenu_emit_submenu_gated(e, LV_SYMBOL_WIFI, "WFB-NG", &air_wfbng_page); + colmenu_emit_submenu_gated(e, LV_SYMBOL_WIFI, "ALink", &air_alink_page); + } + colmenu_emit_submenu_gated(e, LV_SYMBOL_IMAGE, "Camera", &camera_page); + colmenu_emit_submenu_gated(e, LV_SYMBOL_DOWNLOAD, "Telemetry", &air_tel_page); + colmenu_emit_submenu_gated(e, LV_SYMBOL_PLAY, "Actions", &air_actions_page); + + colmenu_emit_label(e, "GS Settings"); + if(apfpv) colmenu_emit_submenu(e, LV_SYMBOL_WIFI, "APFPV", &gs_apfpv_page); + else colmenu_emit_submenu(e, LV_SYMBOL_WIFI, "WFB-NG", &gs_wfbng_page); + colmenu_emit_submenu(e, LV_SYMBOL_VIDEO, "DVR-Player", &dvr_page); + colmenu_emit_submenu(e, LV_SYMBOL_SETTINGS, "System Settings", &system_page); + colmenu_emit_submenu(e, LV_SYMBOL_WIFI, "WiFi", &wifi_page); + colmenu_emit_submenu(e, LV_SYMBOL_PLAY, "Actions", &gs_actions_page); +} +static const colmenu_page_t root_page = { .title="Menu", .build=build_root }; + +const colmenu_page_t * colmenu_pages_root(void) +{ + mode_is_apfpv(); /* initialise RXMODE from the backend before first build */ + return &root_page; +} diff --git a/src/gsmenu/colmenu_pages.h b/src/gsmenu/colmenu_pages.h new file mode 100644 index 00000000..f7580941 --- /dev/null +++ b/src/gsmenu/colmenu_pages.h @@ -0,0 +1,4 @@ +#pragma once +#include "colmenu.h" +/* The full PixelPilot menu tree as descriptors (replaces the lv_menu builders). */ +const colmenu_page_t * colmenu_pages_root(void); diff --git a/src/gsmenu/column_stack.c b/src/gsmenu/column_stack.c new file mode 100644 index 00000000..cb581f70 --- /dev/null +++ b/src/gsmenu/column_stack.c @@ -0,0 +1,802 @@ +#include "column_stack.h" +#include "styles.h" +#include +#include +#include +#include + +/* Provided by the app (input.cpp / menu.c). The stack retargets this indev's + * group to the right-most column so keys always hit the visible level. */ +extern lv_indev_t * indev_drv; + +/* The project's control mode. We flip it to SLIDER while editing a slider so + * the WASD mapping in input.cpp routes +/- to us; NAV otherwise. */ +#include "../input.h" +extern gsmenu_control_mode_t control_mode; + +#define CS_MAX_COLS 8 +#define CS_COL_WIDTH 360 + +typedef enum { + CS_SUBMENU, + CS_SWITCH, + CS_SLIDER, + CS_DROPDOWN, + CS_RADIO, + CS_ACTION, + CS_TEXT, +} cs_role_t; + +typedef struct { + cs_role_t role; + colstack_t * cs; + const char * param; /* bound gsmenu.sh param (for external reflect) */ + lv_obj_t * label; /* main text label (measured for autofit) */ + /* submenu */ + colstack_builder_t builder; + void * user; + char title[64]; + /* switch */ + lv_obj_t * sw; + /* slider */ + lv_obj_t * slider; + lv_obj_t * slider_val; + int precision; /* slider decimals (value = raw/10^prec) */ + int display_scale; /* multiply displayed value by this (e.g., 100 for pos*100 MB) */ + /* dropdown */ + lv_obj_t * value_lbl; /* the "current value" text on the row */ + char * options; /* '\n' separated, owned */ + int sel; + /* radio */ + lv_obj_t * owner; /* dropdown row this radio option belongs to */ + int index; + /* value binding (optional): fired with the new value string on change */ + void (* on_change)(void * ctx, const char * value); + void * ctx; + /* live slider preview (optional): fired with the value on every adjustment + * while editing, and again with the original value on cancel */ + void (* on_live)(void * ctx, const char * value); + void * live_ctx; + /* action binding (optional): fired on activate */ + void (* on_activate)(void * ctx); + void * act_ctx; +} cs_item_t; + +struct colstack { + lv_obj_t * stack; + struct { + lv_obj_t * col; + lv_obj_t * body; + lv_group_t * group; + lv_obj_t * origin; /* the row that opened this column */ + } cols[CS_MAX_COLS]; + int depth; /* number of live columns; root included */ + lv_obj_t * edit_slider; /* non-NULL while a slider is being edited */ + cs_item_t * edit_item; /* the item owning edit_slider */ + int32_t edit_start; /* slider value on entering edit (for cancel) */ + void (* root_back)(void * ctx); /* back pressed at the root column */ + void * root_back_ctx; +}; + +/* ─────────────────────────────────────────────────────────────────────────── */ + +static void refresh_active_styling(colstack_t * cs) +{ + for(int i = 0; i < cs->depth; i++) { + bool active = (i == cs->depth - 1); + /* constant border WIDTH (only the colour changes) so a column's content + * doesn't shift by a pixel when it goes from active to inactive. */ + lv_obj_set_style_border_width(cs->cols[i].col, 2, 0); + lv_obj_set_style_border_color(cs->cols[i].col, + active ? lv_color_hex(0x4c60d8) : lv_color_hex(0x39424f), 0); + /* de-emphasise columns the user has stepped back from — but only slightly, + * so their text stays readable (LV_OPA_70 washed them out over the video). */ + lv_obj_set_style_opa(cs->cols[i].col, active ? LV_OPA_COVER : LV_OPA_90, 0); + } +} + +static lv_obj_t * make_column(colstack_t * cs, const char * title) +{ + lv_obj_t * col = lv_obj_create(cs->stack); + /* Height fits the content (short pages don't waste the whole screen) but is + * capped at the viewport so long lists still bound their scroll area. */ + lv_obj_set_width(col, CS_COL_WIDTH); + lv_obj_set_height(col, LV_SIZE_CONTENT); + lv_obj_set_style_max_height(col, LV_PCT(100), 0); + lv_obj_set_style_bg_color(col, lv_color_hex(0x0b0e14), 0); + lv_obj_set_style_bg_opa(col, LV_OPA_COVER, 0); + /* light text inherited by all rows — otherwise the default (dark) theme text + * on the dark column reads as if every item is disabled */ + lv_obj_set_style_text_color(col, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_radius(col, 8, 0); + lv_obj_set_style_pad_all(col, 0, 0); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(col, LV_OBJ_FLAG_SCROLL_ELASTIC); + + /* column header */ + lv_obj_t * hdr = lv_label_create(col); + lv_label_set_text(hdr, title ? title : ""); + lv_obj_set_style_text_color(hdr, lv_color_hex(0x8a93a6), 0); + lv_obj_set_style_pad_all(hdr, 10, 0); + lv_obj_set_width(hdr, LV_PCT(100)); + + /* scrollable body that holds the rows. Height fits the rows (so the column + * wraps its content) but is capped in pixels at the viewport minus the + * header, so a long list bounds its own scroll area while the header stays + * put. NOTE: flex_grow does NOT work under a SIZE_CONTENT parent (the child + * collapses), so the body must be SIZE_CONTENT with an explicit max. */ + lv_obj_t * body = lv_obj_create(col); + lv_obj_set_width(body, LV_PCT(100)); + lv_obj_set_height(body, LV_SIZE_CONTENT); + lv_obj_update_layout(cs->stack); + int32_t vh = lv_obj_get_content_height(cs->stack); + if(vh > 100) lv_obj_set_style_max_height(body, vh - 52 /*header+pads*/, 0); + lv_obj_set_style_bg_opa(body, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(body, 0, 0); + lv_obj_set_style_pad_all(body, 6, 0); + lv_obj_set_flex_flow(body, LV_FLEX_FLOW_COLUMN); + + lv_obj_set_user_data(col, body); + return col; +} + +lv_obj_t * colstack_push(colstack_t * cs, const char * title) +{ + if(cs->depth >= CS_MAX_COLS) return NULL; + + lv_obj_t * col = make_column(cs, title); + lv_obj_t * body = lv_obj_get_user_data(col); + lv_group_t * grp = lv_group_create(); + + int i = cs->depth++; + cs->cols[i].col = col; + cs->cols[i].body = body; + cs->cols[i].group = grp; + + lv_indev_set_group(indev_drv, grp); + lv_obj_scroll_to_view(col, LV_ANIM_ON); + refresh_active_styling(cs); + return body; +} + +void colstack_pop(colstack_t * cs) +{ + if(cs->depth <= 1) return; /* never pop the root */ + + int i = --cs->depth; + lv_group_delete(cs->cols[i].group); + lv_obj_delete(cs->cols[i].col); + cs->cols[i].col = NULL; + + lv_indev_set_group(indev_drv, cs->cols[cs->depth - 1].group); + lv_obj_scroll_to_view(cs->cols[cs->depth - 1].col, LV_ANIM_ON); + refresh_active_styling(cs); +} + +int colstack_depth(colstack_t * cs) { return cs->depth; } + +lv_obj_t * colstack_root(colstack_t * cs) { return cs->cols[0].body; } + +lv_group_t * colstack_cur_group(colstack_t * cs) { return cs->cols[cs->depth - 1].group; } + +/* ── key handling ──────────────────────────────────────────────────────────── */ + +static bool is_back_key(uint32_t k) { return k == LV_KEY_LEFT || k == LV_KEY_ESC || k == LV_KEY_HOME; } +static bool is_forward_key(uint32_t k) { return k == LV_KEY_RIGHT || k == LV_KEY_ENTER; } + +static void fire_change(cs_item_t * it, const char * value) +{ + if(it && it->on_change) it->on_change(it->ctx, value); +} + +/* Live slider preview: format the raw value and fire the on_live callback (for + * effects that should track the slider as it moves, e.g. DRM video scaling). */ +static void fire_slider_live(cs_item_t * it, int32_t raw) +{ + if(!it || !it->on_live) return; + char buf[32]; + snprintf(buf, sizeof(buf), "%.*f", it->precision, raw / pow(10, it->precision)); + it->on_live(it->live_ctx, buf); +} + +/* Announce slider edit mode on the slider itself: the bar grows taller and gains + * a bright white border + knob, clearly stronger than the row's focus outline + * (an outline gets clipped by the row, so we use size + border instead). */ +static void slider_edit_style(lv_obj_t * s, bool on) +{ + lv_obj_set_height(s, on ? 20 : 10); + lv_obj_set_style_border_width(s, on ? 3 : 0, LV_PART_MAIN); + lv_obj_set_style_border_color(s, lv_color_hex(0xffffff), LV_PART_MAIN); + lv_obj_set_style_border_opa(s, on ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_MAIN); + lv_obj_set_style_bg_color(s, on ? lv_color_hex(0xffffff) : lv_color_hex(0x4c60d8), LV_PART_KNOB); +} + +static void leave_slider_edit(colstack_t * cs) +{ + if(cs->edit_slider) slider_edit_style(cs->edit_slider, false); + cs->edit_slider = NULL; + cs->edit_item = NULL; + control_mode = GSMENU_CONTROL_MODE_NAV; +} + +static void commit_slider_edit(colstack_t * cs) +{ + if(cs->edit_item && cs->edit_slider) { + double v = lv_slider_get_value(cs->edit_slider) / pow(10, cs->edit_item->precision); + char buf[32]; + snprintf(buf, sizeof(buf), "%.*f", cs->edit_item->precision, v); + fire_change(cs->edit_item, buf); + } + leave_slider_edit(cs); +} + +/* cancel: restore the value we started with, without firing the binding */ +static void cancel_slider_edit(colstack_t * cs) +{ + if(cs->edit_slider) { + lv_slider_set_value(cs->edit_slider, cs->edit_start, LV_ANIM_OFF); + if(cs->edit_item && cs->edit_item->slider_val) { + double f = pow(10, cs->edit_item->precision); + double displayed = (cs->edit_start / f) * (cs->edit_item->display_scale ? cs->edit_item->display_scale : 1); + lv_label_set_text_fmt(cs->edit_item->slider_val, "%.*f", + cs->edit_item->precision, displayed); + } + fire_slider_live(cs->edit_item, cs->edit_start); /* revert live preview */ + } + leave_slider_edit(cs); +} + +/* deferred radio apply — deleting the focused row inside its own key event is a + * use-after-free, so pop + fire the binding on the next LVGL cycle. */ +typedef struct { colstack_t * cs; cs_item_t * owner; int index; char text[64]; } cs_radio_apply_t; + +static void cs_radio_apply_cb(void * p) +{ + cs_radio_apply_t * a = p; + if(a->owner && a->owner->value_lbl) { + lv_label_set_text(a->owner->value_lbl, a->text); + a->owner->sel = a->index; + } + colstack_pop(a->cs); + colstack_autofit(a->cs); /* re-fit the column now the value changed length */ + fire_change(a->owner, a->text); + free(a); +} + +static void item_key_cb(lv_event_t * e) +{ + cs_item_t * it = lv_event_get_user_data(e); + colstack_t * cs = it->cs; + uint32_t k = lv_event_get_key(e); + + /* Editing a slider: RIGHT/LEFT adjust, ENTER commits, Back (ESC) reverts. */ + if(cs->edit_slider) { + if(k == LV_KEY_RIGHT || k == LV_KEY_LEFT) { + int32_t v = lv_slider_get_value(cs->edit_slider); + int32_t min = lv_slider_get_min_value(cs->edit_slider); + int32_t max = lv_slider_get_max_value(cs->edit_slider); + int32_t step = (max - min) / 20; if(step < 1) step = 1; + v += (k == LV_KEY_RIGHT) ? step : -step; + if(v < min) v = min; if(v > max) v = max; + lv_slider_set_value(cs->edit_slider, v, LV_ANIM_OFF); + if(it->slider_val) { + double f = pow(10, it->precision); + double displayed = (v / f) * (it->display_scale ? it->display_scale : 1); + lv_label_set_text_fmt(it->slider_val, "%.*f", it->precision, displayed); + } + fire_slider_live(it, v); /* live-preview the effect as it moves */ + } + else if(k == LV_KEY_ENTER) { + commit_slider_edit(cs); + } + else { /* ESC / HOME → cancel, revert to original */ + cancel_slider_edit(cs); + } + return; + } + + /* disabled rows (e.g. drone pages when the VTX isn't detected) can't be + * entered/activated; Back still works to leave. */ + if(is_forward_key(k) && lv_obj_has_state(lv_event_get_target(e), LV_STATE_DISABLED)) + return; + + if(is_forward_key(k)) { + switch(it->role) { + case CS_SUBMENU: { + lv_obj_t * body = colstack_push(cs, it->title); + if(body) cs->cols[cs->depth - 1].origin = lv_event_get_target(e); + if(body && it->builder) it->builder(cs, body, it->user); + break; + } + case CS_SWITCH: { + bool on = !lv_obj_has_state(it->sw, LV_STATE_CHECKED); + if(on) lv_obj_add_state(it->sw, LV_STATE_CHECKED); + else lv_obj_remove_state(it->sw, LV_STATE_CHECKED); + /* gsmenu.sh set expects on/off for switches (get returns 0/1). */ + fire_change(it, on ? "on" : "off"); + break; + } + case CS_SLIDER: { + cs->edit_slider = it->slider; + cs->edit_item = it; + cs->edit_start = lv_slider_get_value(it->slider); + slider_edit_style(it->slider, true); + control_mode = GSMENU_CONTROL_MODE_SLIDER; + break; + } + case CS_DROPDOWN: { + /* open the options as a radio list in a new column */ + lv_obj_t * body = colstack_push(cs, it->title); + if(!body) break; + char * opts = strdup(it->options ? it->options : ""); + int idx = 0; + char * save = NULL; + lv_obj_t * sel_row = NULL; + for(char * tok = strtok_r(opts, "\n", &save); tok; tok = strtok_r(NULL, "\n", &save)) { + lv_obj_t * r = colstack_add_action(cs, body, NULL, tok); + cs_item_t * ri = lv_obj_get_user_data(r); + ri->role = CS_RADIO; + ri->index = idx; + /* radio bullet on the current value */ + lv_obj_t * bullet = lv_label_create(r); + lv_label_set_text(bullet, idx == it->sel ? LV_SYMBOL_OK : ""); + lv_obj_set_style_text_color(bullet, lv_color_hex(0x4c60d8), 0); + /* remember which dropdown this belongs to */ + ri->owner = lv_event_get_target(e); + if(idx == it->sel) sel_row = r; + idx++; + } + free(opts); + colstack_autofit(cs); + if(sel_row) { + lv_group_focus_obj(sel_row); /* open on current value */ + /* centre it so options above and below are visible (clamped + * to the ends when near the top/bottom) */ + lv_obj_update_layout(body); + lv_area_t rc, bc; + lv_obj_get_coords(sel_row, &rc); + lv_obj_get_coords(body, &bc); + int32_t diff = (rc.y1 + rc.y2) / 2 - (bc.y1 + bc.y2) / 2; + lv_obj_scroll_to_y(body, lv_obj_get_scroll_y(body) + diff, LV_ANIM_OFF); + } + break; + } + case CS_RADIO: { + cs_item_t * owner = it->owner ? lv_obj_get_user_data(it->owner) : NULL; + lv_obj_t * txt = lv_obj_get_child(lv_event_get_target(e), 0); + cs_radio_apply_t * a = calloc(1, sizeof(cs_radio_apply_t)); + a->cs = cs; a->owner = owner; a->index = it->index; + if(txt) strncpy(a->text, lv_label_get_text(txt), sizeof(a->text) - 1); + lv_async_call(cs_radio_apply_cb, a); /* deferred: safe delete + bind */ + break; + } + case CS_ACTION: + /* Fire on the KEY press, NOT via LV_EVENT_CLICKED: the keypad + * sends CLICKED on ENTER *release*, which after a column push + * would land on the child column's freshly focused row. */ + if(it->on_activate) it->on_activate(it->act_ctx); + break; + } + return; + } + + if(is_back_key(k)) { + if(cs->depth <= 1) { if(cs->root_back) cs->root_back(cs->root_back_ctx); } + else colstack_pop(cs); + } +} + +static void item_delete_cb(lv_event_t * e) +{ + cs_item_t * it = lv_event_get_user_data(e); + if(it) { free(it->options); free(it); } +} + +/* ── row construction ──────────────────────────────────────────────────────── */ + +static lv_obj_t * base_row(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, cs_item_t ** out_it) +{ + lv_obj_t * row = lv_obj_create(col); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_color(row, lv_color_hex(0x161b24), 0); + lv_obj_set_style_bg_opa(row, 200, 0); + lv_obj_set_style_radius(row, 6, 0); + lv_obj_set_style_pad_hor(row, 12, 0); + lv_obj_set_style_pad_ver(row, 10, 0); + lv_obj_set_style_margin_bottom(row, 4, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_add_style(row, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_add_style(row, &style_openipc_disabled, LV_PART_MAIN | LV_STATE_DISABLED); + lv_obj_add_flag(row, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + if(icon) { + lv_obj_t * ic = lv_label_create(row); + lv_label_set_text(ic, icon); + lv_obj_set_style_text_color(ic, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_pad_right(ic, 8, 0); + } + + lv_obj_t * lbl = lv_label_create(row); + lv_label_set_text(lbl, text ? text : ""); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); /* wrap only if it must */ + lv_obj_set_style_text_color(lbl, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_flex_grow(lbl, 1); + + cs_item_t * it = calloc(1, sizeof(cs_item_t)); + it->cs = cs; + it->label = lbl; + if(text) { strncpy(it->title, text, sizeof(it->title) - 1); } + lv_obj_set_user_data(row, it); + + /* Keypad-focusable, but NOT clickable: we drive everything from LV_EVENT_KEY. + * Leaving rows clickable makes LVGL fire a native CLICKED on ENTER, which — + * after a forward key pushes a new column — lands on the freshly focused row + * of that column (spurious activation). */ + lv_group_t * grp = cs->cols[cs->depth - 1].group; + lv_group_add_obj(grp, row); + lv_obj_add_event_cb(row, item_key_cb, LV_EVENT_KEY, it); + lv_obj_add_event_cb(row, item_delete_cb, LV_EVENT_DELETE, it); + + if(out_it) *out_it = it; + return row; +} + +lv_obj_t * colstack_add_title(colstack_t * cs, lv_obj_t * col, const char * text) +{ + (void)cs; + lv_obj_t * lbl = lv_label_create(col); + lv_label_set_text(lbl, text); + lv_obj_set_style_text_color(lbl, lv_color_hex(0x8a93a6), 0); + lv_obj_set_style_pad_top(lbl, 6, 0); + lv_obj_set_style_pad_bottom(lbl, 2, 0); + lv_obj_set_style_pad_left(lbl, 4, 0); + return lbl; +} + +lv_obj_t * colstack_add_submenu(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, colstack_builder_t builder, void * user) +{ + cs_item_t * it; + lv_obj_t * row = base_row(cs, col, icon, text, &it); + it->role = CS_SUBMENU; + it->builder = builder; + it->user = user; + lv_obj_t * chev = lv_label_create(row); + lv_label_set_text(chev, LV_SYMBOL_RIGHT); + lv_obj_set_style_text_color(chev, lv_color_hex(0x8a93a6), 0); + return row; +} + +lv_obj_t * colstack_add_switch(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, bool on) +{ + cs_item_t * it; + lv_obj_t * row = base_row(cs, col, icon, text, &it); + it->role = CS_SWITCH; + lv_obj_t * sw = lv_switch_create(row); + /* lv_switch auto-adds itself to the default group (group_def=TRUE); the first + * one then shows a stray focus outline. The row is what's navigable, not the + * switch, so take it out of the group. */ + lv_group_remove_obj(sw); + lv_obj_add_style(sw, &style_openipc, LV_PART_INDICATOR | LV_STATE_CHECKED); + /* consistent "cornery" look in both states (not just when checked) */ + lv_obj_set_style_radius(sw, 6, LV_PART_MAIN); + lv_obj_set_style_radius(sw, 6, LV_PART_INDICATOR); + lv_obj_set_style_radius(sw, 6, LV_PART_KNOB); + lv_obj_remove_flag(sw, LV_OBJ_FLAG_CLICKABLE); + if(on) lv_obj_add_state(sw, LV_STATE_CHECKED); + it->sw = sw; + return row; +} + +lv_obj_t * colstack_add_slider(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, int32_t min, int32_t max, int32_t val, + int precision, int display_scale) +{ + cs_item_t * it; + lv_obj_t * row = base_row(cs, col, icon, text, &it); + it->role = CS_SLIDER; + it->precision = precision; + it->display_scale = display_scale; + + lv_obj_t * val_lbl = lv_label_create(row); + double f = pow(10, precision); + double displayed = (val / f) * (display_scale ? display_scale : 1); + lv_label_set_text_fmt(val_lbl, "%.*f", precision, displayed); + lv_obj_set_style_text_color(val_lbl, lv_color_hex(0xf2f4f8), 0); + lv_obj_set_style_pad_right(val_lbl, 8, 0); + + lv_obj_t * slider = lv_slider_create(row); + lv_obj_set_width(slider, 120); + lv_obj_set_height(slider, 10); /* baseline; grows in edit mode */ + lv_slider_set_range(slider, min, max); + lv_slider_set_value(slider, val, LV_ANIM_OFF); + lv_obj_add_style(slider, &style_openipc, LV_PART_INDICATOR); + lv_obj_add_style(slider, &style_openipc, LV_PART_KNOB); + /* lv_slider also auto-joins the default group (group_def=TRUE); the row is + * what's navigable, so keep the slider out of the group (avoids a stray + * focus outline on the first slider of a page). */ + lv_group_remove_obj(slider); + lv_obj_remove_flag(slider, LV_OBJ_FLAG_CLICKABLE); + it->slider = slider; + it->slider_val = val_lbl; + return row; +} + +lv_obj_t * colstack_add_dropdown(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, const char * options, int sel) +{ + cs_item_t * it; + lv_obj_t * row = base_row(cs, col, icon, text, &it); + it->role = CS_DROPDOWN; + it->options = strdup(options ? options : ""); + it->sel = sel; + + /* current value shown on the right, then a chevron (opens a column). Cap its + * width so a very long value wraps within the cap instead of squeezing the + * label to nothing. */ + lv_obj_t * val = lv_label_create(row); + lv_obj_set_style_text_color(val, lv_color_hex(0x4c60d8), 0); + lv_obj_set_style_pad_right(val, 8, 0); + lv_label_set_long_mode(val, LV_LABEL_LONG_WRAP); + lv_obj_set_style_max_width(val, 380, 0); + /* pick the sel-th option for the initial text */ + { + char * tmp = strdup(options ? options : ""); + char * save = NULL; int i = 0; const char * chosen = ""; + for(char * t = strtok_r(tmp, "\n", &save); t; t = strtok_r(NULL, "\n", &save), i++) + if(i == sel) { chosen = t; break; } + lv_label_set_text(val, chosen); + /* chosen points into tmp; copy before free */ + char buf[64]; strncpy(buf, lv_label_get_text(val), sizeof(buf) - 1); buf[sizeof(buf)-1] = 0; + lv_label_set_text(val, buf); + free(tmp); + } + it->value_lbl = val; + + lv_obj_t * chev = lv_label_create(row); + lv_label_set_text(chev, LV_SYMBOL_RIGHT); + lv_obj_set_style_text_color(chev, lv_color_hex(0x8a93a6), 0); + return row; +} + +lv_obj_t * colstack_add_action(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text) +{ + cs_item_t * it; + lv_obj_t * row = base_row(cs, col, icon, text, &it); + it->role = CS_ACTION; + return row; +} + +void colstack_set_binding(lv_obj_t * row, void (*on_change)(void * ctx, const char * value), + void * ctx) +{ + cs_item_t * it = lv_obj_get_user_data(row); + if(it) { it->on_change = on_change; it->ctx = ctx; } +} + +void colstack_set_row_param(lv_obj_t * row, const char * param) +{ + cs_item_t * it = lv_obj_get_user_data(row); + if(it) it->param = param; +} + +void colstack_reflect_switch(colstack_t * cs, const char * param, bool on) +{ + if(!param) return; + /* search every visible column so it still works if the user has stepped into + * a child column (e.g. an open dropdown) of the page holding the switch. */ + for(int c = 0; c < cs->depth; c++) { + lv_obj_t * body = cs->cols[c].body; + uint32_t n = lv_obj_get_child_count(body); + for(uint32_t i = 0; i < n; i++) { + lv_obj_t * row = lv_obj_get_child(body, i); + cs_item_t * it = lv_obj_get_user_data(row); + if(!it || it->role != CS_SWITCH || !it->sw || !it->param) continue; + if(strcmp(it->param, param) != 0) continue; + if(on) lv_obj_add_state(it->sw, LV_STATE_CHECKED); + else lv_obj_remove_state(it->sw, LV_STATE_CHECKED); + return; + } + } +} + +void colstack_set_slider_live(lv_obj_t * row, void (*on_live)(void * ctx, const char * value), + void * ctx) +{ + cs_item_t * it = lv_obj_get_user_data(row); + if(it) { it->on_live = on_live; it->live_ctx = ctx; } +} + +void colstack_set_action(lv_obj_t * row, void (*on_activate)(void * ctx), void * ctx) +{ + cs_item_t * it = lv_obj_get_user_data(row); + if(it) { it->on_activate = on_activate; it->act_ctx = ctx; } +} + +void colstack_set_root_back(colstack_t * cs, void (*cb)(void * ctx), void * ctx) +{ + cs->root_back = cb; + cs->root_back_ctx = ctx; +} + +lv_group_t * colstack_root_group(colstack_t * cs) { return cs->cols[0].group; } + +lv_obj_t * colstack_column_origin(colstack_t * cs, int idx) +{ + if(idx < 0 || idx >= cs->depth) return NULL; + return cs->cols[idx].origin; +} + +/* Re-run the builder of the top (non-root) column into a freshly-cleared body — + * used to refresh a dynamic list in place (e.g. a wifi re-scan). */ +void colstack_rebuild_current(colstack_t * cs) +{ + if(cs->depth <= 1) return; + int i = cs->depth - 1; + lv_obj_t * origin = cs->cols[i].origin; + cs_item_t * it = origin ? lv_obj_get_user_data(origin) : NULL; + if(!it || !it->builder) return; + lv_obj_clean(cs->cols[i].body); + it->builder(cs, cs->cols[i].body, it->user); +} + +void colstack_set_enabled(lv_obj_t * row, bool enabled) +{ + if(enabled) lv_obj_remove_state(row, LV_STATE_DISABLED); /* for the key handler */ + else lv_obj_add_state(row, LV_STATE_DISABLED); + /* dim the whole row (icon + label + chevron) — the label's own text colour + * would otherwise override an inherited disabled style. */ + lv_obj_set_style_opa(row, enabled ? LV_OPA_COVER : LV_OPA_40, 0); +} + +void colstack_ensure_escapable(colstack_t * cs) +{ + if(cs->depth <= 1) return; /* root is never a dead end */ + int top = cs->depth - 1; + if(lv_group_get_obj_count(cs->cols[top].group) > 0) return; /* already navigable */ + + /* An inert, zero-footprint focusable row: the group auto-focuses it (it is the + * only member), so the Back key lands on item_key_cb and pops the column. The + * page's own "none found" caption stays the visible content; this is invisible. + * NOTE: it must NOT be hidden or disabled — focus_next_core skips both. */ + lv_obj_t * row = lv_obj_create(cs->cols[top].body); + lv_obj_remove_style_all(row); + lv_obj_set_size(row, 0, 0); + + cs_item_t * it = calloc(1, sizeof(cs_item_t)); + it->cs = cs; + it->role = CS_ACTION; /* forward keys inert; Back pops */ + lv_obj_set_user_data(row, it); + + lv_group_add_obj(cs->cols[top].group, row); + lv_obj_add_event_cb(row, item_key_cb, LV_EVENT_KEY, it); + lv_obj_add_event_cb(row, item_delete_cb, LV_EVENT_DELETE, it); +} + +/* Size the current (top) column to fit its widest row, clamped to [MIN,MAX]. + * Labels wrap only when they'd exceed MAX — so short pages stay compact and + * long labels (e.g. verbose slider descriptions) get a wider column instead of + * being squeezed. */ +#define CS_COL_MIN 320 +#define CS_COL_MAX 720 + +static int32_t label_text_w(lv_obj_t * label) +{ + if(!label) return 0; + const lv_font_t * font = lv_obj_get_style_text_font(label, LV_PART_MAIN); + lv_point_t sz; + lv_text_get_size(&sz, lv_label_get_text(label), font, + lv_obj_get_style_text_letter_space(label, LV_PART_MAIN), 0, + LV_COORD_MAX, LV_TEXT_FLAG_NONE); + return sz.x; +} + +static int32_t role_ctrl_w(const cs_item_t * it) +{ + switch(it->role) { + case CS_SLIDER: return label_text_w(it->slider_val) + 120 /*bar*/ + 60; + case CS_DROPDOWN: { /* value (capped) + chevron + gaps */ + int32_t vw = label_text_w(it->value_lbl); + if(vw > 380) vw = 380; + return vw + 48; + } + case CS_SWITCH: return 70; + case CS_SUBMENU: return 34; /* chevron */ + default: return 20; + } +} + +void colstack_autofit(colstack_t * cs) +{ + int top = cs->depth - 1; + lv_obj_t * body = cs->cols[top].body; + int32_t need = CS_COL_MIN; + + uint32_t n = lv_obj_get_child_count(body); + for(uint32_t i = 0; i < n; i++) { + lv_obj_t * ch = lv_obj_get_child(body, i); + int32_t w; + /* +SLACK so the label always has a few px to spare and never wraps a + * single trailing character. */ + const int32_t SLACK = 32; + if(lv_obj_check_type(ch, &lv_label_class)) { /* a caption/title row */ + w = label_text_w(ch) + 24 + SLACK; + } else { + cs_item_t * it = lv_obj_get_user_data(ch); + if(!it) { + /* not a colstack row (e.g. an emit_value info card): sum its + * label children so a long value/SSID still widens the column. */ + int32_t sum = 0; + uint32_t m = lv_obj_get_child_count(ch); + for(uint32_t j = 0; j < m; j++) { + lv_obj_t * gc = lv_obj_get_child(ch, j); + if(lv_obj_check_type(gc, &lv_label_class)) sum += label_text_w(gc) + 8; + } + if(!sum) continue; + w = sum + 24 /*pad*/ + SLACK; + } else { + /* ACTION rows (including TEXT items) may have a value label child. + * For TEXT items, this holds the current value (e.g., SSID or password). + * If present, sum the value width instead of guessing. */ + int32_t ctrl = role_ctrl_w(it); + if(it->role == CS_ACTION) { + int32_t sum = 0; + uint32_t m = lv_obj_get_child_count(ch); + for(uint32_t j = 0; j < m; j++) { + lv_obj_t * gc = lv_obj_get_child(ch, j); + if(lv_obj_check_type(gc, &lv_label_class)) sum += label_text_w(gc) + 8; + } + if(sum > 0) ctrl = sum; + } + w = label_text_w(it->label) + ctrl + 34 /*icon*/ + 24 /*pad*/ + SLACK; + } + } + if(w > need) need = w; + } + if(need > CS_COL_MAX) need = CS_COL_MAX; + lv_obj_set_width(cs->cols[top].col, need); + /* Force the flex/label layout to settle at the new width now — otherwise a + * LONG_WRAP label that wrapped at the old width keeps its stale line break + * until some later event (e.g. moving a slider) re-flows it. */ + lv_obj_update_layout(cs->cols[top].col); +} + +/* ── lifecycle ─────────────────────────────────────────────────────────────── */ + +colstack_t * colstack_create(lv_obj_t * parent) +{ + colstack_t * cs = calloc(1, sizeof(colstack_t)); + + lv_obj_t * stack = lv_obj_create(parent); + lv_obj_set_size(stack, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_opa(stack, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(stack, 0, 0); + lv_obj_set_style_pad_all(stack, 8, 0); + lv_obj_set_style_pad_column(stack, 8, 0); + lv_obj_set_flex_flow(stack, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(stack, LV_DIR_HOR); + /* keep the root at the left; scroll_to_view brings deeper columns into view + * once the cascade grows wider than the viewport. */ + lv_obj_set_scroll_snap_x(stack, LV_SCROLL_SNAP_NONE); + lv_obj_set_flex_align(stack, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + cs->stack = stack; + + /* root column (always present) */ + lv_obj_t * col = make_column(cs, "Menu"); + lv_obj_t * body = lv_obj_get_user_data(col); + lv_group_t * grp = lv_group_create(); + cs->cols[0].col = col; + cs->cols[0].body = body; + cs->cols[0].group = grp; + cs->depth = 1; + + lv_indev_set_group(indev_drv, grp); + refresh_active_styling(cs); + return cs; +} diff --git a/src/gsmenu/column_stack.h b/src/gsmenu/column_stack.h new file mode 100644 index 00000000..37db0048 --- /dev/null +++ b/src/gsmenu/column_stack.h @@ -0,0 +1,128 @@ +#pragma once +/* + * column_stack — a cascading "Miller column" navigation container for LVGL. + * + * Instead of lv_menu's single-visible-page model (sidebar + one swapped main + * page), the column stack keeps every level visible side by side: + * + * [ root column | page A | sub-page B | dropdown list C ] + * + * RIGHT/ENTER on a row that owns a sub-level pushes a new column to the right + * (the existing columns stay put); LEFT/ESC pops the right-most column. Each + * column owns its own input group, so focus and the key handler always target + * the visible right-most column. + * + * This is a self-contained widget (plain lv_obj containers + groups); it does + * NOT subclass or depend on lv_menu. + */ + +#include "../../lvgl/lvgl.h" + +typedef struct colstack colstack_t; + +/* Builder callback: fill `col_body` with the rows for a freshly pushed column. + * `user` is the pointer passed to colstack_add_submenu(). */ +typedef void (*colstack_builder_t)(colstack_t * cs, lv_obj_t * col_body, void * user); + +/* Create the horizontal stack inside `parent` and its first (root) column. + * The stack takes the full size of `parent`. */ +colstack_t * colstack_create(lv_obj_t * parent); + +/* Body of the root column — add your top-level rows here. */ +lv_obj_t * colstack_root(colstack_t * cs); + +/* Push an empty column titled `title`, make it the active column (its group + * becomes the indev group) and scroll it into view. Returns the column body. + * Normally you don't call this directly — the row helpers below do. */ +lv_obj_t * colstack_push(colstack_t * cs, const char * title); + +/* Pop the right-most column (no-op on the root column). */ +void colstack_pop(colstack_t * cs); + +int colstack_depth(colstack_t * cs); + +/* Input group of the active (right-most) column — the shim adds reparented + * rows to this so keys reach them. */ +lv_group_t * colstack_cur_group(colstack_t * cs); + +/* ── Row helpers ───────────────────────────────────────────────────────────── + * These mirror the shape of the existing create_* helpers in helper.c so the + * page-builder files barely change when ported. Each returns the row object. */ + +/* A row that descends into a sub-column built lazily by `builder`. */ +lv_obj_t * colstack_add_submenu(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, colstack_builder_t builder, void * user); + +/* A plain inline toggle (like lv_menu switch). */ +lv_obj_t * colstack_add_switch(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, bool on); + +/* An inline slider edited in place: ENTER toggles edit mode, LEFT/RIGHT adjust. + * min/max/val are integers scaled by 10^precision; the label and the value + * reported to a binding are formatted with `precision` decimals. + * display_scale: multiply the displayed value by this (e.g., 100 for slider_pos*100 MB). */ +lv_obj_t * colstack_add_slider(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, int32_t min, int32_t max, int32_t val, + int precision, int display_scale); + +/* A dropdown that opens a radio list in a NEW column (the requested behaviour). + * `options` is a '\n'-separated list; `sel` is the initially selected index. */ +lv_obj_t * colstack_add_dropdown(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text, const char * options, int sel); + +/* A non-descending action row (fires a click; shown for completeness). */ +lv_obj_t * colstack_add_action(colstack_t * cs, lv_obj_t * col, const char * icon, + const char * text); + +/* Optional: a section-title label row. */ +lv_obj_t * colstack_add_title(colstack_t * cs, lv_obj_t * col, const char * text); + +/* Attach a value binding to a switch/slider/dropdown row: `on_change` is called + * with the new value string ("1"/"0" for a switch, a formatted number for a + * slider, the chosen option text for a dropdown) whenever the user changes it. */ +void colstack_set_binding(lv_obj_t * row, void (*on_change)(void * ctx, const char * value), + void * ctx); + +/* Tag a row with its gsmenu.sh param so its switch can be found and updated by + * an external state change (see colstack_reflect_switch). */ +void colstack_set_row_param(lv_obj_t * row, const char * param); + +/* Set the visual state of a bound switch (found by `param`) in any visible + * column, WITHOUT firing its binding — for state changed outside the menu + * (e.g. a hardware button toggling DVR recording). No-op if not shown. */ +void colstack_reflect_switch(colstack_t * cs, const char * param, bool on); + +/* Attach a "live" callback to a slider row: fired with the formatted value on + * every adjustment while editing, and again with the original value if the edit + * is cancelled — for live-preview effects (e.g. DRM video scaling). */ +void colstack_set_slider_live(lv_obj_t * row, void (*on_live)(void * ctx, const char * value), + void * ctx); + +/* Attach an activation callback to an action row (fired on the ENTER key press). */ +void colstack_set_action(lv_obj_t * row, void (*on_activate)(void * ctx), void * ctx); + +/* Called when Back is pressed at the root column (depth 1) — e.g. close the menu. */ +void colstack_set_root_back(colstack_t * cs, void (*cb)(void * ctx), void * ctx); +/* Input group of the root column (for the app to focus when opening the menu). */ +lv_group_t * colstack_root_group(colstack_t * cs); + +/* Enable/disable a row: disabled rows are greyed and can't be entered/activated + * (Back still works), used to gate the drone pages on VTX detection. */ +void colstack_set_enabled(lv_obj_t * row, bool enabled); + +/* The row that opened column `idx` (idx 1 = first pushed from the root). */ +lv_obj_t * colstack_column_origin(colstack_t * cs, int idx); + +/* Re-run the top column's builder into a cleared body (refresh a dynamic list). */ +void colstack_rebuild_current(colstack_t * cs); + +/* Resize the current (top) column to fit its widest row (clamped). Call after + * a column has been fully populated. */ +void colstack_autofit(colstack_t * cs); + +/* Guarantee the top (non-root) column can be left with Back. A dynamic page that + * emits only captions (e.g. an empty list showing just a "none found" note) leaves + * the column's input group with no focusable row — so no object receives the Back + * key and the user is stranded. Call after a column is populated: if the group is + * empty it drops in an inert, invisible focusable catcher so Back still pops. */ +void colstack_ensure_escapable(colstack_t * cs); diff --git a/src/gsmenu/executor.c b/src/gsmenu/executor.c index 17f8de78..2c36fb53 100644 --- a/src/gsmenu/executor.c +++ b/src/gsmenu/executor.c @@ -27,7 +27,7 @@ extern lv_indev_t * indev_drv; lv_obj_t * msgbox = NULL; lv_obj_t * msgbox_label = NULL; char buffer[BUFFER_SIZE]; -extern lv_group_t *loader_group; +lv_group_t *loader_group = NULL; /* was helper.c; only used as a NULL-check now */ extern lv_group_t * default_group; @@ -80,6 +80,7 @@ void show_error(CommandResult result) { lv_obj_add_style(button, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); msgbox_label = lv_msgbox_add_text(msgbox,""); // lv_label_set_long_mode(msgbox_label, LV_LABEL_LONG_MODE_SCROLL); + theme_msgbox(msgbox); }; build_output_string( @@ -225,167 +226,11 @@ void run_command_and_block(lv_event_t* e, const char *command, callback_fn callb data->callback_fn = callback; // Show loading screen - data->spinner = lv_spinner_create(lv_layer_top()); - lv_obj_add_style(data->spinner,&style_openipc, LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_center(data->spinner); + data->spinner = openipc_spinner_create(lv_layer_top()); // Create worker thread pthread_create(&data->thread_id, NULL, worker_thread, data); // Create timer to check for completion lv_timer_create(check_thread_complete, 30, data); // Check every 30ms -} - -void generic_switch_event_cb(lv_event_t * e) -{ - lv_key_t key = lv_indev_get_key(indev_drv); - if (key == LV_KEY_HOME) { - printf("skipping change as user wants to go back"); // workaround for see: https://github.com/lvgl/lvgl/issues/8093 - return; - } - lv_obj_t * target = lv_event_get_target(e); - thread_data_t * user_data = (thread_data_t *) lv_event_get_user_data(e); - char final_command[200] = "gsmenu.sh set "; - strcat(final_command,user_data->menu_page_data->type); - strcat(final_command," "); - strcat(final_command,user_data->menu_page_data->page); - strcat(final_command," "); - strcat(final_command,user_data->parameter); - strcat(final_command," "); - - if(lv_obj_has_state(target, LV_STATE_CHECKED)) { - strcat(final_command,"on"); - } else { - strcat(final_command,"off"); - } - - for(int i=0;iarguments[i]) { - if (lv_obj_check_type(user_data->arguments[i],&lv_textarea_class)) { - strcat(final_command," \""); - strcat(final_command,lv_textarea_get_text(user_data->arguments[i])); - strcat(final_command,"\""); - } - } - } - - if (user_data->blocking) - run_command(final_command); - else - run_command_and_block(e,final_command,NULL); -} - -void generic_checkbox_event_cb(lv_event_t * e) -{ - lv_key_t key = lv_indev_get_key(indev_drv); - if (key == LV_KEY_HOME) { - printf("skipping change as user wants to go back"); - return; - } - lv_obj_t * target = lv_event_get_target(e); - thread_data_t * user_data = (thread_data_t *) lv_event_get_user_data(e); - char final_command[200] = "gsmenu.sh set "; - strcat(final_command,user_data->menu_page_data->type); - strcat(final_command," "); - strcat(final_command,user_data->menu_page_data->page); - strcat(final_command," "); - strcat(final_command,user_data->parameter); - strcat(final_command," "); - - if(lv_obj_has_state(target, LV_STATE_CHECKED)) { - strcat(final_command,"on"); - } else { - strcat(final_command,"off"); - } - - for(int i=0;iarguments[i]) { - if (lv_obj_check_type(user_data->arguments[i],&lv_checkbox_class)) { - strcat(final_command," \""); - strcat(final_command,lv_checkbox_get_text(user_data->arguments[i])); - strcat(final_command,"\""); - } - } - } - - if (user_data->blocking) - run_command(final_command); - else - run_command_and_block(e,final_command,user_data->callback_fn); -} - -void generic_dropdown_event_cb(lv_event_t * e) -{ - lv_obj_t * target = lv_event_get_target(e); - thread_data_t * user_data = (thread_data_t*) lv_event_get_user_data(e); - char final_command[200] = "gsmenu.sh set "; - strcat(final_command,user_data->menu_page_data->type); - strcat(final_command," "); - strcat(final_command,user_data->menu_page_data->page); - strcat(final_command," "); - strcat(final_command,user_data->parameter); - strcat(final_command," "); - char arg[100] = ""; - lv_dropdown_get_selected_str(target,arg,99); - user_data->argument_string = strdup(arg); - strcat(final_command,"\""); - strcat(final_command,user_data->argument_string); - strcat(final_command,"\""); - - for(int i=0;iarguments[i]) { - if (lv_obj_check_type(user_data->arguments[i],&lv_textarea_class)) { - strcat(final_command," \""); - strcat(final_command,lv_textarea_get_text(user_data->arguments[i])); - strcat(final_command,"\""); - } - } - } - - if (user_data->blocking) - run_command(final_command); - else - run_command_and_block(e,final_command,NULL); -} - -void generic_slider_event_cb(lv_event_t * e) -{ - lv_obj_t * target = lv_event_get_target(e); - lv_obj_t * slider_label = lv_obj_get_child_by_type(lv_obj_get_parent(target),1,&lv_label_class); - - int32_t *start_value = lv_obj_get_user_data(slider_label); - if (start_value) { - if (*start_value == lv_slider_get_value(target)) { - return; - } - } - thread_data_t * user_data = lv_event_get_user_data(e); - char final_command[200] = "gsmenu.sh set "; - strcat(final_command,user_data->menu_page_data->type); - strcat(final_command," "); - strcat(final_command,user_data->menu_page_data->page); - strcat(final_command," "); - strcat(final_command,user_data->parameter); - strcat(final_command," "); - // precision - char buf[32]; - char format[16]; - snprintf(format, sizeof(format), "%%.%df", user_data->precision); - float scaled_value = (float)lv_slider_get_value(target) / powf(10, user_data->precision); - snprintf(buf, sizeof(buf), format, scaled_value); - user_data->argument_string = malloc(32); - sprintf(user_data->argument_string, "%s", buf); - strcat(final_command,user_data->argument_string); - strcat(final_command," "); - printf("final_command: %s\n",final_command); - - if (user_data->blocking) - run_command(final_command); - else - run_command_and_block(e,final_command,NULL); - - // Free previous user data if it exists - int32_t *old_value = lv_obj_get_user_data(slider_label); - if (old_value) free(old_value); - lv_obj_set_user_data(slider_label,NULL); } \ No newline at end of file diff --git a/src/gsmenu/executor.h b/src/gsmenu/executor.h index 9b20672b..0b5633e4 100644 --- a/src/gsmenu/executor.h +++ b/src/gsmenu/executor.h @@ -1,5 +1,25 @@ #pragma once -#include "ui.h" +#include +#include "../../lvgl/lvgl.h" + +/* Per-page metadata. These lived in ui.h with the old lv_menu; the executor is + * now their only real user, so they live here. */ +typedef void (*ReloadFunc)(lv_obj_t * page, lv_obj_t * target); + +typedef struct { + const char *caption; + lv_obj_t *target; + ReloadFunc reload; +} PageEntry; + +typedef struct { + char type[100]; + char page[100]; + void (*page_load_callback)(lv_obj_t * page); + lv_group_t *indev_group; + size_t entry_count; + PageEntry *page_entries; +} menu_page_data_t; #define MAX_CMD_ARGS 5 @@ -21,10 +41,5 @@ typedef struct { callback_fn callback_fn; } thread_data_t; - char* run_command(const char* command); void run_command_and_block(lv_event_t* e,const char * command, callback_fn callback); -void generic_switch_event_cb(lv_event_t * e); -void generic_checkbox_event_cb(lv_event_t * e); -void generic_dropdown_event_cb(lv_event_t * e); -void generic_slider_event_cb(lv_event_t * e); \ No newline at end of file diff --git a/src/gsmenu/gs_actions.c b/src/gsmenu/gs_actions.c deleted file mode 100644 index ed80c519..00000000 --- a/src/gsmenu/gs_actions.c +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include -#include - -#include "lvgl/lvgl.h" -#include "../main.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" -#include -#include "../menu.h" -#include "gs_actions.h" -#include "gs_scripts.h" - -#ifdef USE_SIMULATOR -void sig_handler(int exit_code) -{ - exit(exit_code); -} -#endif - -extern lv_group_t * default_group; - -lv_obj_t * restart_pp; -lv_obj_t * exit_pp; -lv_obj_t * gs_reboot; -lv_obj_t * gs_custom_action; -int restart_value = 1; -int exit_value = 2; - -void gs_actions_exit_pp(lv_event_t * event) -{ - int *signal = lv_event_get_user_data(event); - sig_handler(*signal); -} - -void create_gs_actions_menu(lv_obj_t * parent) { - - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "actions"); - menu_page_data->page_load_callback = gs_scripts_page_refresh; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - restart_pp = create_button(section, "Restart Pixelpilot"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(restart_pp,0,&lv_button_class),gs_actions_exit_pp,LV_EVENT_CLICKED,&restart_value); - - exit_pp = create_button(section, "Exit Pixelpilot"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(exit_pp,0,&lv_button_class),gs_actions_exit_pp,LV_EVENT_CLICKED,&exit_value); - - gs_reboot = create_button(section, "Reboot"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(gs_reboot,0,&lv_button_class),generic_button_callback,LV_EVENT_CLICKED,menu_page_data); - - - for (size_t i = 0; i < gsactions_count; i++) { - gs_custom_action = create_button(section, gsactions[i].label); - lv_obj_add_event_cb(lv_obj_get_child_by_type(gs_custom_action,0,&lv_button_class),custom_actions_cb,LV_EVENT_CLICKED,&gsactions[i]); - } - - gs_scripts_init_in_page(parent, menu_page_data); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/gs_actions.h b/src/gsmenu/gs_actions.h deleted file mode 100644 index 66aeabb8..00000000 --- a/src/gsmenu/gs_actions.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include -#include "../menu.h" - -extern MenuAction gsactions[MAX_ACTIONS]; -extern size_t gsactions_count; - -void create_gs_actions_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/gs_apfpv.c b/src/gsmenu/gs_apfpv.c deleted file mode 100644 index f19c3d7b..00000000 --- a/src/gsmenu/gs_apfpv.c +++ /dev/null @@ -1,339 +0,0 @@ -#include -#include -#include -#include -#include "ui.h" -#include "../input.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" -#include "executor.h" -#include "gs_wifi.h" - -extern lv_obj_t * menu; -extern gsmenu_control_mode_t control_mode; -extern lv_group_t * default_group; - -extern lv_obj_t * rx_mode; - -lv_obj_t *this_page; -lv_obj_t * ap_fpv_ssid; -lv_obj_t * ap_fpv_password; -lv_obj_t * reset_apfpv; -lv_obj_t * autoconnect_cont; - -static int adapter_count = 0; - -#define MAX_DEVICES 32 -#define MAX_NAME_LENGTH 64 -#define MAX_PATH_LENGTH 256 - -typedef struct { - char device_name[MAX_NAME_LENGTH]; - char usb_port[MAX_NAME_LENGTH]; -} NetworkDevice; - -int get_usb_port_udev(const char* device_name, char* usb_port, size_t port_size) { - char cmd[256]; - snprintf(cmd, sizeof(cmd), - "udevadm info -q path /sys/class/net/%s 2>/dev/null | grep -o 'usb[0-9]*/.*' | head -1", - device_name); - - char* result = run_command(cmd); - if (result == NULL || strlen(result) == 0) { - free(result); - return -1; - } - - // Remove newline and extract relevant portion - result[strcspn(result, "\n")] = '\0'; - - // Find the USB port part (typically after usbX/) - char* usb_part = strstr(result, "usb"); - if (usb_part != NULL) { - char* port_part = strchr(usb_part, '/'); - if (port_part != NULL) { - port_part++; // Move past the slash - - // Copy up to next slash or end of string - char* next_slash = strchr(port_part, '/'); - if (next_slash != NULL) { - size_t len = next_slash - port_part; - if (len < port_size) { - strncpy(usb_port, port_part, len); - usb_port[len] = '\0'; - free(result); - return 0; - } - } else { - // No more slashes, use the remaining string - if (strlen(port_part) < port_size) { - strcpy(usb_port, port_part); - free(result); - return 0; - } - } - } - } - - free(result); - return -1; -} - -// Main function to get the list of network devices with USB ports -NetworkDevice* get_wireless_devices(int* count) { - NetworkDevice* devices = malloc(MAX_DEVICES * sizeof(NetworkDevice)); - if (devices == NULL) { - *count = 0; - return NULL; - } - - // Get wireless device names using ip command - char* ip_output = run_command("ip -o link show | awk -F': ' '{print $2}' | grep '^wlx'"); - if (ip_output == NULL) { - *count = 0; - free(devices); - return NULL; - } - - *count = 0; - char* line = ip_output; - char* next_line; - - while (*count < MAX_DEVICES && (next_line = strchr(line, '\n')) != NULL) { - // Extract device name - size_t name_len = next_line - line; - if (name_len > 0 && name_len < MAX_NAME_LENGTH) { - strncpy(devices[*count].device_name, line, name_len); - devices[*count].device_name[name_len] = '\0'; - - // Remove trailing whitespace - devices[*count].device_name[strcspn(devices[*count].device_name, " \t\n")] = '\0'; - - // Get USB port information - devices[*count].usb_port[0] = '\0'; - - // Try udev method first - get_usb_port_udev(devices[*count].device_name, - devices[*count].usb_port, - MAX_NAME_LENGTH); - - (*count)++; - } - - line = next_line + 1; - } - - free(ip_output); - return devices; -} - -// Example usage and cleanup function -void free_devices(NetworkDevice* devices) { - free(devices); -} - -void focus_label(lv_event_t* e) -{ - lv_obj_t* obj = lv_event_get_current_target(e); - lv_obj_t* label = lv_event_get_user_data(e); - lv_obj_scroll_to_view_recursive(label, LV_ANIM_ON); -} - -static void btn_event_cb(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t * btn = lv_event_get_target(e); - lv_obj_t * kb = lv_event_get_user_data(e); - lv_obj_t * target_ta = lv_obj_get_user_data(btn); // Retrieve associated textarea - - if(code == LV_EVENT_CLICKED) { - if (target_ta) { - lv_keyboard_set_textarea(kb, target_ta); - lv_obj_remove_flag(kb, LV_OBJ_FLAG_HIDDEN); - lv_obj_scroll_to_view_recursive(target_ta, LV_ANIM_OFF); - lv_indev_wait_release(lv_event_get_param(e)); - lv_group_focus_obj(kb); - } - } -} - -static void change_textarea_value(lv_event_t *e ,lv_obj_t * ta) { - - thread_data_t * user_data = (thread_data_t*) lv_obj_get_user_data(ta); - char final_command[200] = "gsmenu.sh set "; - strcat(final_command,user_data->menu_page_data->type); - strcat(final_command," "); - strcat(final_command,user_data->menu_page_data->page); - strcat(final_command," "); - strcat(final_command,user_data->parameter); - strcat(final_command," "); - user_data->argument_string = strdup(lv_textarea_get_text(ta)); - strcat(final_command,"\""); - strcat(final_command,user_data->argument_string); - strcat(final_command,"\""); - - run_command_and_block(e,final_command,NULL); -} - -static void kb_event_cb(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t * ta = lv_event_get_target(e); - lv_obj_t * kb = lv_event_get_user_data(e); - - if (code == LV_EVENT_FOCUSED) { - control_mode = GSMENU_CONTROL_MODE_KEYBOARD; - - } - else if (code == LV_EVENT_DEFOCUSED) - { - control_mode = GSMENU_CONTROL_MODE_NAV; - } - else if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) { - lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); - lv_indev_reset(NULL, ta); /*To forget the last clicked object to make it focusable again*/ - lv_group_focus_obj(lv_obj_get_child_by_type(lv_obj_get_parent(lv_keyboard_get_textarea(kb)),0,&lv_button_class)); - lv_obj_update_layout(lv_obj_get_parent(kb)); - } - - if(code == LV_EVENT_READY) { - change_textarea_value(e,lv_keyboard_get_textarea(kb)); - } -} - -void checkbox_event_cb(void) -{ - generic_page_load_callback(this_page); -} - -void apfpv_page_load_callback(lv_obj_t *page) { - - this_page = page; - menu_page_data_t *menu_page_data = lv_obj_get_user_data(page); - PageEntry *entries = menu_page_data->page_entries; - - if (adapter_count == 0 ) { - NetworkDevice* devices = get_wireless_devices(&adapter_count); - - if (devices == NULL) { - printf("Failed to get device list\n"); - } - - printf("Found %d wireless devices:\n", adapter_count); - for (int i = 0; i < adapter_count; i++) { - printf("Device: %s -> USB Port: %s\n", - devices[i].device_name, - devices[i].usb_port[0] ? devices[i].usb_port : "Unknown"); - lv_group_set_default(menu_page_data->indev_group); - - // Create dynamic strings - char checkbox_label[128]; - char loading_text[128]; - char param_text[128]; - - snprintf(checkbox_label, sizeof(checkbox_label), "Device: %s USB: %s", - devices[i].device_name, - devices[i].usb_port[0] ? devices[i].usb_port : "Unknown"); - - snprintf(loading_text, sizeof(loading_text), "Loading device %s...", - devices[i].device_name); - - snprintf(param_text, sizeof(param_text), "status %s", - devices[i].device_name); - - lv_obj_t * adapter = create_checkbox(autoconnect_cont, LV_SYMBOL_WIFI, checkbox_label, devices[i].device_name, menu_page_data, false); - thread_data_t* data = lv_obj_get_user_data(lv_obj_get_child_by_type(adapter,0,&lv_checkbox_class)); - data->callback_fn = checkbox_event_cb; - lv_obj_t * adapter_status = create_text(autoconnect_cont, LV_SYMBOL_SETTINGS, "Status", param_text, menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - - lv_obj_add_event_cb(lv_obj_get_child_by_type(adapter,0,&lv_checkbox_class), focus_label, LV_EVENT_FOCUSED, adapter_status); - - add_entry_to_menu_page(menu_page_data, loading_text, adapter, reload_checkbox_value); - add_entry_to_menu_page(menu_page_data, loading_text, adapter_status, reload_label_value); - - lv_group_set_default(default_group); - } - - free_devices(devices); - - } - - generic_page_load_callback(page); -} - -void gs_actions_reset_apfpv(lv_event_t * event) -{ - lv_textarea_set_text(lv_obj_get_child_by_type(ap_fpv_ssid,0,&lv_textarea_class),"OpenIPC"); - lv_textarea_set_text(lv_obj_get_child_by_type(ap_fpv_password,0,&lv_textarea_class),"12345678"); - run_command_and_block(event,"gsmenu.sh set gs apfpv reset",NULL); - lv_obj_send_event(lv_obj_get_child_by_type(rx_mode,0,&lv_dropdown_class),LV_EVENT_VALUE_CHANGED,NULL); - - int child_count = lv_obj_get_child_count(autoconnect_cont); - for (int i = child_count - 1; i >= 0; i--) { - lv_obj_t *child = lv_obj_get_child(autoconnect_cont, i); - int grand_child_count = lv_obj_get_child_count(child); - - for (int j = grand_child_count - 1; j >= 0; j--) { - lv_obj_t *grand_child = lv_obj_get_child(child, j); - thread_data_t* user_data = lv_obj_get_user_data(grand_child); - - if (user_data != NULL) { - delete_menu_page_entry_by_obj(user_data->menu_page_data,child); - } - } - lv_obj_del(child); - } - adapter_count = 0; -} - -void create_apfpv_menu(lv_obj_t * parent) { - - menu_page_data_t *menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "apfpv"); - menu_page_data->page_load_callback = apfpv_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - create_text(parent, NULL, "Connection Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - lv_obj_t * cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - ap_fpv_ssid = create_textarea(cont, "OpenIPC", "SSID", "ssid", menu_page_data, false); - ap_fpv_password = create_textarea(cont, "12345678", "Password", "password", menu_page_data, true); - - reset_apfpv = create_button(section, "Reset APFPV"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(reset_apfpv,0,&lv_button_class),gs_actions_reset_apfpv,LV_EVENT_CLICKED,NULL); - - lv_obj_t * kb = lv_keyboard_create(lv_obj_get_parent(section)); - lv_obj_add_flag(kb, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_style(kb, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(kb, &style_openipc, LV_PART_ITEMS| LV_STATE_FOCUS_KEY); - lv_obj_add_style(kb, &style_openipc_dark_background, LV_PART_ITEMS| LV_STATE_DEFAULT); - lv_obj_add_style(kb, &style_openipc_textcolor, LV_PART_ITEMS| LV_STATE_FOCUS_KEY); - lv_obj_add_style(kb, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_add_event_cb(lv_obj_get_child_by_type(ap_fpv_ssid,0,&lv_button_class), btn_event_cb, LV_EVENT_ALL, kb); - lv_obj_add_event_cb(lv_obj_get_child_by_type(ap_fpv_password,0,&lv_button_class), btn_event_cb, LV_EVENT_ALL, kb); - lv_obj_add_event_cb(kb, kb_event_cb, LV_EVENT_ALL,kb); - lv_keyboard_set_textarea(kb, NULL); - - create_text(parent, NULL, "Autoconnect", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - autoconnect_cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(autoconnect_cont, LV_FLEX_FLOW_COLUMN); - - add_entry_to_menu_page(menu_page_data,"Loading SSID ...", ap_fpv_ssid, reload_textarea_value ); - add_entry_to_menu_page(menu_page_data,"Loading Password ...", ap_fpv_password, reload_textarea_value ); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/gs_apfpv.h b/src/gsmenu/gs_apfpv.h deleted file mode 100644 index d5d20bfd..00000000 --- a/src/gsmenu/gs_apfpv.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_apfpv_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/gs_dvr.c b/src/gsmenu/gs_dvr.c deleted file mode 100644 index 38d6e534..00000000 --- a/src/gsmenu/gs_dvr.c +++ /dev/null @@ -1,146 +0,0 @@ -#include -#include -#include -#include - -#include "lvgl/lvgl.h" -#include "../main.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" -#include "gs_dvrplayer.h" - -extern char* dvr_template; -extern lv_group_t * default_group; -extern lv_obj_t * dvr_screen; -extern lv_indev_t * indev_drv; -extern lv_group_t * dvr_group; -lv_group_t * dvr_page_group; - - -char path[256]; -lv_obj_t* rec_list = NULL; - -// Structure to hold file information -typedef struct { - char* filepath; -} file_data_t; - -// Button event handler -void button_event_handler(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - if (code == LV_EVENT_CLICKED) { - lv_obj_t* btn = lv_event_get_target(e); - file_data_t* data = (file_data_t*)lv_obj_get_user_data(btn); - printf("Selected video: %s\n", data->filepath); -#ifndef USE_SIMULATOR - switch_pipeline_source("file",data->filepath); -#else - printf("switch_pipeline_source(\"file\",data->filepath);\n"); -#endif - lv_screen_load(dvr_screen); - lv_indev_set_group(indev_drv,dvr_group); - change_playbutton_label(LV_SYMBOL_PAUSE); - } - - else if (code == LV_EVENT_DELETE) { - lv_obj_t* btn = lv_event_get_target(e); - file_data_t* data = (file_data_t*)lv_obj_get_user_data(btn); - if (data) { - free(data->filepath); - free(data); - } - } -} - - -void dvr_menu_load_callback(lv_obj_t * page) { - - menu_page_data_t * menu_page_data = (menu_page_data_t *) lv_obj_get_user_data(page); - lv_indev_set_group(indev_drv,menu_page_data->indev_group); - lv_group_set_default(menu_page_data->indev_group); - - if (rec_list) { - lv_obj_clean(rec_list); - } else { - rec_list = lv_list_create(page); - lv_obj_set_size(rec_list, LV_PCT(100), LV_SIZE_CONTENT); // Make the list fill the parent - lv_obj_center(rec_list); - lv_obj_add_style(rec_list, &style_openipc, LV_PART_SELECTED | LV_STATE_CHECKED); - lv_obj_add_style(rec_list, &style_openipc_section, LV_PART_MAIN); - } - - DIR* dir; - struct dirent* ent; - - // Open directory and add file names to the list - if ((dir = opendir(path)) != NULL) { - while ((ent = readdir(dir)) != NULL) { - // Check for .mp4 files - char* ext = strrchr(ent->d_name, '.'); - if (ext && strcmp(ext, ".mp4") == 0) { - // Add a new button to the list with the filename as its text - lv_obj_t* btn = lv_list_add_btn(rec_list, LV_SYMBOL_VIDEO, ent->d_name); - - // Store the full file path in the button's user data - size_t path_len = strlen(path) + strlen(ent->d_name) + 1; // +1 for '\0' - file_data_t* data = malloc(sizeof(file_data_t)); - data->filepath = malloc(path_len); - snprintf(data->filepath, path_len, "%s%s", path, ent->d_name); // Path already has a trailing slash - lv_obj_set_user_data(btn, data); - - lv_obj_add_event_cb(btn, button_event_handler, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(btn, generic_back_event_handler, LV_EVENT_KEY,NULL); - - lv_obj_add_style(btn, &style_openipc, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(btn, &style_openipc_section, LV_PART_MAIN); - } - } - closedir(dir); - } else { - // Handle case where directory cannot be opened - lv_obj_t* btn = lv_list_add_btn(rec_list, LV_SYMBOL_WARNING, "Could not open directory"); - lv_obj_add_event_cb(btn, generic_back_event_handler, LV_EVENT_KEY,NULL); - lv_obj_add_style(btn, &style_openipc, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - } - if (lv_obj_get_child_count(rec_list) == 0) { - lv_obj_t* btn = lv_list_add_btn(rec_list, LV_SYMBOL_VIDEO, "No recordings found"); - lv_obj_add_event_cb(btn, generic_back_event_handler, LV_EVENT_KEY,NULL); - lv_obj_add_style(btn, &style_openipc, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - } - lv_group_set_default(default_group); -} - -void create_gs_dvr_menu(lv_obj_t * parent) { - - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "dvr"); - menu_page_data->page_load_callback = dvr_menu_load_callback; - menu_page_data->indev_group = lv_group_create(); - dvr_page_group = menu_page_data->indev_group; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - // Extract directory path from dvr_template - if (dvr_template) { // Check if dvr_template is not NULL - const char *last_slash = strrchr(dvr_template, '/'); - if (last_slash) { - size_t path_len = last_slash - dvr_template + 1; - strncpy(path, dvr_template, path_len); - path[path_len] = '\0'; // Null-terminate - } else { - strcpy(path, "./"); // Fallback to current directory if no slash found - } - printf("Extracted path: \"%s\"\n", path); - - dvr_menu_load_callback(parent); - } else { - strcpy(path, "./"); // Fallback to current directory if dvr_template is null - } - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/gs_dvr.h b/src/gsmenu/gs_dvr.h deleted file mode 100644 index 90d6465c..00000000 --- a/src/gsmenu/gs_dvr.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_gs_dvr_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/gs_dvrplayer.c b/src/gsmenu/gs_dvrplayer.c index 9aaf2d0a..831ad2cd 100644 --- a/src/gsmenu/gs_dvrplayer.c +++ b/src/gsmenu/gs_dvrplayer.c @@ -10,7 +10,7 @@ extern lv_obj_t * pp_menu_screen; extern lv_obj_t * dvr_screen; extern lv_group_t * default_group; -extern lv_group_t * dvr_page_group; +lv_group_t * dvr_page_group; /* moved here from the old gs_dvr.c */ lv_group_t * dvr_group; lv_obj_t * btn_container = NULL; diff --git a/src/gsmenu/gs_main.c b/src/gsmenu/gs_main.c deleted file mode 100644 index 0077c6fe..00000000 --- a/src/gsmenu/gs_main.c +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include -#include -#include -#include "ui.h" -#include "../input.h" -#include "helper.h" -#include "styles.h" -#include "executor.h" -#include "gs_wifi.h" - - -extern lv_obj_t * menu; -extern gsmenu_control_mode_t control_mode; -extern lv_group_t *main_group; -extern lv_group_t *default_group; - -lv_obj_t * version ; -lv_obj_t * disk ; -lv_obj_t * wfb_nics ; -lv_obj_t * channel ; -lv_obj_t * screen_mode ; - -void gs_main_page_load_callback(lv_obj_t * page) -{ - reload_label_value(page,version); - reload_label_value(page,disk); - reload_label_value(page,wfb_nics); - reload_label_value(page,channel); - reload_label_value(page,screen_mode); -} - -void create_main_menu(lv_obj_t * parent) { - - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "main"); - menu_page_data->page_load_callback = gs_main_page_load_callback; - menu_page_data->indev_group = main_group; - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - lv_obj_t * cont; - lv_obj_t * label; - lv_obj_t * section; - lv_obj_t * obj; - - create_text(parent, NULL, "", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - channel = create_text(cont, LV_SYMBOL_SETTINGS, "Channel", "Channel", menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - screen_mode = create_text(cont, LV_SYMBOL_SETTINGS, "HDMI-OUT", "HDMI-OUT", menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - wfb_nics = create_text(cont, LV_SYMBOL_SETTINGS, "WFB_NICS", "WFB_NICS", menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - disk = create_text(cont, LV_SYMBOL_SETTINGS, "Version", "Disk", menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - version = create_text(cont, LV_SYMBOL_SETTINGS, "Version", "Version", menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - - lv_group_set_default(default_group); - -} diff --git a/src/gsmenu/gs_main.h b/src/gsmenu/gs_main.h deleted file mode 100644 index a81b4f0c..00000000 --- a/src/gsmenu/gs_main.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_main_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/gs_scripts.c b/src/gsmenu/gs_scripts.c index 358036fd..8900a12a 100644 --- a/src/gsmenu/gs_scripts.c +++ b/src/gsmenu/gs_scripts.c @@ -40,18 +40,19 @@ typedef struct { char *output; size_t output_len; lv_group_t *group; + uint32_t term_tick; /* tick when SIGTERM was sent (0 = not stopping); used + to escalate to SIGKILL if the script ignores it */ } script_runner_t; -static lv_obj_t *g_parent_page; -static lv_obj_t *g_scripts_section; -static menu_page_data_t *g_menu_page_data; static char *g_script_names[MAX_SCRIPT_COUNT]; static int g_script_count; static lv_obj_t *g_confirm_msgbox; static lv_group_t *g_confirm_group; static char g_pending_script[256]; +/* group to restore the keypad to after the confirm/runner closes; captured at + * run time so the feature works from colmenu (no menu_page_data there). */ +static lv_group_t *g_return_group; -static void build_script_list(void); static script_runner_t g_runner = { .pid = -1, .fd = -1, @@ -114,6 +115,11 @@ static int collect_scripts(char **scripts, int max_scripts) return count; } +int gs_scripts_collect(char **names, int max) +{ + return collect_scripts(names, max); +} + static void clear_script_names(void) { for (int i = 0; i < g_script_count; i++) { @@ -158,8 +164,15 @@ static void runner_append(const char *text) g_runner.output[g_runner.output_len] = '\0'; if (g_runner.output_ta && lv_obj_is_valid(g_runner.output_ta)) { - lv_textarea_set_text(g_runner.output_ta, g_runner.output); - lv_textarea_set_cursor_pos(g_runner.output_ta, LV_TEXTAREA_CURSOR_LAST); + lv_obj_t *ta = g_runner.output_ta; + lv_textarea_set_text(ta, g_runner.output); + lv_textarea_set_cursor_pos(ta, LV_TEXTAREA_CURSOR_LAST); + /* scroll the newest output into view */ + lv_obj_update_layout(ta); + int32_t sb = lv_obj_get_scroll_bottom(ta); + if (sb > 0) { + lv_obj_scroll_by(ta, 0, -sb, LV_ANIM_OFF); + } } } @@ -190,10 +203,17 @@ static void script_poll_timer_cb(lv_timer_t *timer) } if (g_runner.running && g_runner.pid > 0) { + /* Escalate to SIGKILL if the script ignored SIGTERM for over a second. */ + if (g_runner.term_tick && + lv_tick_elaps(g_runner.term_tick) > 1000) { + kill(-g_runner.pid, SIGKILL); + g_runner.term_tick = 0; + } int status = 0; pid_t ret = waitpid(g_runner.pid, &status, WNOHANG); if (ret == g_runner.pid) { g_runner.running = false; + g_runner.term_tick = 0; runner_finish_message(status); if (g_runner.fd >= 0) { close(g_runner.fd); @@ -219,12 +239,15 @@ static void close_runner_ui(void) } if (g_runner.running && g_runner.pid > 0) { - kill(g_runner.pid, SIGTERM); + /* Tearing down — force-kill the whole process group (SIGKILL can't be + * ignored) so nothing is left running when the dialog closes. */ + kill(-g_runner.pid, SIGKILL); waitpid(g_runner.pid, NULL, 0); } g_runner.running = false; g_runner.pid = -1; + g_runner.term_tick = 0; if (g_runner.msgbox && lv_obj_is_valid(g_runner.msgbox)) { lv_msgbox_close(g_runner.msgbox); @@ -243,9 +266,9 @@ static void close_runner_ui(void) g_runner.group = NULL; } - /* Restore focus to the scripts page. */ - if (g_menu_page_data && g_menu_page_data->indev_group) { - lv_indev_set_group(indev_drv, g_menu_page_data->indev_group); + /* Restore focus to wherever the run was launched from. */ + if (g_return_group) { + lv_indev_set_group(indev_drv, g_return_group); } } @@ -259,7 +282,12 @@ static void script_stop_cb(lv_event_t *e) { (void)e; if (g_runner.running && g_runner.pid > 0) { - kill(g_runner.pid, SIGTERM); + /* The child did setsid(), so it leads its own process group. Signal the + * whole GROUP (negative pid) — otherwise a foreground child of the shell + * (a loop, a sleep, a spawned tool) keeps running and the script never + * actually stops. */ + kill(-g_runner.pid, SIGTERM); + g_runner.term_tick = lv_tick_get() ? lv_tick_get() : 1; runner_append("\nStopping script...\n"); } } @@ -394,6 +422,8 @@ static void start_script_runner(const char *script_name) lv_obj_add_style(g_runner.close_btn, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_style(g_runner.close_btn, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + theme_msgbox(g_runner.msgbox); + /* Fixed size: triggers msgbox_size_changed_event_cb which sets flex_grow=1 * on the content area, making the textarea fill the available height. */ lv_obj_set_size(g_runner.msgbox, lv_pct(98), lv_pct(92)); @@ -423,9 +453,9 @@ static void confirm_cancel_cb(lv_event_t *e) lv_group_del(g_confirm_group); g_confirm_group = NULL; } - /* Restore focus to the scripts page. */ - if (g_menu_page_data && g_menu_page_data->indev_group) { - lv_indev_set_group(indev_drv, g_menu_page_data->indev_group); + /* Restore focus to wherever the run was launched from. */ + if (g_return_group) { + lv_indev_set_group(indev_drv, g_return_group); } } @@ -443,20 +473,17 @@ static void confirm_execute_cb(lv_event_t *e) start_script_runner(g_pending_script); } -static void run_script_cb(lv_event_t *e) +/* Build the confirm dialog for g_pending_script. Deferred via lv_async_call from + * gs_scripts_run so the key press that launched it has released first (colmenu + * actions fire on the key press, and the release would otherwise click a button). */ +static void show_confirm_dialog(void *unused) { - const char *script_name = (const char *)lv_event_get_user_data(e); - if (!script_name || strchr(script_name, '/')) { - return; - } - + (void)unused; if (g_confirm_msgbox && lv_obj_is_valid(g_confirm_msgbox)) { lv_msgbox_close(g_confirm_msgbox); g_confirm_msgbox = NULL; } - snprintf(g_pending_script, sizeof(g_pending_script), "%s", script_name); - g_confirm_msgbox = lv_msgbox_create(NULL); lv_obj_add_style(g_confirm_msgbox, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); lv_msgbox_add_title(g_confirm_msgbox, "Confirm Execution"); @@ -480,6 +507,8 @@ static void run_script_cb(lv_event_t *e) lv_obj_add_event_cb(cancel_btn, confirm_cancel_cb, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(exec_btn, confirm_execute_cb, LV_EVENT_CLICKED, NULL); + theme_msgbox(g_confirm_msgbox); + lv_obj_set_width(g_confirm_msgbox, lv_pct(75)); /* Create a dedicated input group so key navigation reaches Cancel/Execute @@ -487,68 +516,21 @@ static void run_script_cb(lv_event_t *e) g_confirm_group = lv_group_create(); lv_group_add_obj(g_confirm_group, cancel_btn); lv_group_add_obj(g_confirm_group, exec_btn); + lv_group_focus_obj(cancel_btn); /* default to the safe choice */ lv_indev_set_group(indev_drv, g_confirm_group); + /* the key press that opened this (colmenu actions fire on press) is still + * down — without this its release would immediately click a button. */ + lv_indev_wait_release(indev_drv); } -static void build_script_list(void) +void gs_scripts_run(const char *name) { - if (!g_parent_page || !g_menu_page_data) { + if (!name || strchr(name, '/')) { return; } - - if (g_scripts_section && lv_obj_is_valid(g_scripts_section)) { - lv_obj_del(g_scripts_section); - g_scripts_section = NULL; - } - - /* Set the page group as default so every new focusable widget (the inner - * lv_btn created by create_button) auto-registers into the correct group. - * This also ensures generic_back_event_handler receives key events so - * HOME / back navigation works. */ - lv_group_set_default(g_menu_page_data->indev_group); - - g_scripts_section = lv_menu_section_create(g_parent_page); - lv_obj_add_style(g_scripts_section, &style_openipc_section, 0); - - g_script_count = collect_scripts(g_script_names, MAX_SCRIPT_COUNT); - - if (g_script_count == 0) { - create_text(g_scripts_section, LV_SYMBOL_WARNING, "No scripts found", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - create_text(g_scripts_section, NULL, "Place .sh files in " SCRIPT_DIR, NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - } - - for (int i = 0; i < g_script_count; i++) { - /* Build a display label with a play icon; user_data keeps the raw name - * so the execute path is not affected. */ - char display[300]; - snprintf(display, sizeof(display), LV_SYMBOL_PLAY " %s", g_script_names[i]); - lv_obj_t *row = create_button(g_scripts_section, display); - lv_obj_t *btn = lv_obj_get_child_by_type(row, 0, &lv_button_class); - lv_obj_add_event_cb(btn, run_script_cb, LV_EVENT_CLICKED, g_script_names[i]); - } - - /* Restore the global default group. */ - lv_group_set_default(default_group); + /* remember where to hand the keypad back (the column the run started from) */ + g_return_group = lv_indev_get_group(indev_drv); + snprintf(g_pending_script, sizeof(g_pending_script), "%s", name); + show_confirm_dialog(NULL); } -void gs_scripts_page_refresh(lv_obj_t *page) -{ - (void)page; - clear_script_names(); - if (g_scripts_section && lv_obj_is_valid(g_scripts_section)) { - lv_obj_del(g_scripts_section); - g_scripts_section = NULL; - } - build_script_list(); -} - -void gs_scripts_init_in_page(lv_obj_t *parent, menu_page_data_t *menu_page_data) -{ - g_parent_page = parent; - g_menu_page_data = menu_page_data; - - create_text(parent, LV_SYMBOL_DIRECTORY, "Custom Scripts", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - create_text(parent, NULL, "Scripts are loaded from " SCRIPT_DIR, NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - - build_script_list(); -} diff --git a/src/gsmenu/gs_scripts.h b/src/gsmenu/gs_scripts.h index 72227b92..1ffd568c 100644 --- a/src/gsmenu/gs_scripts.h +++ b/src/gsmenu/gs_scripts.h @@ -1,7 +1,8 @@ #pragma once #include "lvgl/lvgl.h" -#include "ui.h" -void gs_scripts_page_refresh(lv_obj_t *page); -void gs_scripts_init_in_page(lv_obj_t *parent, menu_page_data_t *menu_page_data); +/* colmenu integration: list the .sh files under the script dir (each strdup'd, + * caller owns), and run one (shows the confirm dialog + streaming output). */ +int gs_scripts_collect(char **names, int max); +void gs_scripts_run(const char *name); diff --git a/src/gsmenu/gs_system.c b/src/gsmenu/gs_system.c deleted file mode 100644 index c8912b9d..00000000 --- a/src/gsmenu/gs_system.c +++ /dev/null @@ -1,602 +0,0 @@ -#include -#include -#include -#include -#include "../main.h" -#ifndef USE_SIMULATOR -#include "../drm.h" -#endif -#include "../gstrtpreceiver.h" -#include "gs_system.h" -#include "lvgl/lvgl.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" - -extern lv_group_t * default_group; -extern lv_obj_t * menu; -extern lv_indev_t * indev_drv; -extern lv_obj_t * sub_gs_system_page; -enum RXMode RXMODE = WFB; - -lv_obj_t * rx_codec; -lv_obj_t * rx_mode; -lv_obj_t * gs_rendering; -lv_obj_t * connector; -lv_obj_t * resolution; -lv_obj_t * video_scale; -lv_obj_t * gs_live_colortrans; -lv_obj_t * gs_dvr_colortrans; - -// DVR widgets (unified section) -lv_obj_t * rec_enabled; -static lv_obj_t * dvr_mode_dd; -static lv_obj_t * dvr_max_size; -lv_obj_t * rec_fps; -static lv_obj_t * dvr_reenc_codec; -static lv_obj_t * dvr_reenc_fps; -static lv_obj_t * dvr_reenc_bitrate; -static lv_obj_t * dvr_reenc_resolution; -static lv_obj_t * dvr_reenc_osd; - -extern lv_obj_t * ap_fpv_ssid; -extern lv_obj_t * ap_fpv_password; - -extern int dvr_enabled; -extern bool enable_live_colortrans; -extern float live_colortrans_gain; -extern float live_colortrans_offset; -#ifndef USE_SIMULATOR -extern gamma_lut_controller lut_ctrl; -#endif - -// C interface to DVR control (defined in main.cpp) -void dvr_reenc_notify_colortrans(int enabled); -void dvr_reenc_set_fps(int fps); -void dvr_reenc_set_osd(int enabled); -void dvr_reenc_set_bitrate(int kbps); -void dvr_reenc_set_codec(int idx); -void dvr_reenc_set_resolution(int idx); -void dvr_set_mode(int mode); -void dvr_start_all(void); -void dvr_stop_all(void); -int dvr_get_mode(void); -int dvr_reenc_get_fps(void); -int dvr_reenc_get_bitrate(void); -int dvr_reenc_get_osd(void); -int dvr_reenc_get_codec(void); -int dvr_reenc_get_resolution(void); -void dvr_set_max_size(int mb); -int dvr_get_max_size(void); -#ifndef USE_SIMULATOR -void drm_set_video_scale(float factor); -#endif - -// Raw FPS setter (defined in dvr.cpp) -typedef struct Dvr* Dvr; -void dvr_set_video_framerate(Dvr* dvr, int f); -extern Dvr *dvr_raw; - -// Show/hide DVR widgets based on current mode. -// mode 0=raw: show raw fps, hide reenc options -// mode 1=reencode: hide raw fps, show reenc options -// mode 2=both: show all -static void update_dvr_mode_visibility(void) -{ - int mode = dvr_get_mode(); - bool show_raw = (mode == 0 || mode == 2); - bool show_reenc = (mode == 1 || mode == 2); - - if (show_raw) lv_obj_remove_flag(rec_fps, LV_OBJ_FLAG_HIDDEN); - else lv_obj_add_flag(rec_fps, LV_OBJ_FLAG_HIDDEN); - - lv_obj_t *reenc_widgets[] = {dvr_reenc_codec, dvr_reenc_fps, - dvr_reenc_bitrate, dvr_reenc_resolution, dvr_reenc_osd}; - for (int i = 0; i < 5; i++) { - if (show_reenc) lv_obj_remove_flag(reenc_widgets[i], LV_OBJ_FLAG_HIDDEN); - else lv_obj_add_flag(reenc_widgets[i], LV_OBJ_FLAG_HIDDEN); - } -} - -// ── Custom reload functions ─────────────────────────────────────────────────── - -static void reload_live_colortrans_fn(lv_obj_t *page, lv_obj_t *parameter) { - lv_obj_t *sw = lv_obj_get_child_by_type(parameter, 0, &lv_switch_class); - lv_lock(); - lv_obj_set_state(sw, LV_STATE_CHECKED, enable_live_colortrans); - lv_unlock(); -} - -static void reload_rec_enabled_fn(lv_obj_t *page, lv_obj_t *parameter) { - lv_obj_t *sw = lv_obj_get_child_by_type(parameter, 0, &lv_switch_class); - lv_lock(); - lv_obj_set_state(sw, LV_STATE_CHECKED, dvr_enabled); - lv_unlock(); -} - -static void reload_dvr_reenc_osd_fn(lv_obj_t *page, lv_obj_t *parameter) { - lv_obj_t *sw = lv_obj_get_child_by_type(parameter, 0, &lv_switch_class); - lv_lock(); - lv_obj_set_state(sw, LV_STATE_CHECKED, dvr_reenc_get_osd()); - lv_unlock(); -} - -static void reload_rx_mode_fn(lv_obj_t *page, lv_obj_t *parameter) { - reload_dropdown_value(page, parameter); - lv_lock(); - RXMODE = lv_dropdown_get_selected(lv_obj_get_child_by_type(parameter, 0, &lv_dropdown_class)); - lv_unlock(); -} - -// dvr_max_size: slider position = max_size/100, label shows actual MB (pos*100). -static void reload_dvr_max_size_fn(lv_obj_t *page, lv_obj_t *parameter) { - lv_obj_t *slider = lv_obj_get_child_by_type(parameter, 0, &lv_slider_class); - lv_obj_t *label = lv_obj_get_child_by_type(parameter, 1, &lv_label_class); - thread_data_t *param_user_data = (thread_data_t*)lv_obj_get_user_data(slider); - char *raw = get_paramater(page, param_user_data->parameter); - char *values_str = NULL; - char *value = split_value_and_options(raw, &values_str); - int slider_pos = atoi(value); - if (values_str) { - float min, max; - if (sscanf(values_str, "%f %f", &min, &max) == 2) { - lv_lock(); - lv_slider_set_range(slider, (int32_t)min, (int32_t)max); - lv_unlock(); - } - } - char buf[16]; - snprintf(buf, sizeof(buf), "%d", slider_pos * 100); - lv_lock(); - lv_slider_set_value(slider, slider_pos, LV_ANIM_OFF); - lv_label_set_text(label, buf); - lv_unlock(); -} - -// dvr_mode must be last among DVR entries so visibility is applied after all -// DVR items have been loaded. -static void reload_dvr_mode_fn(lv_obj_t *page, lv_obj_t *parameter) { - reload_dropdown_value(page, parameter); - lv_lock(); - update_dvr_mode_visibility(); - lv_unlock(); -} - -void toggle_rec_enabled() -{ - lv_obj_t * rec_switch = lv_obj_get_child_by_type(rec_enabled,0,&lv_switch_class); - if (lv_obj_has_state(rec_switch, LV_STATE_CHECKED)) lv_obj_clear_state(rec_switch, LV_STATE_CHECKED); - else lv_obj_add_state(rec_switch, LV_STATE_CHECKED); - lv_obj_send_event(rec_switch, LV_EVENT_VALUE_CHANGED, NULL); -} - -void rec_enabled_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - if (lv_obj_has_state(ta, LV_STATE_CHECKED)) { -#ifndef USE_SIMULATOR - dvr_start_all(); -#else - printf("dvr_start_all();\n"); -#endif - } else { -#ifndef USE_SIMULATOR - dvr_stop_all(); -#else - printf("dvr_stop_all();\n"); -#endif - } - } -} - - -void gs_live_colortrans_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - - if (lv_obj_has_state(ta, LV_STATE_CHECKED)) { -#ifndef USE_SIMULATOR - gamma_lut_enable(&lut_ctrl, live_colortrans_offset, live_colortrans_gain); -#endif - enable_live_colortrans = true; - lv_obj_invalidate(lv_screen_active()); -#ifndef USE_SIMULATOR - dvr_reenc_notify_colortrans(1); -#endif - printf("Live colortrans ENABLED (offset=%f, gain=%f)\n", - live_colortrans_offset, live_colortrans_gain); - } else { -#ifndef USE_SIMULATOR - gamma_lut_disable(&lut_ctrl); -#endif - enable_live_colortrans = false; - lv_obj_invalidate(lv_screen_active()); -#ifndef USE_SIMULATOR - dvr_reenc_notify_colortrans(0); -#endif - printf("Live colortrans DISABLED\n"); - } - } -} - -void rec_fps_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - char val[100] = ""; - lv_dropdown_get_selected_str(ta,val,99); - int fps = atoi(val); -#ifndef USE_SIMULATOR - dvr_set_video_framerate(dvr_raw, fps); -#else - printf("dvr_set_video_framerate(dvr_raw,%i);\n",fps); -#endif - } -} - -void dvr_reenc_fps_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - char val[32] = ""; - lv_dropdown_get_selected_str(ta, val, sizeof(val) - 1); - int fps = atoi(val); - if (fps > 0) { -#ifndef USE_SIMULATOR - dvr_reenc_set_fps(fps); -#else - printf("dvr_reenc_set_fps(%d);\n", fps); -#endif - } - } -} - -void dvr_reenc_osd_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - int osd_on = lv_obj_has_state(ta, LV_STATE_CHECKED) ? 1 : 0; -#ifndef USE_SIMULATOR - dvr_reenc_set_osd(osd_on); -#else - printf("dvr_reenc_set_osd(%d);\n", osd_on); -#endif - } -} - -void dvr_reenc_bitrate_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - char val[32] = ""; - lv_dropdown_get_selected_str(ta, val, sizeof(val) - 1); - int kbps = atoi(val); - if (kbps > 0) { -#ifndef USE_SIMULATOR - dvr_reenc_set_bitrate(kbps); -#else - printf("dvr_reenc_set_bitrate(%d);\n", kbps); -#endif - } - } -} - -void dvr_reenc_codec_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - int idx = lv_dropdown_get_selected(ta); // 0=h264, 1=h265 -#ifndef USE_SIMULATOR - dvr_reenc_set_codec(idx); -#else - printf("dvr_reenc_set_codec(%d);\n", idx); -#endif - } -} - -void dvr_reenc_resolution_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - int idx = lv_dropdown_get_selected(ta); // 0=720p, 1=1080p -#ifndef USE_SIMULATOR - dvr_reenc_set_resolution(idx); -#else - printf("dvr_reenc_set_resolution(%d);\n", idx); -#endif - } -} - -void resolution_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) - show_restart_notice(); -} - -void video_scale_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *slider = lv_event_get_target(e); - float factor = lv_slider_get_value(slider) / 100.0f; -#ifndef USE_SIMULATOR - drm_set_video_scale(factor); -#else - printf("drm_set_video_scale(%.2f);\n", factor); -#endif - } -} - -/* Reverts DRM to the pre-edit scale when the user cancels the slider. - * Reads start_value from the label's user_data if it's still there (we ran - * before generic_back_event_handler), otherwise falls back to the current - * slider value which generic_back_event_handler already restored. */ -static void video_scale_revert_cb(lv_event_t *e) { - if (lv_event_get_key(e) != LV_KEY_ESC) return; - lv_obj_t *slider = lv_event_get_target(e); - lv_obj_t *label = lv_obj_get_child_by_type(lv_obj_get_parent(slider), 1, &lv_label_class); - thread_data_t *data = lv_obj_get_user_data(slider); - - int32_t *start_value = label ? lv_obj_get_user_data(label) : NULL; - float factor; - if (start_value) - factor = *start_value / powf(10, data->precision); - else - factor = lv_slider_get_value(slider) / powf(10, data->precision); - -#ifndef USE_SIMULATOR - drm_set_video_scale(factor); -#else - printf("drm_set_video_scale(%.2f) [revert]\n", factor); -#endif -} - -void dvr_max_size_label_cb(lv_event_t *e) { - lv_obj_t *slider = lv_event_get_target(e); - lv_obj_t *label = lv_obj_get_child_by_type(lv_obj_get_parent(slider), 1, &lv_label_class); - char buf[16]; - snprintf(buf, sizeof(buf), "%d", lv_slider_get_value(slider) * 100); - lv_label_set_text(label, buf); -} - -void dvr_max_size_cb(lv_event_t *e) { - lv_obj_t *slider = lv_event_get_target(e); - int mb = lv_slider_get_value(slider) * 100; -#ifndef USE_SIMULATOR - dvr_set_max_size(mb); -#else - printf("dvr_set_max_size(%d);\n", mb); -#endif -} - -void dvr_mode_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - int mode = lv_dropdown_get_selected(ta); // 0=raw, 1=reencode, 2=both -#ifndef USE_SIMULATOR - dvr_set_mode(mode); -#else - printf("dvr_set_mode(%d);\n", mode); -#endif - update_dvr_mode_visibility(); - } -} - -void rx_mode_cb(lv_event_t *e) { - lv_event_code_t event = lv_event_get_code(e); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_t *ta = lv_event_get_target(e); - RXMODE = lv_dropdown_get_selected(ta); - gsmenu_toggle_rxmode(); - } -} - -// ── Sub-page back navigation ────────────────────────────────────────────────── - -// Back handler for Receiver/Display/DVR sub-pages: go up to System Settings -// landing page rather than all the way to the main menu. -static void gs_system_sub_back_handler(lv_event_t * e) { - lv_key_t key = lv_event_get_key(e); - if (key == LV_KEY_HOME) { - menu_page_data_t *sys_data = (menu_page_data_t *)lv_obj_get_user_data(sub_gs_system_page); - lv_menu_set_page(menu, sub_gs_system_page); - if (sys_data) lv_indev_set_group(indev_drv, sys_data->indev_group); - } -} - -// Replace generic_back_event_handler on the focusable inner child of a widget -// container with gs_system_sub_back_handler. -static void use_sub_back_handler(lv_obj_t *container) { - lv_obj_t *inner = lv_obj_get_child_by_type(container, 0, &lv_dropdown_class); - if (!inner) inner = lv_obj_get_child_by_type(container, 0, &lv_switch_class); - if (!inner) inner = lv_obj_get_child_by_type(container, 0, &lv_slider_class); - if (!inner) return; - lv_obj_remove_event_cb(inner, generic_back_event_handler); - lv_obj_add_event_cb(inner, gs_system_sub_back_handler, LV_EVENT_KEY, NULL); -} - -// ── Sub-page create functions ───────────────────────────────────────────────── - -void create_gs_system_receiver_menu(lv_obj_t * parent) { - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "system"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "Receiver", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - rx_codec = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Codec", "", "rx_codec", menu_page_data, false); - use_sub_back_handler(rx_codec); - rx_mode = create_dropdown(cont, LV_SYMBOL_SETTINGS, "RX Mode", "", "rx_mode", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(rx_mode, 0, &lv_dropdown_class), rx_mode_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(rx_mode); - thread_data_t* data = lv_obj_get_user_data(lv_obj_get_child_by_type(rx_mode, 0, &lv_dropdown_class)); - data->arguments[0] = lv_obj_get_child_by_type(ap_fpv_ssid, 0, &lv_textarea_class); - data->arguments[1] = lv_obj_get_child_by_type(ap_fpv_password, 0, &lv_textarea_class); - reload_dropdown_value(parent, rx_mode); - RXMODE = lv_dropdown_get_selected(lv_obj_get_child_by_type(rx_mode, 0, &lv_dropdown_class)); - - add_entry_to_menu_page(menu_page_data, "Loading Codec ...", rx_codec, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading RX Mode ...", rx_mode, reload_rx_mode_fn); - - lv_group_set_default(default_group); -} - -void create_gs_system_display_menu(lv_obj_t * parent) { - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "system"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "Display", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - gs_rendering = create_switch(cont, LV_SYMBOL_SETTINGS, "GS Rendering", "gs_rendering", menu_page_data, false); - use_sub_back_handler(gs_rendering); - connector = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Connector", "", "connector", menu_page_data, false); - use_sub_back_handler(connector); - resolution = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Resolution", "", "resolution", menu_page_data, true); - lv_obj_add_event_cb(lv_obj_get_child_by_type(resolution, 0, &lv_dropdown_class), resolution_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(resolution); - video_scale = create_slider(cont, LV_SYMBOL_SETTINGS, "Video scale factor", "video_scale", menu_page_data, false, 2); - lv_obj_add_event_cb(lv_obj_get_child_by_type(video_scale, 0, &lv_slider_class), video_scale_cb, LV_EVENT_VALUE_CHANGED, NULL); - lv_obj_add_event_cb(lv_obj_get_child_by_type(video_scale, 0, &lv_slider_class), video_scale_revert_cb, LV_EVENT_KEY, NULL); - use_sub_back_handler(video_scale); - gs_live_colortrans = create_switch(cont, LV_SYMBOL_SETTINGS, "Live Colortrans", "gs_live_colortrans", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(gs_live_colortrans, 0, &lv_switch_class), gs_live_colortrans_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(gs_live_colortrans); - - add_entry_to_menu_page(menu_page_data, "Loading GS Rendering ...", gs_rendering, reload_switch_value); - add_entry_to_menu_page(menu_page_data, "Loading Connector ...", connector, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Resolution ...", resolution, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Video Scale ...", video_scale, reload_slider_value); - add_entry_to_menu_page(menu_page_data, "Loading Colortrans ...", gs_live_colortrans, reload_live_colortrans_fn); - - lv_group_set_default(default_group); -} - -void create_gs_system_dvr_menu(lv_obj_t * parent) { - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "system"); - menu_page_data->page_load_callback = generic_page_load_callback; - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "DVR", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - rec_enabled = create_switch(cont, LV_SYMBOL_SETTINGS, "Enabled", "rec_enabled", menu_page_data, true); - lv_obj_add_event_cb(lv_obj_get_child_by_type(rec_enabled, 0, &lv_switch_class), rec_enabled_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(rec_enabled); - - dvr_mode_dd = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Mode", "", "dvr_mode", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_mode_dd, 0, &lv_dropdown_class), dvr_mode_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_mode_dd); - - dvr_max_size = create_slider(cont, LV_SYMBOL_SETTINGS, "Max file size (MB)", "dvr_max_size", menu_page_data, false, 0); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_max_size, 0, &lv_slider_class), dvr_max_size_label_cb, LV_EVENT_VALUE_CHANGED, NULL); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_max_size, 0, &lv_slider_class), dvr_max_size_cb, LV_EVENT_CLICKED, NULL); - use_sub_back_handler(dvr_max_size); - - rec_fps = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Raw FPS", "", "rec_fps", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(rec_fps, 0, &lv_dropdown_class), rec_fps_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(rec_fps); - - dvr_reenc_codec = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Codec", "", "dvr_reenc_codec", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_reenc_codec, 0, &lv_dropdown_class), dvr_reenc_codec_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_reenc_codec); - dvr_reenc_resolution = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Resolution", "", "dvr_reenc_resolution", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_reenc_resolution, 0, &lv_dropdown_class), dvr_reenc_resolution_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_reenc_resolution); - dvr_reenc_fps = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Re-encode FPS", "", "dvr_reenc_fps", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_reenc_fps, 0, &lv_dropdown_class), dvr_reenc_fps_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_reenc_fps); - dvr_reenc_bitrate = create_dropdown(cont, LV_SYMBOL_SETTINGS, "Bitrate (kbps)", "", "dvr_reenc_bitrate", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_reenc_bitrate, 0, &lv_dropdown_class), dvr_reenc_bitrate_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_reenc_bitrate); - dvr_reenc_osd = create_switch(cont, LV_SYMBOL_SETTINGS, "Record OSD in DVR", "dvr_osd", menu_page_data, false); - lv_obj_add_event_cb(lv_obj_get_child_by_type(dvr_reenc_osd, 0, &lv_switch_class), dvr_reenc_osd_cb, LV_EVENT_VALUE_CHANGED, NULL); - use_sub_back_handler(dvr_reenc_osd); - - // dvr_mode_fn must be last: it calls update_dvr_mode_visibility() after all - // DVR widgets have been loaded so none are skipped due to hidden state. - add_entry_to_menu_page(menu_page_data, "Loading DVR enabled ...", rec_enabled, reload_rec_enabled_fn); - add_entry_to_menu_page(menu_page_data, "Loading DVR max size ...", dvr_max_size, reload_dvr_max_size_fn); - add_entry_to_menu_page(menu_page_data, "Loading Raw FPS ...", rec_fps, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Re-enc Codec ...", dvr_reenc_codec, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Re-enc Res ...", dvr_reenc_resolution, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Re-enc FPS ...", dvr_reenc_fps, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading Re-enc Bitrate ...", dvr_reenc_bitrate, reload_dropdown_value); - add_entry_to_menu_page(menu_page_data, "Loading DVR OSD ...", dvr_reenc_osd, reload_dvr_reenc_osd_fn); - add_entry_to_menu_page(menu_page_data, "Loading DVR Mode ...", dvr_mode_dd, reload_dvr_mode_fn); - - lv_group_set_default(default_group); -} - -void create_gs_system_menu(lv_obj_t * parent, lv_obj_t * receiver_page, lv_obj_t * display_page, lv_obj_t * dvr_page) { - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "system"); - menu_page_data->page_load_callback = NULL; - menu_page_data->entry_count = 0; - menu_page_data->page_entries = NULL; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - - lv_obj_t * section; - - create_text(parent, NULL, "System Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - lv_obj_t * receiver_item = create_text(section, LV_SYMBOL_WIFI, "Receiver", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, receiver_item); - lv_menu_set_load_page_event(menu, receiver_item, receiver_page); - lv_obj_add_event_cb(receiver_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * display_item = create_text(section, LV_SYMBOL_IMAGE, "Display", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, display_item); - lv_menu_set_load_page_event(menu, display_item, display_page); - lv_obj_add_event_cb(display_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_obj_t * dvr_item = create_text(section, LV_SYMBOL_VIDEO, "DVR", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(menu_page_data->indev_group, dvr_item); - lv_menu_set_load_page_event(menu, dvr_item, dvr_page); - lv_obj_add_event_cb(dvr_item, generic_back_event_handler, LV_EVENT_KEY, NULL); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/gs_system.h b/src/gsmenu/gs_system.h deleted file mode 100644 index 4fadd6f5..00000000 --- a/src/gsmenu/gs_system.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "lvgl/lvgl.h" - -enum RXMode { - WFB, - APFPV -}; - -void toggle_rec_enabled(void); -void create_gs_system_receiver_menu(lv_obj_t * parent); -void create_gs_system_display_menu(lv_obj_t * parent); -void create_gs_system_dvr_menu(lv_obj_t * parent); -void create_gs_system_menu(lv_obj_t * parent, lv_obj_t * receiver_page, lv_obj_t * display_page, lv_obj_t * dvr_page); diff --git a/src/gsmenu/gs_wfbng.c b/src/gsmenu/gs_wfbng.c deleted file mode 100644 index b4a1f090..00000000 --- a/src/gsmenu/gs_wfbng.c +++ /dev/null @@ -1,69 +0,0 @@ -#include -#include -#include -#include "gs_wfbng.h" -#include "air_wfbng.h" -#include "lvgl/lvgl.h" -#include "helper.h" -#include "executor.h" -#include "styles.h" - -extern lv_group_t * default_group; - -lv_obj_t * gs_channel; -lv_obj_t * gs_search; -lv_obj_t * bandwidth; -lv_obj_t * gs_txpower; -lv_obj_t * adaptivelink; - - -void gs_wfbng_page_load_callback(lv_obj_t * page) -{ - reload_dropdown_value(page,gs_channel); - reload_dropdown_value(page,bandwidth); - reload_slider_value(page,gs_txpower); - reload_switch_value(page,adaptivelink); -} - -void gs_wfbng_search_callback(lv_event_t * event) -{ - run_command_and_block(event,"gsmenu.sh search channel",NULL); -} - - -void create_gs_wfbng_menu(lv_obj_t * parent) { - - menu_page_data_t* menu_page_data = malloc(sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "wfbng"); - menu_page_data->page_load_callback = gs_wfbng_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent,menu_page_data); - - - lv_obj_t * cont; - lv_obj_t * section; - - create_text(parent, NULL, "WFB-NG", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - gs_channel = create_dropdown(cont,LV_SYMBOL_SETTINGS, "Channel", "","gs_channel",menu_page_data,false); - gs_search = create_button(cont, "Search"); - lv_obj_add_event_cb(lv_obj_get_child_by_type(gs_search,0,&lv_button_class),gs_wfbng_search_callback,LV_EVENT_CLICKED,menu_page_data); - bandwidth = create_dropdown(cont,LV_SYMBOL_SETTINGS, "bandwidth", "","bandwidth",menu_page_data,false); - gs_txpower = create_slider(cont,LV_SYMBOL_SETTINGS,"TXPower (%)","txpower",menu_page_data,false,0); - - create_text(parent, NULL, "Adaptive Link", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - adaptivelink = create_switch(cont,LV_SYMBOL_SETTINGS,"Enabled","adaptivelink", menu_page_data,false); - - lv_group_set_default(default_group); -} \ No newline at end of file diff --git a/src/gsmenu/gs_wfbng.h b/src/gsmenu/gs_wfbng.h deleted file mode 100644 index 762566b6..00000000 --- a/src/gsmenu/gs_wfbng.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once -#include "lvgl/lvgl.h" - -void create_gs_wfbng_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/gs_wifi.c b/src/gsmenu/gs_wifi.c deleted file mode 100644 index 4536a3e0..00000000 --- a/src/gsmenu/gs_wifi.c +++ /dev/null @@ -1,699 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include "ui.h" -#include "../input.h" -#include "../gstrtpreceiver.h" -#include "helper.h" -#include "styles.h" -#include "executor.h" -#include "gs_wifi.h" - -/* ── Network data ────────────────────────────────────────────────────────── */ - -#define MAX_NETWORKS 64 -#define MAX_SSID_LEN 64 - -typedef struct { - char ssid[MAX_SSID_LEN]; - char security[64]; /* empty string = open network */ - int signal; - char saved_password[128]; /* non-empty = remembered password */ -} wifi_network_t; - -static wifi_network_t networks[MAX_NETWORKS]; -static int network_count = 0; -static int selected_network_idx = -1; - -/* ── Async scan state ────────────────────────────────────────────────────── */ - -typedef struct { - char * output; - volatile bool complete; - bool running; - pthread_t thread; - lv_obj_t * spinner; -} scan_state_t; - -static scan_state_t scan_state = {0}; - -/* ── External references ─────────────────────────────────────────────────── */ - -extern lv_obj_t * menu; -extern gsmenu_control_mode_t control_mode; -extern lv_group_t * default_group; -extern lv_indev_t * indev_drv; - -/* ── Widget references ───────────────────────────────────────────────────── */ - -static lv_obj_t * hotspot; -static lv_obj_t * net_list_cont; /* dynamic list container */ -static lv_obj_t * pwd_row; /* password row, hidden by default */ -static lv_obj_t * pwd_ta; /* password textarea */ -static lv_obj_t * wifi_kb; /* on-screen keyboard */ -static lv_obj_t * status_lbl; /* connection status */ -static lv_obj_t * ipinfo; -static lv_obj_t * ipinfo_label_ref; /* cached ref for group reordering */ -static lv_obj_t * hotspot_sw_ref; /* cached ref for group reordering */ -static lv_obj_t * restream_sw_ref; /* cached ref for group reordering */ -static lv_obj_t * restream; -static lv_obj_t * ip_dropdown; -static menu_page_data_t* wifi_mpd; - -/* ── Forward declarations ────────────────────────────────────────────────── */ - -static void parse_scan_output(char * output); -static void load_saved_passwords(void); -static void rebuild_network_list(void); -static void update_status(void); -static void scan_btn_cb(lv_event_t * e); -static void connect_network_btn_cb(lv_event_t * e); -static void disconnect_btn_cb(lv_event_t * e); -static void pwd_kb_cb(lv_event_t * e); -static void after_connect(void); -static void after_disconnect(void); -static void hotspot_switch_status_cb(lv_event_t * e); -static void scan_complete_timer_cb(lv_timer_t * t); - -/* ── Restream helpers ─────────────────────────────────────────────────── */ - -static uint16_t find_dropdown_option_index(const char * options, const char * value) -{ - if (!value || value[0] == '\0') return 0; - uint16_t idx = 0; - const char * option = options; - while (option && *option) { - const char * nl = strchr(option, '\n'); - size_t len = nl ? (size_t)(nl - option) : strlen(option); - if (strlen(value) == len && strncmp(option, value, len) == 0) return idx; - idx++; - option = nl ? nl + 1 : NULL; - } - return 0; -} - -static void ip_dropdown_cb(lv_event_t * e) { - if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) { - lv_obj_t * dd = lv_event_get_target(e); - char buf[64]; - lv_dropdown_get_selected_str(dd, buf, sizeof(buf)); -#ifndef USE_SIMULATOR - restream_set_manual_ip(buf); -#endif - } -} - -static void restream_switch_callback(lv_event_t * e) { - if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) { - lv_obj_t * target = lv_event_get_target(e); -#ifndef USE_SIMULATOR - restream_set_enabled(lv_obj_has_state(target, LV_STATE_CHECKED)); -#endif - } -} - -/* ── Parse iw scan output ────────────────────────────────────────────────── */ -/* - * Each line from: gsmenu.sh get gs wifi networks (iw dev wlan0 scan | awk) - * ESCAPED_SSID:SECURITY:SIGNAL_PCT - * where literal ':' inside SSID is escaped as '\:'. - * SECURITY is "--" for open networks, "WPA" for secured. - */ -static void parse_scan_output(char * output) { - network_count = 0; - if (!output || output[0] == '\0') return; - - char * saveptr = NULL; - char * line = strtok_r(output, "\n", &saveptr); - - while (line && network_count < MAX_NETWORKS) { - if (line[0] == '\0') { line = strtok_r(NULL, "\n", &saveptr); continue; } - - int len = (int)strlen(line); - - /* Find the last two unescaped colons (not preceded by backslash) */ - int last_colon = -1, second_last_colon = -1; - for (int i = 0; i < len; i++) { - if (line[i] == ':' && (i == 0 || line[i - 1] != '\\')) { - second_last_colon = last_colon; - last_colon = i; - } - } - if (last_colon < 0 || second_last_colon < 0) { - line = strtok_r(NULL, "\n", &saveptr); - continue; - } - - char * signal_str = line + last_colon + 1; - line[last_colon] = '\0'; - char * security = line + second_last_colon + 1; - line[second_last_colon] = '\0'; - char * ssid_raw = line; - - /* Unescape \: → : in-place */ - int si = 0, di = 0; - while (ssid_raw[si]) { - if (ssid_raw[si] == '\\' && ssid_raw[si + 1] == ':') { - ssid_raw[di++] = ':'; - si += 2; - } else { - ssid_raw[di++] = ssid_raw[si++]; - } - } - ssid_raw[di] = '\0'; - - if (ssid_raw[0] == '\0') { line = strtok_r(NULL, "\n", &saveptr); continue; } - - /* Deduplicate by SSID (keep first = strongest, nmcli sorts by signal) */ - bool seen = false; - for (int j = 0; j < network_count; j++) { - if (strcmp(networks[j].ssid, ssid_raw) == 0) { seen = true; break; } - } - if (seen) { line = strtok_r(NULL, "\n", &saveptr); continue; } - - strncpy(networks[network_count].ssid, ssid_raw, MAX_SSID_LEN - 1); - networks[network_count].ssid[MAX_SSID_LEN - 1] = '\0'; - strncpy(networks[network_count].security, security, - sizeof(networks[0].security) - 1); - networks[network_count].security[sizeof(networks[0].security) - 1] = '\0'; - networks[network_count].signal = atoi(signal_str); - network_count++; - - line = strtok_r(NULL, "\n", &saveptr); - } -} - -/* ── Async scan ──────────────────────────────────────────────────────────── */ - -static void * scan_thread_fn(void * arg) { - (void)arg; - scan_state.output = run_command("gsmenu.sh get gs wifi networks"); - scan_state.complete = true; - return NULL; -} - -static void scan_complete_timer_cb(lv_timer_t * t) { - if (!scan_state.complete) return; - - pthread_join(scan_state.thread, NULL); - - if (scan_state.spinner && lv_obj_is_valid(scan_state.spinner)) { - lv_obj_del(scan_state.spinner); - scan_state.spinner = NULL; - } - lv_timer_del(t); - scan_state.running = false; - - parse_scan_output(scan_state.output); - free(scan_state.output); - scan_state.output = NULL; - scan_state.complete = false; - - rebuild_network_list(); - - /* Restore input group */ - if (wifi_mpd) lv_indev_set_group(indev_drv, wifi_mpd->indev_group); -} - -/* ── Saved password store ───────────────────────────────────────────────── */ - -static void load_saved_passwords(void) { - int i; - for (i = 0; i < network_count; i++) - networks[i].saved_password[0] = '\0'; - - char * output = run_command("gsmenu.sh get gs wifi savednetworks"); - if (!output || output[0] == '\0') { free(output); return; } - - char * saveptr = NULL; - char * line = strtok_r(output, "\n", &saveptr); - while (line) { - if (line[0] == '\0') { line = strtok_r(NULL, "\n", &saveptr); continue; } - int len = (int)strlen(line); - /* Find first unescaped colon — separator between SSID and password */ - int colon = -1; - for (int j = 0; j < len; j++) { - if (line[j] == ':' && (j == 0 || line[j-1] != '\\')) { colon = j; break; } - } - if (colon < 0) { line = strtok_r(NULL, "\n", &saveptr); continue; } - char * password = line + colon + 1; - line[colon] = '\0'; - /* Unescape \: -> : in SSID */ - int si = 0, di = 0; - while (line[si]) { - if (line[si] == '\\' && line[si+1] == ':') { line[di++] = ':'; si += 2; } - else { line[di++] = line[si++]; } - } - line[di] = '\0'; - /* Match to scanned networks */ - for (i = 0; i < network_count; i++) { - if (strcmp(networks[i].ssid, line) == 0) { - strncpy(networks[i].saved_password, password, - sizeof(networks[i].saved_password) - 1); - networks[i].saved_password[sizeof(networks[i].saved_password) - 1] = '\0'; - break; - } - } - line = strtok_r(NULL, "\n", &saveptr); - } - free(output); -} - -/* ── Network list ────────────────────────────────────────────────────────── */ - -static void rebuild_network_list(void) { - load_saved_passwords(); - /* Remove all below-list objects from group so network buttons are inserted - immediately after scan/disconnect in the correct nav order. */ - if (hotspot_sw_ref && lv_obj_is_valid(hotspot_sw_ref)) - lv_group_remove_obj(hotspot_sw_ref); - if (restream_sw_ref && lv_obj_is_valid(restream_sw_ref)) - lv_group_remove_obj(restream_sw_ref); - lv_group_remove_obj(ip_dropdown); - if (ipinfo_label_ref && lv_obj_is_valid(ipinfo_label_ref)) - lv_group_remove_obj(ipinfo_label_ref); - - /* lv_obj_clean auto-removes any deleted Connect buttons from all groups */ - lv_obj_clean(net_list_cont); - - if (network_count == 0) { - lv_obj_t * lbl = lv_label_create(net_list_cont); - lv_label_set_text(lbl, "No networks found"); - } else { - /* Fetch currently connected SSID to mark it. - * Skip when hotspot is active — the interface is in AP mode. */ - char connected_ssid[MAX_SSID_LEN] = ""; - bool hotspot_on = (hotspot_sw_ref && lv_obj_is_valid(hotspot_sw_ref) && - lv_obj_has_state(hotspot_sw_ref, LV_STATE_CHECKED)); - if (!hotspot_on) { - char * conn = run_command("gsmenu.sh get gs wifi ssid"); - if (conn) { - size_t len = strlen(conn); - while (len > 0 && (conn[len-1] == '\n' || conn[len-1] == '\r')) - conn[--len] = '\0'; - if (len > 0 && len < MAX_SSID_LEN) - strncpy(connected_ssid, conn, MAX_SSID_LEN - 1); - free(conn); - } - } - - for (int i = 0; i < network_count; i++) { - bool is_connected = (connected_ssid[0] != '\0' && - strcmp(networks[i].ssid, connected_ssid) == 0); - bool is_open = (networks[i].security[0] == '\0' || - strcmp(networks[i].security, "--") == 0); - - /* One full-width button per network — no nested rows, no nav issues */ - lv_obj_t * btn = lv_btn_create(net_list_cont); - lv_obj_set_width(btn, LV_PCT(100)); - lv_obj_set_height(btn, LV_SIZE_CONTENT); - lv_obj_set_layout(btn, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_START, - LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(btn, 6, 0); - lv_obj_set_style_pad_column(btn, 8, 0); - - if (is_connected) { - lv_obj_add_style(btn, &style_openipc_dark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_add_style(btn, &style_openipc, - LV_PART_MAIN | LV_STATE_DEFAULT); - } - lv_obj_add_style(btn, &style_openipc_outline, - LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_set_user_data(btn, (void *)(intptr_t)i); - lv_obj_add_event_cb(btn, connect_network_btn_cb, - LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(btn, generic_back_event_handler, - LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(btn, on_focus, LV_EVENT_FOCUSED, NULL); - lv_group_add_obj(wifi_mpd->indev_group, btn); - - lv_obj_t * icon = lv_label_create(btn); - bool has_saved = (networks[i].saved_password[0] != '\0'); - lv_label_set_text(icon, is_open ? LV_SYMBOL_WIFI : - (has_saved ? LV_SYMBOL_OK : LV_SYMBOL_WARNING)); - - lv_obj_t * name = lv_label_create(btn); - char name_buf[MAX_SSID_LEN + 16]; - snprintf(name_buf, sizeof(name_buf), "%s (%d%%)", - networks[i].ssid, networks[i].signal); - lv_label_set_text(name, name_buf); - lv_obj_set_flex_grow(name, 1); - - lv_obj_t * state_lbl = lv_label_create(btn); - lv_label_set_text(state_lbl, - is_connected ? (LV_SYMBOL_OK " Connected") : ">"); - } - } - - /* Restore in correct nav order: hotspot, restream, ip_dropdown, ipinfo */ - if (hotspot_sw_ref && lv_obj_is_valid(hotspot_sw_ref)) - lv_group_add_obj(wifi_mpd->indev_group, hotspot_sw_ref); - if (restream_sw_ref && lv_obj_is_valid(restream_sw_ref)) - lv_group_add_obj(wifi_mpd->indev_group, restream_sw_ref); - lv_group_add_obj(wifi_mpd->indev_group, ip_dropdown); - if (ipinfo_label_ref && lv_obj_is_valid(ipinfo_label_ref)) - lv_group_add_obj(wifi_mpd->indev_group, ipinfo_label_ref); -} - -/* ── Status ──────────────────────────────────────────────────────────────── */ - -static void update_status(void) { - /* When hotspot is active the interface is in AP mode — not a client. - * Read from the already-loaded switch widget to avoid a redundant - * gsmenu.sh call on page load (reload_switch_value runs first). */ - if (hotspot_sw_ref && lv_obj_is_valid(hotspot_sw_ref) && - lv_obj_has_state(hotspot_sw_ref, LV_STATE_CHECKED)) { - lv_label_set_text(status_lbl, LV_SYMBOL_WIFI " Hotspot active"); - return; - } - char * conn = run_command("gsmenu.sh get gs wifi ssid"); - if (conn) { - size_t len = strlen(conn); - if (len > 0 && conn[len - 1] == '\n') conn[len - 1] = '\0'; - if (conn[0] != '\0') { - char buf[256]; - snprintf(buf, sizeof(buf), LV_SYMBOL_WIFI " Connected: %s", conn); - lv_label_set_text(status_lbl, buf); - } else { - lv_label_set_text(status_lbl, "Not connected"); - } - free(conn); - } else { - lv_label_set_text(status_lbl, "Not connected"); - } -} - -/* ── After-action callbacks ──────────────────────────────────────────────── */ - -static void after_connect(void) { - lv_obj_add_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - lv_textarea_set_text(pwd_ta, ""); - /* Connecting to WiFi means hotspot is now off — update switch visually */ - if (hotspot_sw_ref && lv_obj_is_valid(hotspot_sw_ref)) - lv_obj_clear_state(hotspot_sw_ref, LV_STATE_CHECKED); - update_status(); - if (network_count > 0) - rebuild_network_list(); /* refresh key icons after saving new password */ -} - -static void after_disconnect(void) { - update_status(); -} - -static void hotspot_switch_status_cb(lv_event_t * e) { - if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; - lv_obj_t * sw = lv_event_get_target(e); - if (lv_obj_has_state(sw, LV_STATE_CHECKED)) { - lv_label_set_text(status_lbl, LV_SYMBOL_WIFI " Hotspot active"); - } else { - lv_label_set_text(status_lbl, "Not connected"); - } - /* Refresh the network list so no network remains marked "Connected" - * after hotspot is toggled on, and vice versa. */ - if (network_count > 0) - rebuild_network_list(); -} - -/* ── Page load callback ──────────────────────────────────────────────────── */ - -void wifi_page_load_callback(lv_obj_t * page) -{ - reload_switch_value(page, hotspot); - update_status(); - reload_label_value(page, ipinfo); - - lv_obj_add_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - - if (restream_get_enabled()) - lv_obj_add_state(lv_obj_get_child_by_type(restream, 0, &lv_switch_class), - LV_STATE_CHECKED); - else - lv_obj_clear_state(lv_obj_get_child_by_type(restream, 0, &lv_switch_class), - LV_STATE_CHECKED); - { - char clients[512]; - restream_scan_clients(clients, sizeof(clients)); - lv_dropdown_set_options(ip_dropdown, clients); - lv_dropdown_set_selected(ip_dropdown, - find_dropdown_option_index(clients, restream_get_manual_ip())); - } -} - -/* ── Event handlers ──────────────────────────────────────────────────────── */ - -static void scan_btn_cb(lv_event_t * e) { - if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; - if (scan_state.running) return; - - scan_state.running = true; - scan_state.complete = false; - - lv_indev_set_group(indev_drv, default_group); - - scan_state.spinner = lv_spinner_create(lv_layer_top()); - lv_obj_add_style(scan_state.spinner, &style_openipc, - LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_center(scan_state.spinner); - - pthread_create(&scan_state.thread, NULL, scan_thread_fn, NULL); - lv_timer_create(scan_complete_timer_cb, 30, NULL); -} - -static void connect_network_btn_cb(lv_event_t * e) { - if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; - lv_obj_t * btn = lv_event_get_target(e); - int idx = (int)(intptr_t)lv_obj_get_user_data(btn); - if (idx < 0 || idx >= network_count) return; - - selected_network_idx = idx; - - bool is_open = (networks[idx].security[0] == '\0' - || strcmp(networks[idx].security, "--") == 0); - bool has_saved = (networks[idx].saved_password[0] != '\0'); - - if (is_open || has_saved) { - char cmd[512]; - if (is_open) { - snprintf(cmd, sizeof(cmd), - "gsmenu.sh set gs wifi connect \"%s\"", networks[idx].ssid); - } else { - snprintf(cmd, sizeof(cmd), - "gsmenu.sh set gs wifi connect \"%s\" \"%s\"", - networks[idx].ssid, networks[idx].saved_password); - } - run_command_and_block(e, cmd, after_connect); - } else { - lv_obj_clear_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - lv_textarea_set_text(pwd_ta, ""); - lv_keyboard_set_textarea(wifi_kb, pwd_ta); - lv_obj_scroll_to_view_recursive(pwd_row, LV_ANIM_OFF); - control_mode = GSMENU_CONTROL_MODE_KEYBOARD; - lv_group_focus_obj(wifi_kb); - } -} - -static void disconnect_btn_cb(lv_event_t * e) { - if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; - run_command_and_block(e, "gsmenu.sh set gs wifi disconnect", after_disconnect); -} - -static void pwd_kb_cb(lv_event_t * e) { - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_FOCUSED) { - control_mode = GSMENU_CONTROL_MODE_KEYBOARD; - } else if (code == LV_EVENT_DEFOCUSED) { - control_mode = GSMENU_CONTROL_MODE_NAV; - } else if (code == LV_EVENT_READY) { - if (selected_network_idx >= 0 && selected_network_idx < network_count) { - const char * pwd = lv_textarea_get_text(pwd_ta); - char cmd[512]; - snprintf(cmd, sizeof(cmd), - "gsmenu.sh set gs wifi connect \"%s\" \"%s\"", - networks[selected_network_idx].ssid, pwd); - lv_obj_add_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - control_mode = GSMENU_CONTROL_MODE_NAV; - run_command_and_block(e, cmd, after_connect); - } - } else if (code == LV_EVENT_CANCEL) { - lv_obj_add_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - lv_textarea_set_text(pwd_ta, ""); - control_mode = GSMENU_CONTROL_MODE_NAV; - lv_indev_reset(NULL, lv_event_get_target(e)); - } -} - -/* ── Create menu ─────────────────────────────────────────────────────────── */ - -void create_wifi_menu(lv_obj_t * parent) { - - menu_page_data_t * menu_page_data = malloc(sizeof(menu_page_data_t)); - memset(menu_page_data, 0, sizeof(menu_page_data_t)); - strcpy(menu_page_data->type, "gs"); - strcpy(menu_page_data->page, "wifi"); - menu_page_data->page_load_callback = wifi_page_load_callback; - menu_page_data->indev_group = lv_group_create(); - lv_group_set_default(menu_page_data->indev_group); - lv_obj_set_user_data(parent, menu_page_data); - wifi_mpd = menu_page_data; - - /* ── WiFi Connection ─────────────────────────────────────────────────── */ - - create_text(parent, NULL, "WiFi Connection", NULL, NULL, false, - LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_obj_t * section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - - lv_obj_t * cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - /* Top bar: [Scan] [Status] [Disconnect] */ - lv_obj_t * top_row = lv_menu_cont_create(cont); - - lv_obj_t * scan_btn = lv_btn_create(top_row); - lv_obj_t * scan_lbl = lv_label_create(scan_btn); - lv_label_set_text(scan_lbl, LV_SYMBOL_REFRESH " Scan"); - lv_obj_add_style(scan_btn, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_style(scan_btn, &style_openipc_outline, - LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_event_cb(scan_btn, scan_btn_cb, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(scan_btn, generic_back_event_handler, LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(scan_btn, on_focus, LV_EVENT_FOCUSED, NULL); - lv_group_add_obj(menu_page_data->indev_group, scan_btn); - - status_lbl = lv_label_create(top_row); - lv_label_set_text(status_lbl, "Not connected"); - lv_obj_set_flex_grow(status_lbl, 1); - - lv_obj_t * disc_btn = lv_btn_create(top_row); - lv_obj_t * disc_lbl = lv_label_create(disc_btn); - lv_label_set_text(disc_lbl, LV_SYMBOL_CLOSE " Disconnect"); - lv_obj_add_style(disc_btn, &style_openipc_dark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_style(disc_btn, &style_openipc_outline, - LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_event_cb(disc_btn, disconnect_btn_cb, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(disc_btn, generic_back_event_handler, LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(disc_btn, on_focus, LV_EVENT_FOCUSED, NULL); - lv_group_add_obj(menu_page_data->indev_group, disc_btn); - - /* Network list — populated dynamically after scan */ - net_list_cont = lv_menu_cont_create(cont); - lv_obj_set_flex_flow(net_list_cont, LV_FLEX_FLOW_COLUMN); - { - lv_obj_t * hint = lv_label_create(net_list_cont); - lv_label_set_text(hint, "Press Scan to search for networks"); - } - - /* Password row (hidden until a secured network is selected) */ - pwd_row = lv_menu_cont_create(cont); - lv_obj_add_flag(pwd_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_t * pwd_label = lv_label_create(pwd_row); - lv_label_set_text(pwd_label, LV_SYMBOL_EYE_CLOSE " Password"); - pwd_ta = lv_textarea_create(pwd_row); - lv_textarea_set_one_line(pwd_ta, true); - lv_textarea_set_password_mode(pwd_ta, false); - lv_textarea_set_placeholder_text(pwd_ta, "Enter password..."); - lv_obj_set_flex_grow(pwd_ta, 1); - lv_obj_add_style(pwd_ta, &style_openipc_dark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - - /* On-screen keyboard (hidden by default) */ - wifi_kb = lv_keyboard_create(lv_obj_get_parent(section)); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(wifi_kb, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_add_style(wifi_kb, &style_openipc_outline, - LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(wifi_kb, &style_openipc, - LV_PART_ITEMS | LV_STATE_FOCUS_KEY); - lv_obj_add_style(wifi_kb, &style_openipc_dark_background, - LV_PART_ITEMS | LV_STATE_DEFAULT); - lv_obj_add_style(wifi_kb, &style_openipc_textcolor, - LV_PART_ITEMS | LV_STATE_FOCUS_KEY); - lv_obj_add_style(wifi_kb, &style_openipc_lightdark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(wifi_kb, pwd_kb_cb, LV_EVENT_ALL, NULL); - lv_keyboard_set_textarea(wifi_kb, NULL); - - /* ── Hotspot ─────────────────────────────────────────────────────────── */ - - create_text(parent, NULL, "Hotspot", NULL, NULL, false, - LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - hotspot = create_switch(cont, NULL, "Hotspot", "hotspot", menu_page_data, false); - hotspot_sw_ref = lv_obj_get_child_by_type(hotspot, 0, &lv_switch_class); - lv_obj_add_event_cb(hotspot_sw_ref, hotspot_switch_status_cb, LV_EVENT_VALUE_CHANGED, NULL); - - /* ── Phone Restream ──────────────────────────────────────────────────── */ - - create_text(parent, NULL, "Phone Restream", NULL, NULL, false, - LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - restream = create_switch(cont, LV_SYMBOL_VIDEO, "Phone Restream", NULL, NULL, false); - restream_sw_ref = lv_obj_get_child_by_type(restream, 0, &lv_switch_class); - lv_obj_add_event_cb(lv_obj_get_child_by_type(restream, 0, &lv_switch_class), - restream_switch_callback, LV_EVENT_VALUE_CHANGED, NULL); - - lv_obj_t * ip_dropdown_row = lv_menu_cont_create(cont); - lv_obj_t * dd_icon = lv_image_create(ip_dropdown_row); - lv_image_set_src(dd_icon, LV_SYMBOL_WIFI); - lv_obj_t * ip_dd_label = lv_label_create(ip_dropdown_row); - lv_label_set_text(ip_dd_label, "Stream To"); - lv_obj_set_flex_grow(ip_dd_label, 1); - ip_dropdown = lv_dropdown_create(ip_dropdown_row); - lv_dropdown_set_options(ip_dropdown, "Auto"); - lv_dropdown_set_dir(ip_dropdown, LV_DIR_RIGHT); - lv_dropdown_set_symbol(ip_dropdown, LV_SYMBOL_RIGHT); - lv_obj_set_width(ip_dropdown, 200); - lv_obj_add_style(ip_dropdown, &style_openipc_outline, - LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(ip_dropdown, &style_openipc_dark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_t * ip_dd_list = lv_dropdown_get_list(ip_dropdown); - lv_obj_add_style(ip_dd_list, &style_openipc, - LV_PART_SELECTED | LV_STATE_CHECKED); - lv_obj_add_style(ip_dd_list, &style_openipc_dark_background, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(ip_dropdown, dropdown_event_handler, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ip_dropdown, ip_dropdown_cb, LV_EVENT_VALUE_CHANGED, NULL); - lv_obj_add_event_cb(ip_dropdown, generic_back_event_handler, LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(ip_dropdown, on_focus, LV_EVENT_FOCUSED, NULL); - lv_group_add_obj(menu_page_data->indev_group, ip_dropdown); - - /* ── Network info ────────────────────────────────────────────────────── */ - - create_text(parent, NULL, "Network", NULL, NULL, false, - LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(parent); - lv_obj_add_style(section, &style_openipc_section, 0); - cont = lv_menu_cont_create(section); - lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); - - ipinfo = create_text(cont, LV_SYMBOL_SETTINGS, "Network", "IP", - menu_page_data, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_obj_t * ipinfo_label = lv_obj_get_child_by_type(ipinfo, 0, &lv_label_class); - ipinfo_label_ref = ipinfo_label; /* cache for group reordering in rebuild_network_list */ - lv_group_add_obj(menu_page_data->indev_group, ipinfo_label); - - lv_group_set_default(default_group); -} diff --git a/src/gsmenu/gs_wifi.h b/src/gsmenu/gs_wifi.h deleted file mode 100644 index 0de15a47..00000000 --- a/src/gsmenu/gs_wifi.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void create_wifi_menu(lv_obj_t * parent); \ No newline at end of file diff --git a/src/gsmenu/helper.c b/src/gsmenu/helper.c index f255a239..9f314efa 100644 --- a/src/gsmenu/helper.c +++ b/src/gsmenu/helper.c @@ -1,875 +1,15 @@ #include -#include +#include #include -#include #include "../../lvgl/lvgl.h" -#include "gs_system.h" -#include "../input.h" +#include "../menu.h" /* enum RXMode, RXMODE */ #include "helper.h" #include "styles.h" -#include "ui.h" -#include "executor.h" #include "../WiFiRSSIMonitor.h" -#include "../menu.h" -extern enum RXMode RXMODE; - -static void update_dropdown_width(lv_obj_t * dropdown); - -extern gsmenu_control_mode_t control_mode; -extern lv_obj_t * menu; extern lv_indev_t * indev_drv; -extern lv_obj_t * sub_gs_main_page; - -extern lv_obj_t * air_wfbng_cont; -extern lv_obj_t * air_alink_cont; -extern lv_obj_t * air_aalink_cont; -extern lv_obj_t * air_camera_cont; -extern lv_obj_t * air_telemetry_cont; -extern lv_obj_t * air_actions_cont; -extern lv_obj_t * gs_dvr_cont; -extern lv_obj_t * gs_wfbng_cont; -extern lv_obj_t * gs_apfpv_cont; -extern lv_obj_t * gs_system_cont; -extern lv_obj_t * gs_wlan_cont; -extern lv_obj_t * gs_actions_cont; -extern lv_group_t *main_group; -extern lv_group_t * error_group; -extern lv_obj_t * size; // air camera size setting wfb-ng only -extern lv_obj_t * fps; // air camera fps setting wfb-ng only -extern lv_obj_t * bitrate; // air camera bitrate setting wfb-ng only -extern lv_obj_t * video_mode; // air camera video_mode setting apfpv only -extern lv_obj_t * router; -extern lv_obj_t * osd_fps; -extern lv_obj_t * air_gs_rendering; -extern lv_obj_t * air_telemetry_msposd_text; -extern lv_obj_t * air_telemetry_msposd_section; - - -extern lv_obj_t * msgbox; - -lv_group_t *loader_group; -pthread_t loader_thread; -lv_obj_t *loader_msgbox = NULL; - -void on_focus(lv_event_t* e) -{ - lv_obj_t* obj = lv_event_get_current_target(e); - lv_obj_scroll_to_view_recursive(lv_obj_get_parent(obj), LV_ANIM_ON); -} - -void loader_cancel_button_cb(lv_event_t * e) { - printf("Cancelling loader thread\n"); - - // Cancel the loader thread - pthread_cancel(loader_thread); - - menu_page_data_t *menu_page_data = lv_event_get_user_data(e); - - // Close the message box - lv_lock(); - if (loader_msgbox) - lv_msgbox_close(loader_msgbox); - lv_unlock(); - - // Restore input device group - if (!error_group) { - lv_indev_set_group(indev_drv, menu_page_data->indev_group); - } else { - // Find the first focusable object recursively - lv_obj_t * first_obj = find_first_focusable_obj(msgbox); - lv_group_focus_obj(first_obj); - lv_indev_set_group(indev_drv, error_group); - } - - lv_group_del(loader_group); - loader_group = NULL; -} - -void* generic_page_load_thread(void *arg) { - // Set cancel type to deferred to allow cleanup - pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); - - lv_obj_t *page = (lv_obj_t*)arg; - menu_page_data_t *menu_page_data = lv_obj_get_user_data(page); - PageEntry *entries = menu_page_data->page_entries; - - lv_lock(); // Lock LVGL before any GUI operations - - // Create progress UI - loader_msgbox = lv_msgbox_create(NULL); - lv_obj_add_style(loader_msgbox, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *label = lv_label_create(loader_msgbox); - lv_obj_t *bar = lv_bar_create(loader_msgbox); - lv_obj_add_style(bar, &style_openipc, LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_add_style(bar, &style_openipc_dropdown, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_bar_set_range(bar, 0, menu_page_data->entry_count); - lv_obj_center(bar); - - lv_obj_t * cancel_button = lv_msgbox_add_footer_button(loader_msgbox, "Cancel"); - lv_group_add_obj(loader_group,cancel_button); - lv_obj_add_style(cancel_button, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_style(cancel_button, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_event_cb(cancel_button,loader_cancel_button_cb,LV_EVENT_CLICKED,menu_page_data); - lv_obj_center(cancel_button); - - // lv_obj_set_width(loader_msgbox, LV_SIZE_CONTENT); - // lv_obj_set_flex_grow(cancel_button, 1); - // lv_obj_align_to(bar, cancel_button, LV_ALIGN_OUT_BOTTOM_MID, 0, -50); - // lv_obj_set_height(loader_msgbox, LV_SIZE_CONTENT); - - - lv_unlock(); - - // Process entries with cancellation point - for (int i = 0; i < menu_page_data->entry_count; i++) { - // Add cancellation point - pthread_testcancel(); - - PageEntry *entry = &entries[i]; - if (entry->caption && entry->reload) { - lv_lock(); - lv_label_set_text(label, entry->caption); - lv_bar_set_value(bar, i, LV_ANIM_OFF); - lv_unlock(); - entry->reload(page, entry->target); - } - } - - lv_lock(); - lv_msgbox_close(loader_msgbox); - lv_unlock(); - - if (! error_group) - lv_indev_set_group(indev_drv,menu_page_data->indev_group); - else - lv_indev_set_group(indev_drv,error_group); - - lv_group_del(loader_group); - loader_group = NULL; - - return NULL; -} - -void generic_page_load_callback(lv_obj_t *page) { - - if (!loader_group) - loader_group = lv_group_create(); - lv_indev_set_group(indev_drv,loader_group); - pthread_create(&loader_thread, NULL, generic_page_load_thread, page); - pthread_detach(loader_thread); // Don't wait for thread to finish -} - -void generic_back_event_handler(lv_event_t * e) { - lv_key_t key = lv_event_get_key(e); - if (key == LV_KEY_HOME) { - lv_menu_set_page(menu,NULL); - lv_obj_remove_state(air_wfbng_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_alink_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_aalink_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_camera_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_telemetry_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_actions_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_dvr_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_wfbng_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_apfpv_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_system_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_wlan_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_actions_cont, LV_STATE_CHECKED); - lv_indev_set_group(indev_drv,main_group); - } -} - -lv_obj_t * create_text(lv_obj_t * parent, const char * icon, const char * txt, const char * parameter, menu_page_data_t* menu_page_data,bool blocking,lv_menu_builder_variant_t builder_variant) -{ - lv_obj_t * obj = lv_menu_cont_create(parent); - //lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t * img = NULL; - lv_obj_t * label = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - if(txt) { - label = lv_label_create(obj); - lv_label_set_text(label, txt); - // lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); - lv_obj_set_flex_grow(label, 1); - lv_obj_set_user_data(obj,(void *)txt); // ugly but where else to store - } - - if(builder_variant == LV_MENU_ITEM_BUILDER_VARIANT_2 && icon && txt) { - lv_obj_add_flag(img, LV_OBJ_FLAG_FLEX_IN_NEW_TRACK); - lv_obj_swap(img, label); - } - - lv_obj_add_style(obj, &style_openipc, LV_PART_MAIN | LV_STATE_CHECKED); - lv_obj_add_style(obj, &style_openipc, LV_PART_MAIN | LV_STATE_FOCUSED); - lv_obj_add_style(obj, &style_openipc, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(obj, &style_openipc_textcolor, LV_PART_MAIN | LV_STATE_CHECKED); - lv_obj_add_style(obj, &style_openipc_disabled, LV_PART_MAIN | LV_STATE_DISABLED ); - - if (menu_page_data) { - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = blocking; - strcpy(data->parameter, parameter); - - lv_obj_set_user_data(label,data); - } - - lv_obj_add_event_cb(label, on_focus, LV_EVENT_FOCUSED, NULL); - - return obj; -} - -static void slider_event_cb(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t * slider = lv_event_get_target(e); - lv_obj_t * slider_label = lv_event_get_user_data(e); - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(slider); - - switch (code) - { - case LV_EVENT_CLICKED: - { - if (control_mode == GSMENU_CONTROL_MODE_NAV) { - printf("GSMENU_CONTROL_MODE_SLIDER\n"); - lv_obj_add_style(slider, &style_openipc_outline, LV_PART_KNOB | LV_STATE_DEFAULT); - lv_obj_remove_style(slider, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - - // Free previous user data if it exists - int32_t *old_value = lv_obj_get_user_data(slider_label); - if (old_value) free(old_value); - lv_obj_set_user_data(slider_label,NULL); - - int32_t *start_value = malloc(sizeof(int32_t)); - *start_value = lv_slider_get_value(slider); - lv_obj_set_user_data(slider_label, start_value); - - control_mode = GSMENU_CONTROL_MODE_SLIDER; - } else { - lv_obj_remove_style(slider, &style_openipc_outline, LV_PART_KNOB | LV_STATE_DEFAULT); - lv_obj_add_style(slider, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - - control_mode = GSMENU_CONTROL_MODE_NAV; - } - break; - } - case LV_EVENT_CANCEL: - { - lv_obj_remove_style(slider, &style_openipc_outline, LV_PART_KNOB | LV_STATE_DEFAULT); - lv_obj_add_style(slider, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - - int32_t *start_value = lv_obj_get_user_data(slider_label); - if (start_value) { - if (*start_value != lv_slider_get_value(slider)) { - char buf[32]; - char format[16]; - snprintf(format, sizeof(format), "%%.%df", param_user_data->precision); - float value = *start_value / powf(10, param_user_data->precision); - snprintf(buf, sizeof(buf), format, value); - lv_label_set_text(slider_label, buf); - lv_slider_set_value(slider, *start_value, LV_ANIM_OFF); - } - free(start_value); - lv_obj_set_user_data(slider_label, NULL); - } - - control_mode = GSMENU_CONTROL_MODE_NAV; - break; - } - case LV_EVENT_VALUE_CHANGED: - { - char buf[32]; - char format[16]; - snprintf(format, sizeof(format), "%%.%df", param_user_data->precision); - float value = (float)lv_slider_get_value(slider) / powf(10, param_user_data->precision); - snprintf(buf, sizeof(buf), format, value); - lv_label_set_text(slider_label, buf); - break; - } - default: - break; - } -} - -lv_obj_t * create_slider(lv_obj_t * parent, const char * icon, const char * txt, const char * parameter, menu_page_data_t* menu_page_data, bool blocking, int precision) -{ - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * img = NULL; - lv_obj_t * label = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - if(txt) { - label = lv_label_create(obj); - lv_label_set_text(label, txt); - //lv_obj_set_flex_grow(label, 1); - } - - // Create a buffer for the float value display - char format[16]; - snprintf(format, sizeof(format), "%%.%df", precision); - - lv_obj_t * slider_label = lv_label_create(obj); - char s[32]; - snprintf(s, sizeof(s), format, 0.0f); // Initialize with 0.0 - lv_label_set_text(slider_label, s); - - lv_obj_t * slider = lv_slider_create(obj); - lv_obj_set_flex_grow(slider, 1); - lv_obj_add_style(slider, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_style(slider, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(slider, &style_openipc, LV_PART_KNOB | LV_STATE_DEFAULT); - lv_obj_add_style(slider, &style_openipc, LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_add_style(slider, &style_openipc, LV_STATE_FOCUS_KEY); - - lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_ALL, slider_label); - lv_obj_add_event_cb(slider, on_focus, LV_EVENT_FOCUSED, NULL); - - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = blocking; - data->precision = precision; - strcpy(data->parameter, parameter); - - lv_obj_set_user_data(slider,data); - - lv_obj_add_event_cb(slider, generic_slider_event_cb, LV_EVENT_CLICKED,data); - lv_obj_add_event_cb(slider, generic_back_event_handler, LV_EVENT_KEY,NULL); - - return obj; -} - -void generic_button_callback(lv_event_t * e) { - lv_obj_t * target = lv_event_get_target(e); - lv_obj_t * button = lv_obj_get_child_by_type(target,0,&lv_button_class); - lv_obj_t * button_label = lv_obj_get_child_by_type(target,0,&lv_label_class); - menu_page_data_t* menu_page_data = (menu_page_data_t*) lv_event_get_user_data(e); - char final_command[200] = "gsmenu.sh button "; - strcat(final_command,menu_page_data->type); - strcat(final_command," "); - strcat(final_command,menu_page_data->page); - strcat(final_command," \""); - strcat(final_command,lv_label_get_text(button_label)); - strcat(final_command,"\""); - run_command(final_command); -} - -lv_obj_t * create_button(lv_obj_t * parent, const char * txt) -{ - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * label = NULL; - - lv_obj_t * btn = lv_btn_create(obj); - lv_obj_add_style(btn, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); - - if(txt) { - label = lv_label_create(btn); - lv_label_set_text(label, txt); - } - - lv_obj_add_style(btn, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_event_cb(btn, generic_back_event_handler, LV_EVENT_KEY,NULL); - lv_obj_add_event_cb(btn, on_focus, LV_EVENT_FOCUSED, NULL); - - - return obj; -} - -static void lv_spinbox_increment_event_cb(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - if(code == LV_EVENT_SHORT_CLICKED || code == LV_EVENT_LONG_PRESSED_REPEAT) { - lv_spinbox_increment(lv_event_get_user_data(e)); - } -} - -static void lv_spinbox_decrement_event_cb(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - if(code == LV_EVENT_SHORT_CLICKED || code == LV_EVENT_LONG_PRESSED_REPEAT) { - lv_spinbox_decrement(lv_event_get_user_data(e)); - } -} - -lv_obj_t * create_spinbox(lv_obj_t * parent, const char * icon, const char * txt, int32_t min, int32_t max, - int32_t val) -{ - lv_obj_t * obj = lv_menu_cont_create(parent); - lv_obj_t * img = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - lv_obj_t * label = lv_label_create(obj); - lv_label_set_text(label, txt); - lv_label_set_text(label, txt); - - lv_obj_t * spinbox = lv_spinbox_create(obj); - lv_obj_add_state(spinbox, LV_STATE_DISABLED); - lv_spinbox_set_digit_format(spinbox, 2, 0); - lv_group_remove_obj(spinbox); - lv_spinbox_set_value(spinbox, val); - lv_spinbox_set_range(spinbox, min, max); - lv_obj_t * btn = lv_button_create(obj); - lv_obj_set_style_bg_image_src(btn, LV_SYMBOL_PLUS, 0); - lv_obj_add_event_cb(btn, lv_spinbox_increment_event_cb, LV_EVENT_ALL, spinbox); - - btn = lv_button_create(obj); - lv_obj_set_style_bg_image_src(btn, LV_SYMBOL_MINUS, 0); - lv_obj_add_event_cb(btn, lv_spinbox_decrement_event_cb, LV_EVENT_ALL, spinbox); - - - return obj; -} - -lv_obj_t * create_switch(lv_obj_t * parent, const char * icon, const char * txt,const char * parameter, menu_page_data_t* menu_page_data,bool blocking) -{ - lv_obj_t * obj = lv_menu_cont_create(parent); - lv_obj_t * img = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - lv_obj_t * label = lv_label_create(obj); - lv_label_set_text(label, txt); - - lv_obj_t * sw = lv_switch_create(obj); - lv_obj_add_state(sw, false); - lv_obj_add_style(sw, &style_openipc, LV_PART_INDICATOR | LV_STATE_CHECKED | LV_STATE_FOCUS_KEY); - lv_obj_add_style(sw, &style_openipc, LV_PART_INDICATOR | LV_STATE_CHECKED); - lv_obj_add_style(sw, &style_openipc_outline, LV_PART_MAIN | LV_STATE_CHECKED | LV_STATE_FOCUS_KEY); - lv_obj_add_style(sw, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(sw, &style_openipc_dark_background, LV_PART_KNOB | LV_STATE_DEFAULT); - lv_obj_add_style(sw, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - - if (menu_page_data) { - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = blocking; - strcpy(data->parameter, parameter); - - lv_obj_set_user_data(sw,data); - - lv_obj_add_event_cb(sw, generic_switch_event_cb, LV_EVENT_VALUE_CHANGED,data); - } - - lv_obj_add_event_cb(sw, generic_back_event_handler, LV_EVENT_KEY,NULL); - lv_obj_add_event_cb(sw, on_focus, LV_EVENT_FOCUSED, NULL); - - - return obj; -} - -void dropdown_event_handler(lv_event_t * e) -{ - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t * obj = lv_event_get_target(e); - switch (code) - { - case LV_EVENT_CANCEL: - case LV_EVENT_VALUE_CHANGED: { - control_mode = GSMENU_CONTROL_MODE_NAV; - break; - } - case LV_EVENT_READY: - control_mode = GSMENU_CONTROL_MODE_EDIT; - default: - break; - } -} - -lv_obj_t * create_dropdown(lv_obj_t * parent, const char * icon, const char * label_txt, const char * txt,const char * parameter, menu_page_data_t* menu_page_data,bool blocking) -{ - - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * img = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - lv_obj_t * label = lv_label_create(obj); - lv_label_set_text(label, label_txt); - - lv_obj_t * dd = lv_dropdown_create(obj); - lv_dropdown_set_dir(dd, LV_DIR_RIGHT); - lv_dropdown_set_symbol(dd, LV_SYMBOL_RIGHT); - lv_obj_set_width(dd,300); // someone got a better idea ? - - lv_obj_add_style(dd, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(dd, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_t * list = lv_dropdown_get_list(dd); - lv_obj_add_style(list, &style_openipc, LV_PART_SELECTED | LV_STATE_CHECKED); - lv_obj_add_style(list, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_add_event_cb(dd, dropdown_event_handler, LV_EVENT_ALL, NULL); - - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = blocking; - strcpy(data->parameter, parameter); - lv_obj_set_user_data(dd,data); - - lv_obj_add_event_cb(dd, generic_dropdown_event_cb, LV_EVENT_VALUE_CHANGED,data); - lv_obj_add_event_cb(dd, generic_back_event_handler, LV_EVENT_KEY,NULL); - lv_obj_add_event_cb(dd, on_focus, LV_EVENT_FOCUSED, NULL); - - lv_dropdown_set_options(dd,"Loading ..."); - - return obj; -} - - -lv_obj_t * create_checkbox(lv_obj_t * parent, const char * icon, const char * label_txt, const char * parameter, menu_page_data_t* menu_page_data,bool blocking) -{ - - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * img = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - lv_obj_t * cb; - cb = lv_checkbox_create(obj); - lv_checkbox_set_text(cb, label_txt); - - lv_obj_add_style(cb, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_set_style_border_color(cb, lv_color_hex(0xff4c60d8), LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(cb, lv_color_hex(0xff4c60d8), LV_PART_INDICATOR | LV_STATE_CHECKED); - - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = blocking; - strcpy(data->parameter, parameter); - - lv_obj_set_user_data(cb,data); - - lv_obj_add_event_cb(cb, generic_checkbox_event_cb, LV_EVENT_VALUE_CHANGED,data); - lv_obj_add_event_cb(cb, generic_back_event_handler, LV_EVENT_KEY,NULL); - - return obj; -} - -lv_obj_t * create_backbutton(lv_obj_t * parent, const char * icon, const char * label_txt) -{ - - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * img = NULL; - - if(icon) { - img = lv_image_create(obj); - lv_image_set_src(img, icon); - } - - lv_obj_t *back_button = lv_btn_create(obj); - lv_obj_add_event_cb(back_button, generic_back_event_handler, LV_EVENT_CLICKED,NULL); - lv_obj_t * label = lv_label_create(back_button); - lv_label_set_text(label, label_txt); - return obj; -} - -lv_obj_t * create_textarea(lv_obj_t * parent, char * text, const char * label_txt, const char * parameter, menu_page_data_t* menu_page_data, bool password) -{ - - lv_obj_t * obj = lv_menu_cont_create(parent); - - lv_obj_t * label = lv_label_create(obj); - lv_label_set_text(label, label_txt); - - lv_obj_t * ta = lv_textarea_create(obj); - //lv_textarea_set_placeholder_text(ta_ssid, "SSID"); - lv_textarea_set_one_line(ta,true); - lv_textarea_set_text(ta,text); - lv_obj_add_state(ta, LV_STATE_DISABLED); - lv_obj_add_style(ta, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_style(ta, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); - - thread_data_t* data = malloc(sizeof(thread_data_t)); - if (data) { - memset(data, 0, sizeof(thread_data_t)); - } - data->menu_page_data = menu_page_data; - data->blocking = false; - strcpy(data->parameter, parameter); - - lv_obj_set_user_data(ta,data); - - lv_obj_t * button = lv_button_create(obj); - lv_obj_set_user_data(button, ta); // Associate button with text area - lv_obj_add_style(button, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_style(button, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); - lv_obj_add_event_cb(button, generic_back_event_handler, LV_EVENT_KEY,NULL); - - lv_obj_t * button_label = lv_label_create(button); - lv_label_set_text(button_label,LV_SYMBOL_KEYBOARD); - - - return obj; -} - -// Recursive function to find the first focusable object -lv_obj_t * find_first_focusable_obj(lv_obj_t * parent) { - // Iterate through all children of the parent - if (lv_obj_has_flag(parent, LV_OBJ_FLAG_HIDDEN)) return NULL; - for (int i = 0; i < lv_obj_get_child_cnt(parent); i++) { - lv_obj_t * child = lv_obj_get_child(parent, i); - - // Check if the child is focusable - if ((lv_obj_has_flag(child, LV_OBJ_FLAG_CHECKABLE) || lv_obj_has_flag(child, LV_OBJ_FLAG_CLICKABLE)) && ! lv_obj_has_state(child, LV_STATE_DISABLED)) { - lv_group_t * group = lv_obj_get_group(child); - if (group != NULL) { - return child; // Return the first focusable object - } else { - // The object is not part of any group - } - } - - // Recursively check the child's children - lv_obj_t * result = find_first_focusable_obj(child); - if (result) { - return result; // Return the first focusable object found in the subtree - } - } - - return NULL; // No focusable object found -} - -void handle_sub_page_load(lv_event_t *e) { - - lv_obj_t * menu = lv_event_get_current_target(e); // Get the menu object - lv_obj_t * page = lv_menu_get_cur_main_page(menu); // Get the current main page - - if ( ! page ) - return; - - menu_page_data_t * menu_page_data = (menu_page_data_t *) lv_obj_get_user_data(page); - // set the indev to the pages group - lv_indev_set_group(indev_drv,menu_page_data->indev_group); - - if(menu_page_data != NULL && menu_page_data->page_load_callback != NULL) - menu_page_data->page_load_callback(page); - - // Find the first focusable object recursively - lv_obj_t * first_obj = find_first_focusable_obj(page); - lv_group_focus_obj(first_obj); -} - -char* get_paramater(lv_obj_t * page, char * param) { - menu_page_data_t* menu_page_data = (menu_page_data_t*) lv_obj_get_user_data(page); - char final_command[200] = "gsmenu.sh get "; - strcat(final_command,menu_page_data->type); - strcat(final_command," "); - strcat(final_command,menu_page_data->page); - strcat(final_command," "); - strcat(final_command,param); - char * result = run_command(final_command); - size_t len = strlen(result); - if (len > 0 && result[len - 1] == '\n') { - result[len - 1] = '\0'; - } - return result; -} - -// Split gsmenu.sh output at \x1e separator (in-place). -// Returns the value part, sets *values_out to the allowed-values part (or NULL). -char* split_value_and_options(char* raw, char** values_out) { - *values_out = NULL; - if (!raw) return raw; - char* sep = strchr(raw, '\x1e'); - if (sep) { - *sep = '\0'; - *values_out = sep + 1; - } - // strip trailing newline from value part - size_t len = strlen(raw); - if (len > 0 && raw[len - 1] == '\n') raw[len - 1] = '\0'; - return raw; -} - - -void reload_label_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t *obj = lv_obj_get_child_by_type(parameter, 0, &lv_label_class); - thread_data_t *param_user_data = (thread_data_t*)lv_obj_get_user_data(obj); - - // Get the parameter value - const char *param_value = get_paramater(page, param_user_data->parameter); - - // Original label - const char *org_txt = (const char*)lv_obj_get_user_data(parameter); - - // Create the combined string - char buffer[256]; - snprintf(buffer, sizeof(buffer), "%s: %s", org_txt, param_value); - - // Set the label text - lv_lock(); - lv_label_set_text(obj, buffer); - lv_unlock(); -} - -void reload_switch_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t * obj = lv_obj_get_child_by_type(parameter,0,&lv_switch_class); - if ( !lv_obj_has_state(obj, LV_STATE_DISABLED) && ! lv_obj_has_flag(parameter,LV_OBJ_FLAG_HIDDEN)) { - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(obj); - bool value = atoi(get_paramater(page,param_user_data->parameter)); - lv_lock(); - lv_obj_set_state(obj,LV_STATE_CHECKED,value); - lv_unlock(); - } -} - -void reload_dropdown_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t * obj = lv_obj_get_child_by_type(parameter,0,&lv_dropdown_class); - if ( !lv_obj_has_state(obj, LV_STATE_DISABLED) && ! lv_obj_has_flag(parameter,LV_OBJ_FLAG_HIDDEN)) { - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(obj); - char * raw = get_paramater(page, param_user_data->parameter); - char * values_str = NULL; - char * value = split_value_and_options(raw, &values_str); - lv_lock(); - if (values_str) { - lv_dropdown_set_options(obj, values_str); - update_dropdown_width(obj); - if (lv_dropdown_get_option_cnt(obj) == 1) - lv_obj_add_flag(lv_obj_get_parent(obj), LV_OBJ_FLAG_HIDDEN); - } - lv_dropdown_set_selected(obj,lv_dropdown_get_option_index(obj,value)); - lv_unlock(); - } -} - -void reload_checkbox_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t * obj = lv_obj_get_child_by_type(parameter,0,&lv_checkbox_class); - if ( !lv_obj_has_state(obj, LV_STATE_DISABLED) && ! lv_obj_has_flag(parameter,LV_OBJ_FLAG_HIDDEN)) { - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(obj); - bool value = atoi(get_paramater(page,param_user_data->parameter)); - lv_lock(); - lv_obj_set_state(obj,LV_STATE_CHECKED,value); - lv_unlock(); - } -} - -void reload_textarea_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t * obj = lv_obj_get_child_by_type(parameter,0,&lv_textarea_class); - // if ( !lv_obj_has_state(obj, LV_STATE_DISABLED) && ! lv_obj_has_flag(parameter,LV_OBJ_FLAG_HIDDEN)) { // ToDo: This need rework - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(obj); - const char * value = get_paramater(page,param_user_data->parameter); - lv_lock(); - lv_textarea_set_text(obj,value); - lv_unlock(); - // } -} - -void reload_slider_value(lv_obj_t * page,lv_obj_t * parameter) { - lv_obj_t * obj = lv_obj_get_child_by_type(parameter,0,&lv_slider_class); - lv_obj_t * label = lv_obj_get_child_by_type(parameter,1,&lv_label_class); - if ( !lv_obj_has_state(obj, LV_STATE_DISABLED) && ! lv_obj_has_flag(parameter,LV_OBJ_FLAG_HIDDEN)) { - thread_data_t * param_user_data = (thread_data_t*) lv_obj_get_user_data(obj); - char * raw = get_paramater(page,param_user_data->parameter); - char * values_str = NULL; - char * value = split_value_and_options(raw, &values_str); - - // Set range if allowed values provided - if (values_str) { - float min, max; - if (sscanf(values_str, "%f %f", &min, &max) == 2) { - int32_t scaled_min = (int32_t)(min * powf(10, param_user_data->precision)); - int32_t scaled_max = (int32_t)(max * powf(10, param_user_data->precision)); - lv_lock(); - lv_slider_set_range(obj, scaled_min, scaled_max); - lv_unlock(); - } - } - - // Set current value - float current_value; - if (sscanf(value, "%f", ¤t_value) != 1) { - return; - } - int32_t scaled_value = (int32_t)(current_value * powf(10, param_user_data->precision)); - char format[16]; - snprintf(format, sizeof(format), "%%.%df", param_user_data->precision); - char s[32]; - snprintf(s, sizeof(s), format, current_value); - lv_lock(); - lv_slider_set_value(obj,scaled_value,LV_ANIM_OFF); - lv_label_set_text(label,s); - lv_unlock(); - } -} - -static void update_dropdown_width(lv_obj_t * dropdown) { - const char * options = lv_dropdown_get_options(dropdown); - uint16_t option_cnt = lv_dropdown_get_option_cnt(dropdown); - - lv_coord_t max_width = 0; - lv_point_t p; - - // Get the style to use for font info - const lv_font_t * font = lv_obj_get_style_text_font(dropdown, LV_PART_MAIN); - - // Iterate through options (split by \n) - const char * opt = options; - for(uint16_t i = 0; i < option_cnt; i++) { - const char * next_opt = strchr(opt, '\n'); - size_t len = next_opt ? (size_t)(next_opt - opt) : strlen(opt); - - // Get text width - lv_txt_get_size(&p, opt, font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); - - if(p.x > max_width) max_width = p.x; - - if(!next_opt) break; - opt = next_opt + 1; - } - - // Add some padding (adjust as needed) - max_width += lv_obj_get_style_pad_left(dropdown, LV_PART_MAIN) + - lv_obj_get_style_pad_right(dropdown, LV_PART_MAIN) + - 20; // Extra space for the arrow - - lv_obj_set_width(dropdown, max_width); -} - -bool file_exists(const char *path) { +static bool file_exists(const char *path) { lv_fs_file_t f; lv_fs_res_t res = lv_fs_open(&f, path, LV_FS_MODE_RD); if(res == LV_FS_RES_OK) { @@ -879,65 +19,56 @@ bool file_exists(const char *path) { return false; } +/* Resolve a bundled resource (icon/logo) to an LVGL "A:" path, trying the + * installed locations then the source tree. */ const char* find_resource_file(const char* relative_path) { - static char path[256]; - - // Try installed locations const char* prefixes[] = { "/usr/local/share/pixelpilot", "/usr/share/pixelpilot", "./src/icons", }; - for(size_t i = 0; i < sizeof(prefixes)/sizeof(prefixes[0]); i++) { snprintf(path, sizeof(path), "A:%s/%s", prefixes[i], relative_path); if(file_exists(path)) { return path; } } - - return NULL; // Not found + return NULL; } -void gsmenu_toggle_rxmode() { +/* Find the first focusable (checkable/clickable, non-disabled, grouped) object + * in a subtree — used to place focus when a page opens. */ +lv_obj_t * find_first_focusable_obj(lv_obj_t * parent) { + if (lv_obj_has_flag(parent, LV_OBJ_FLAG_HIDDEN)) return NULL; + for (int i = 0; i < lv_obj_get_child_cnt(parent); i++) { + lv_obj_t * child = lv_obj_get_child(parent, i); + if ((lv_obj_has_flag(child, LV_OBJ_FLAG_CHECKABLE) || lv_obj_has_flag(child, LV_OBJ_FLAG_CLICKABLE)) && ! lv_obj_has_state(child, LV_STATE_DISABLED)) { + lv_group_t * group = lv_obj_get_group(child); + if (group != NULL) { + return child; + } + } + lv_obj_t * result = find_first_focusable_obj(child); + if (result) { + return result; + } + } + + return NULL; +} + +/* Apply the environment for the current RX mode (WFB vs APFPV). */ +void gsmenu_toggle_rxmode() { switch (RXMODE) { case APFPV: - lv_obj_add_flag(air_wfbng_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(router, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(osd_fps, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(air_gs_rendering, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(air_telemetry_msposd_text,LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(air_telemetry_msposd_section,LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(air_alink_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(gs_wfbng_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(size, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(fps, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(bitrate, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(gs_apfpv_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(video_mode, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(air_aalink_cont, LV_OBJ_FLAG_HIDDEN); setenv("REMOTE_IP" , "192.168.0.1", 1); setenv("AIR_FIRMWARE_TYPE" , "apfpv", 1); break; case WFB: - lv_obj_remove_flag(air_wfbng_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(router, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(osd_fps, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(air_gs_rendering, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(air_telemetry_msposd_text,LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(air_telemetry_msposd_section,LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(air_alink_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(gs_wfbng_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(size, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(fps, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(bitrate, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(gs_apfpv_cont, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(video_mode, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(air_aalink_cont, LV_OBJ_FLAG_HIDDEN); setenv("REMOTE_IP" , "10.5.0.10", 1); setenv("AIR_FIRMWARE_TYPE" , "wfb", 1); #ifndef USE_SIMULATOR @@ -973,6 +104,31 @@ static void restart_dialog_btn_cb(lv_event_t *e) { raise(SIGHUP); } +void theme_msgbox(lv_obj_t *mbox) { + lv_obj_add_style(mbox, &style_openipc_lightdark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *header = lv_msgbox_get_header(mbox); + if (header) + lv_obj_add_style(header, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *footer = lv_msgbox_get_footer(mbox); + if (footer) + lv_obj_add_style(footer, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *content = lv_msgbox_get_content(mbox); + if (content) + lv_obj_add_style(content, &style_openipc_dark_background, LV_PART_MAIN | LV_STATE_DEFAULT); + + /* For modal msgboxes (created with a NULL parent) the theme gives the + * backdrop a bright grey 50% tint. Darken it so it dims the video behind + * the dialog instead of washing it out. */ + lv_obj_t *backdrop = lv_obj_get_parent(mbox); + if (backdrop && lv_obj_check_type(backdrop, &lv_msgbox_backdrop_class)) { + lv_obj_set_style_bg_color(backdrop, lv_color_black(), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(backdrop, 160, LV_PART_MAIN | LV_STATE_DEFAULT); + } +} + void show_restart_notice(void) { lv_group_t *prev_group = lv_indev_get_group(indev_drv); lv_group_t *prev_default_group = lv_group_get_default(); @@ -985,6 +141,11 @@ void show_restart_notice(void) { if (backdrop) lv_obj_swap(backdrop, mbox); + /* give it room so the title/text don't wrap into a squeezed box, and centre it */ + lv_obj_set_width(mbox, 520); + lv_obj_set_style_pad_all(mbox, 20, LV_PART_MAIN); + lv_obj_center(mbox); + lv_msgbox_add_title(mbox, LV_SYMBOL_WARNING " Restart required"); lv_msgbox_add_text(mbox, "A restart is required to apply the new resolution.\nRestart now?"); lv_obj_t *yes_btn = lv_msgbox_add_footer_button(mbox, LV_SYMBOL_OK " Yes"); @@ -995,6 +156,8 @@ void show_restart_notice(void) { lv_obj_add_style(no_btn, &style_openipc, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_style(no_btn, &style_openipc_outline, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + theme_msgbox(mbox); + restart_dialog_ctx_t *ctx = malloc(sizeof(restart_dialog_ctx_t)); ctx->mbox = mbox; ctx->prev_group = prev_group; @@ -1002,77 +165,14 @@ void show_restart_notice(void) { ctx->dialog_group = dialog_group; lv_indev_set_group(indev_drv, dialog_group); + /* The key that opened this dialog (e.g. ENTER selecting the resolution) is + * still pressed; without this its release would immediately "click" the + * freshly focused Yes button. Wait for a fresh press. */ + lv_indev_wait_release(indev_drv); lv_obj_add_event_cb(yes_btn, restart_dialog_btn_cb, LV_EVENT_CLICKED, ctx); lv_obj_add_event_cb(no_btn, restart_dialog_btn_cb, LV_EVENT_CLICKED, ctx); -} -void add_entry_to_menu_page(menu_page_data_t *menu_page_data, const char* text, lv_obj_t* obj, ReloadFunc reload_func) { - // Increase entry count - menu_page_data->entry_count++; - - // Reallocate memory for entries - PageEntry *new_entries = realloc(menu_page_data->page_entries, - sizeof(PageEntry) * menu_page_data->entry_count); - - if (new_entries) { - menu_page_data->page_entries = new_entries; - - // Add new entry at the end with string copy - menu_page_data->page_entries[menu_page_data->entry_count - 1] = - (PageEntry){ strdup(text), obj, reload_func }; - } else { - // Handle allocation failure - menu_page_data->entry_count--; // Revert count on failure - } + /* default to the safe choice */ + lv_group_focus_obj(no_btn); } - -void delete_menu_page_entry_by_obj(menu_page_data_t *menu_page_data, lv_obj_t* obj) { - if (!menu_page_data || !menu_page_data->page_entries || menu_page_data->entry_count == 0) { - return; - } - - // Find the index of the entry with matching object - int found_index = -1; - for (size_t i = 0; i < menu_page_data->entry_count; i++) { - if (menu_page_data->page_entries[i].target == obj) { - found_index = i; - break; - } - } - - if (found_index == -1) { - return; - } - - // Free the duplicated string - if (menu_page_data->page_entries[found_index].caption) { - free((void*)menu_page_data->page_entries[found_index].caption); - } - - // Shift entries - for (size_t i = found_index; i < menu_page_data->entry_count - 1; i++) { - menu_page_data->page_entries[i] = menu_page_data->page_entries[i + 1]; - } - - menu_page_data->entry_count--; - - // Only reallocate if we have entries left - if (menu_page_data->entry_count > 0) { - PageEntry *new_entries = realloc(menu_page_data->page_entries, - sizeof(PageEntry) * menu_page_data->entry_count); - // If realloc succeeds, use the new pointer. If it fails, keep the old one. - if (new_entries) { - menu_page_data->page_entries = new_entries; - } - } else { - free(menu_page_data->page_entries); - menu_page_data->page_entries = NULL; - } -} - -void custom_actions_cb(lv_event_t * event) -{ - MenuAction *action = lv_event_get_user_data(event); - run_command_and_block(event, action->action, NULL); -} \ No newline at end of file diff --git a/src/gsmenu/helper.h b/src/gsmenu/helper.h index f8e472eb..6ae23920 100644 --- a/src/gsmenu/helper.h +++ b/src/gsmenu/helper.h @@ -1,59 +1,11 @@ #pragma once -#include -#include -#include - -#include "ui.h" - #include "../../lvgl/lvgl.h" -typedef enum { - LV_MENU_ITEM_BUILDER_VARIANT_1, - LV_MENU_ITEM_BUILDER_VARIANT_2 -} lv_menu_builder_variant_t; - -void on_focus(lv_event_t* e); - -void generic_page_load_callback(lv_obj_t * page); - -lv_obj_t * create_text(lv_obj_t * parent, const char * icon, const char * txt, const char * parameter, menu_page_data_t* menu_page_data,bool blocking,lv_menu_builder_variant_t builder_variant); - -lv_obj_t * create_slider(lv_obj_t * parent, const char * icon, const char * txt, const char * parameter, menu_page_data_t* menu_page_data, bool blocking, int precision); -lv_obj_t * create_switch(lv_obj_t * parent, const char * icon, const char * txt,const char * parameter, menu_page_data_t* menu_page_data,bool blocking); - -void dropdown_event_handler(lv_event_t * e); - -lv_obj_t * create_dropdown(lv_obj_t * parent, const char * icon, const char * label_txt, const char * txt,const char * parameter, menu_page_data_t* menu_page_data,bool blocking); - -lv_obj_t * create_checkbox(lv_obj_t * parent, const char * icon, const char * label_txt, const char * parameter, menu_page_data_t* menu_page_data,bool blocking); - -void generic_button_callback(lv_event_t * e); -lv_obj_t * create_button(lv_obj_t * parent, const char * txt); - -lv_obj_t * create_backbutton(lv_obj_t * parent, const char * icon, const char * label_txt); - -lv_obj_t * create_textarea(lv_obj_t * parent, char * text, const char * label_txt, const char * parameter, menu_page_data_t* menu_page_data, bool password); - -lv_obj_t * create_spinbox(lv_obj_t * parent, const char * icon, const char * txt, int32_t min, int32_t max, - int32_t val); - lv_obj_t * find_first_focusable_obj(lv_obj_t * parent); -void handle_sub_page_load(lv_event_t *e); -char* get_paramater(lv_obj_t * page, char * param); -char* split_value_and_options(char* raw, char** values_out); -void reload_label_value(lv_obj_t * page,lv_obj_t * parameter); -void reload_switch_value(lv_obj_t * page,lv_obj_t * parameter); -void reload_dropdown_value(lv_obj_t * page,lv_obj_t * parameter); -void reload_checkbox_value(lv_obj_t * page,lv_obj_t * parameter); -void reload_textarea_value(lv_obj_t * page,lv_obj_t * parameter); -void reload_slider_value(lv_obj_t * page,lv_obj_t * parameter); -void generic_back_event_handler(lv_event_t * e); - const char* find_resource_file(const char* relative_path); - -void gsmenu_toggle_rxmode(); +void gsmenu_toggle_rxmode(void); void show_restart_notice(void); -void add_entry_to_menu_page(menu_page_data_t *menu_page_data, const char* text, lv_obj_t* obj, ReloadFunc reload_func); -void delete_menu_page_entry_by_obj(menu_page_data_t *menu_page_data, lv_obj_t* obj); -void custom_actions_cb(lv_event_t * event); +/* Apply the dark gsmenu theme to a msgbox (body, header, footer, content). + * Call after the title and any footer buttons have been added. */ +void theme_msgbox(lv_obj_t *mbox); diff --git a/src/gsmenu/styles.c b/src/gsmenu/styles.c index 79b79d4c..b06ee455 100644 --- a/src/gsmenu/styles.c +++ b/src/gsmenu/styles.c @@ -12,54 +12,129 @@ lv_style_t style_openipc_dark_background; lv_style_t style_openipc_lightdark_background; +/* + * dark & translucent theme. + * + * The whole gsmenu renders on a transparent LVGL screen on top of the live + * video, so semi-transparent dark panels let the video subtly show through + * while keeping text readable. The default LVGL theme is light, so we force a + * light text color on the container styles (text color inherits to children). + */ + +/* Panel / surface colors (deep, cool near-black shades) */ +#define COL_PANEL lv_color_hex(0x0b0e14) /* outer menu background */ +#define COL_SECTION lv_color_hex(0x161b24) /* grouped rows (cards) */ +#define COL_DARK lv_color_hex(0x080a10) /* dropdown lists / tables */ +#define COL_LIGHTDARK lv_color_hex(0x232a37) /* text areas / knobs */ +#define COL_ACCENT lv_color_hex(0x4c60d8) /* OpenIPC brand blue (selection / focus) */ +#define COL_TEXT lv_color_hex(0xf2f4f8) /* primary text */ +#define COL_TEXT_DIM lv_color_hex(0x8a93a6) /* disabled / secondary text */ +#define COL_BORDER lv_color_hex(0x39424f) /* subtle panel borders */ + +/* Translucency levels (0 = transparent, 255 = opaque) */ +#define OPA_PANEL 180 /* ~47% (semi-transparent page surface) */ +#define OPA_SECTION 200 /* ~55% */ +#define OPA_DARK 235 /* ~92% (dropdowns need good contrast) */ +#define OPA_LIGHTDARK 210 /* ~82% */ + +#define RADIUS_CARD 8 +#define RADIUS_ITEM 6 + + int style_init(void) { lv_style_reset(&style_rootmenu); lv_style_init(&style_rootmenu); - lv_style_set_bg_color(&style_rootmenu, lv_color_darken( lv_color_make(0xcd, 0xcd, 0xcd), 50)); + /* Semi-transparent panel: the page reads as one cohesive dark surface, + * but the live video still shows through it. */ + lv_style_set_bg_color(&style_rootmenu, COL_PANEL); + lv_style_set_bg_opa(&style_rootmenu, OPA_PANEL); + lv_style_set_text_color(&style_rootmenu, COL_TEXT); lv_style_set_pad_top(&style_rootmenu, 0); lv_style_set_pad_bottom(&style_rootmenu, 0); lv_style_set_pad_left(&style_rootmenu, 0); lv_style_set_pad_right(&style_rootmenu, 0); lv_style_set_radius(&style_rootmenu, 0); lv_style_set_border_width(&style_rootmenu, 0); - lv_style_set_border_color(&style_rootmenu, lv_color_hex(0xff4c60d8)); + lv_style_set_border_color(&style_rootmenu, COL_ACCENT); lv_style_reset(&style_openipc_section); lv_style_init(&style_openipc_section); - lv_style_set_bg_color(&style_openipc_section, lv_color_lighten( lv_color_make(0xcd, 0xcd, 0xcd), 50)); + lv_style_set_bg_color(&style_openipc_section, COL_SECTION); + lv_style_set_bg_opa(&style_openipc_section, OPA_SECTION); + lv_style_set_text_color(&style_openipc_section, COL_TEXT); + lv_style_set_radius(&style_openipc_section, RADIUS_CARD); + lv_style_set_border_width(&style_openipc_section, 1); + lv_style_set_border_color(&style_openipc_section, COL_BORDER); + lv_style_set_border_opa(&style_openipc_section, LV_OPA_40); lv_style_reset(&style_openipc_dark_background); lv_style_init(&style_openipc_dark_background); - lv_style_set_bg_color(&style_openipc_dark_background, lv_color_darken( lv_color_make(0xcd, 0xcd, 0xcd), 90)); + lv_style_set_bg_color(&style_openipc_dark_background, COL_DARK); + lv_style_set_bg_opa(&style_openipc_dark_background, OPA_DARK); + lv_style_set_text_color(&style_openipc_dark_background, COL_TEXT); + lv_style_set_radius(&style_openipc_dark_background, RADIUS_ITEM); + lv_style_set_border_width(&style_openipc_dark_background, 1); + lv_style_set_border_color(&style_openipc_dark_background, COL_BORDER); + lv_style_set_border_opa(&style_openipc_dark_background, LV_OPA_50); lv_style_reset(&style_openipc_lightdark_background); lv_style_init(&style_openipc_lightdark_background); - lv_style_set_bg_color(&style_openipc_lightdark_background, lv_color_darken( lv_color_make(0xcd, 0xcd, 0xcd), 30)); + lv_style_set_bg_color(&style_openipc_lightdark_background, COL_LIGHTDARK); + lv_style_set_bg_opa(&style_openipc_lightdark_background, OPA_LIGHTDARK); + lv_style_set_text_color(&style_openipc_lightdark_background, COL_TEXT); + lv_style_set_radius(&style_openipc_lightdark_background, RADIUS_ITEM); lv_style_reset(&style_openipc); lv_style_init(&style_openipc); - lv_style_set_bg_color(&style_openipc, lv_color_hex(0xff4c60d8)); - lv_style_set_outline_color(&style_openipc, lv_color_hex(0xff4c60d8)); - lv_style_set_arc_color(&style_openipc, lv_color_hex(0xff4c60d8)); + lv_style_set_bg_color(&style_openipc, COL_ACCENT); + lv_style_set_bg_opa(&style_openipc, LV_OPA_COVER); + lv_style_set_text_color(&style_openipc, COL_TEXT); + lv_style_set_radius(&style_openipc, RADIUS_ITEM); + lv_style_set_outline_color(&style_openipc, COL_ACCENT); + lv_style_set_arc_color(&style_openipc, COL_ACCENT); lv_style_init(&style_openipc_dropdown); - lv_style_set_bg_color(&style_openipc_dropdown, lv_color_hex(0xff4c60d8)); + lv_style_set_bg_color(&style_openipc_dropdown, COL_ACCENT); lv_style_reset(&style_openipc_outline); lv_style_init(&style_openipc_outline); - lv_style_set_outline_color(&style_openipc_outline, lv_color_hex(0xff4c60d8)); - lv_style_set_outline_width(&style_openipc_outline,7); + lv_style_set_outline_color(&style_openipc_outline, COL_ACCENT); + lv_style_set_outline_opa(&style_openipc_outline, LV_OPA_COVER); + lv_style_set_outline_width(&style_openipc_outline, 3); + lv_style_set_radius(&style_openipc_outline, RADIUS_ITEM); lv_style_reset(&style_openipc_textcolor); lv_style_init(&style_openipc_textcolor); - lv_style_set_text_color(&style_openipc_textcolor, lv_color_hex(0xff4c60d8)); + lv_style_set_text_color(&style_openipc_textcolor, COL_ACCENT); lv_style_reset(&style_openipc_disabled); lv_style_init(&style_openipc_disabled); - lv_style_set_bg_color(&style_openipc_disabled, lv_color_hex(0xff4c60d8)); - lv_style_set_text_color(&style_openipc_disabled, lv_color_darken( lv_color_make(0xff, 0xff, 0xff), 50)); - //lv_style_set_line_color(&style_openipc_disabled, lv_color_hex(0xffd8ce36)); - //lv_style_set_border_color(&style_openipc_disabled, lv_color_hex(0xffe61212)); + lv_style_set_bg_color(&style_openipc_disabled, COL_DARK); + lv_style_set_bg_opa(&style_openipc_disabled, OPA_SECTION); + lv_style_set_text_color(&style_openipc_disabled, COL_TEXT_DIM); return 0; } + +/* Loading spinner themed for the translucent OSD: an accent-blue indicator arc + * on a small dark disc that matches the menu panels. The default LVGL track ring + * (an opaque grey circle) is hidden so only the moving arc shows. + * + * The disc is NOT just cosmetic: the OSD renders on a transparent screen composited + * over live video by the DRM overlay, and the spinner's arc animates — so its area + * is redrawn every frame. A fully-transparent (unpainted) region leaves a white + * trail there on real hardware instead of showing the video through, so the disc + * gives the animated area actual (theme-dark) pixels to paint each frame. */ +lv_obj_t * openipc_spinner_create(lv_obj_t * parent) +{ + lv_obj_t * sp = lv_spinner_create(parent); + lv_obj_set_size(sp, 64, 64); + lv_obj_center(sp); + lv_obj_add_style(sp, &style_openipc, LV_PART_INDICATOR | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(sp, COL_DARK, LV_PART_MAIN); /* themed dark disc */ + lv_obj_set_style_bg_opa(sp, OPA_DARK, LV_PART_MAIN); + lv_obj_set_style_radius(sp, LV_RADIUS_CIRCLE, LV_PART_MAIN); + lv_obj_set_style_pad_all(sp, 8, LV_PART_MAIN); /* inset arc from edge */ + lv_obj_set_style_arc_opa(sp, LV_OPA_TRANSP, LV_PART_MAIN); /* hide the track ring */ + return sp; +} diff --git a/src/gsmenu/styles.h b/src/gsmenu/styles.h index 2ffd7769..2bb28571 100644 --- a/src/gsmenu/styles.h +++ b/src/gsmenu/styles.h @@ -18,6 +18,9 @@ extern lv_style_t style_openipc_lightdark_background; int style_init(void); +/* Create an OSD-themed loading spinner (accent arc, transparent track) on parent. */ +lv_obj_t * openipc_spinner_create(lv_obj_t * parent); + #ifdef __cplusplus } #endif diff --git a/src/gsmenu/ui.c b/src/gsmenu/ui.c deleted file mode 100644 index 8230094f..00000000 --- a/src/gsmenu/ui.c +++ /dev/null @@ -1,525 +0,0 @@ -#include -#include -#include -#include "../../lvgl/lvgl.h" - -#include "../main.h" -#include "../input.h" -#include "helper.h" -#include "air_wfbng.h" -#include "air_alink.h" -#include "air_aalink.h" -#include "air_camera.h" -#include "air_telemetry.h" -#include "air_actions.h" -#include "gs_dvr.h" -#include "gs_main.h" -#include "gs_wfbng.h" -#include "gs_apfpv.h" -#include "gs_system.h" -#include "gs_wifi.h" -#include "gs_actions.h" -#include "styles.h" -#include "gs_connection_checker.h" - -extern enum RXMode RXMODE; - -static void back_event_handler(lv_event_t * e); -extern lv_obj_t * menu; -extern lv_indev_t * indev_drv; -extern gsmenu_control_mode_t control_mode; -lv_obj_t * root_page; -lv_group_t *main_group; -extern lv_group_t *default_group; -extern lv_obj_t * pp_osd_screen; -extern lv_group_t * osd_group; - -lv_obj_t * sub_gs_main_page; -lv_obj_t * sub_air_wfbng_page; -lv_obj_t * sub_air_alink_page; -lv_obj_t * sub_air_aalink_page; -lv_obj_t * sub_air_camera_video_page; -lv_obj_t * sub_air_camera_image_page; -lv_obj_t * sub_air_camera_recording_page; -lv_obj_t * sub_air_camera_isp_page; -lv_obj_t * sub_air_camera_fpv_page; -lv_obj_t * sub_air_camera_page; -lv_obj_t * sub_air_telemetry_page; -lv_obj_t * sub_air_actions_page; -lv_obj_t * sub_gs_dvr_page; -lv_obj_t * sub_gs_wfbng_page; -lv_obj_t * sub_gs_apfpv_page; -lv_obj_t * sub_gs_system_receiver_page; -lv_obj_t * sub_gs_system_display_page; -lv_obj_t * sub_gs_system_dvr_page; -lv_obj_t * sub_gs_system_page; -lv_obj_t * sub_wlan_page; -lv_obj_t * sub_gs_actions_page; - -lv_obj_t * air_wfbng_cont; -lv_obj_t * air_alink_cont; -lv_obj_t * air_aalink_cont; -lv_obj_t * air_camera_cont; -lv_obj_t * air_telemetry_cont; -lv_obj_t * air_actions_cont; -lv_obj_t * gs_dvr_cont; -lv_obj_t * gs_wfbng_cont; -lv_obj_t * gs_apfpv_cont; -lv_obj_t * gs_system_cont; -lv_obj_t * gs_wlan_cont; -lv_obj_t * gs_actions_cont; - -extern lv_obj_t * ap_fpv_channel; - -extern bool menu_active; -extern uint64_t gtotal_tunnel_data; // global variable for easyer access in gsmenu -uint64_t last_count = 0; - -static int last_value = 0; -static uint32_t last_increase_time = 0; -static bool objects_active = true; - -void recursive_state_set(lv_obj_t *obj, bool enable) { - if (!obj) return; - - // Set state for the current object - const lv_obj_class_t * object_class = lv_obj_get_class(obj); - if(object_class == &lv_textarea_class) { - return; - } - - if (enable) { - lv_obj_remove_state(obj, LV_STATE_DISABLED); - } else { - lv_obj_add_state(obj, LV_STATE_DISABLED); - } - - // Recursively process all children - uint32_t child_count = lv_obj_get_child_count(obj); - for (uint32_t i = 0; i < child_count; i++) { - lv_obj_t *child = lv_obj_get_child(obj, i); - recursive_state_set(child, enable); - } -} - -void check_connection_timer(lv_timer_t * timer) -{ - static uint32_t last_value = 0; - static uint32_t last_increase_time = 0; - - if (RXMODE == APFPV) update_network_status(); - uint32_t current_value = (uint32_t)gtotal_tunnel_data; - - // Reset detection (either manual reset or wraparound) - bool is_reset = false; - - // Case 1: Simple decrease (manual reset) - if (current_value < last_value) { - is_reset = true; - } - // Case 2: Wraparound detection (for unsigned counters) - else if ((last_value > UINT32_MAX - 1000) && (current_value < 1000)) { - is_reset = true; - } - - if (is_reset) { - if (objects_active) { - if(control_mode != GSMENU_CONTROL_MODE_NAV) { - lv_key_t key = LV_KEY_ESC; - lv_group_t * group = lv_indev_get_group(indev_drv); - lv_obj_t * obj = lv_group_get_focused(group); - if (lv_obj_check_type(obj,&lv_dropdown_class)) - lv_obj_send_event(obj,LV_EVENT_KEY,&key); - else if (lv_obj_check_type(obj,&lv_slider_class)) - lv_obj_send_event(obj,LV_EVENT_CANCEL,obj); - } - recursive_state_set(air_wfbng_cont, false); - recursive_state_set(air_alink_cont, false); - recursive_state_set(air_aalink_cont, false); - recursive_state_set(air_camera_cont, false); - recursive_state_set(air_telemetry_cont, false); - recursive_state_set(air_actions_cont, false); - recursive_state_set(sub_air_wfbng_page, false); - recursive_state_set(sub_air_alink_page, false); - recursive_state_set(sub_air_aalink_page, false); - recursive_state_set(sub_air_camera_page, false); - recursive_state_set(sub_air_camera_video_page, false); - recursive_state_set(sub_air_camera_image_page, false); - recursive_state_set(sub_air_camera_recording_page, false); - recursive_state_set(sub_air_camera_isp_page, false); - recursive_state_set(sub_air_camera_fpv_page, false); - recursive_state_set(sub_air_telemetry_page, false); - recursive_state_set(sub_air_actions_page, false); - lv_obj_add_state(lv_obj_get_child_by_type(ap_fpv_channel,0,&lv_dropdown_class),LV_STATE_DISABLED); - setenv("GSMENU_VTX_DETECTED" , "0", 1); - lv_obj_t * current_page = lv_menu_get_cur_main_page(menu); - if (sub_air_wfbng_page == current_page || - sub_air_alink_page == current_page || - sub_air_aalink_page == current_page || - sub_air_camera_page == current_page || - sub_air_camera_video_page == current_page || - sub_air_camera_image_page == current_page || - sub_air_camera_recording_page == current_page || - sub_air_camera_isp_page == current_page || - sub_air_camera_fpv_page == current_page || - sub_air_telemetry_page == current_page || - sub_air_actions_page == current_page - ) { - lv_indev_set_group(indev_drv,main_group); - } - - objects_active = false; - } - last_value = current_value; - return; - } - - // Normal increase detection - if (current_value > last_value) { - last_increase_time = lv_tick_get(); - last_value = current_value; - - if (!objects_active) { - recursive_state_set(air_wfbng_cont, true); - recursive_state_set(air_alink_cont, true); - recursive_state_set(air_aalink_cont, true); - recursive_state_set(air_camera_cont, true); - recursive_state_set(air_telemetry_cont, true); - recursive_state_set(air_actions_cont, true); - recursive_state_set(sub_air_wfbng_page, true); - recursive_state_set(sub_air_alink_page, true); - recursive_state_set(sub_air_aalink_page, true); - recursive_state_set(sub_air_camera_page, true); - recursive_state_set(sub_air_camera_video_page, true); - recursive_state_set(sub_air_camera_image_page, true); - recursive_state_set(sub_air_camera_recording_page, true); - recursive_state_set(sub_air_camera_isp_page, true); - recursive_state_set(sub_air_camera_fpv_page, true); - recursive_state_set(sub_air_telemetry_page, true); - recursive_state_set(sub_air_actions_page, true); - lv_obj_remove_state(lv_obj_get_child_by_type(ap_fpv_channel,0,&lv_dropdown_class), LV_STATE_DISABLED); - setenv("GSMENU_VTX_DETECTED" , "1", 1); - - lv_obj_t * current_page = lv_menu_get_cur_main_page(menu); - if (sub_air_wfbng_page == current_page || - sub_air_alink_page == current_page || - sub_air_aalink_page == current_page || - sub_air_camera_page == current_page || - sub_air_camera_video_page == current_page || - sub_air_camera_image_page == current_page || - sub_air_camera_recording_page == current_page || - sub_air_camera_isp_page == current_page || - sub_air_camera_fpv_page == current_page || - sub_air_telemetry_page == current_page || - sub_air_actions_page == current_page - ) { - menu_page_data_t* menu_page_data = (menu_page_data_t*) lv_obj_get_user_data(current_page); - lv_indev_set_group(indev_drv,menu_page_data->indev_group); - } - objects_active = true; - } - } - // Timeout detection - else if (objects_active && (lv_tick_elaps(last_increase_time) > 2000)) { - if(control_mode != GSMENU_CONTROL_MODE_NAV) { - lv_key_t key = LV_KEY_ESC; - lv_group_t * group = lv_indev_get_group(indev_drv); - lv_obj_t * obj = lv_group_get_focused(group); - if (lv_obj_check_type(obj,&lv_dropdown_class)) - lv_obj_send_event(obj,LV_EVENT_KEY,&key); - else if (lv_obj_check_type(obj,&lv_slider_class)) - lv_obj_send_event(obj,LV_EVENT_CANCEL,obj); - } - recursive_state_set(air_wfbng_cont, false); - recursive_state_set(air_alink_cont, false); - recursive_state_set(air_aalink_cont, false); - recursive_state_set(air_camera_cont, false); - recursive_state_set(air_telemetry_cont, false); - recursive_state_set(air_actions_cont, false); - recursive_state_set(sub_air_wfbng_page, false); - recursive_state_set(sub_air_alink_page, false); - recursive_state_set(sub_air_aalink_page, false); - recursive_state_set(sub_air_camera_page, false); - recursive_state_set(sub_air_camera_video_page, false); - recursive_state_set(sub_air_camera_image_page, false); - recursive_state_set(sub_air_camera_recording_page, false); - recursive_state_set(sub_air_camera_isp_page, false); - recursive_state_set(sub_air_camera_fpv_page, false); - recursive_state_set(sub_air_telemetry_page, false); - recursive_state_set(sub_air_actions_page, false); - lv_obj_add_state(lv_obj_get_child_by_type(ap_fpv_channel,0,&lv_dropdown_class),LV_STATE_DISABLED); - setenv("GSMENU_VTX_DETECTED" , "0", 1); - - lv_obj_t * current_page = lv_menu_get_cur_main_page(menu); - if (sub_air_wfbng_page == current_page || - sub_air_alink_page == current_page || - sub_air_aalink_page == current_page || - sub_air_camera_page == current_page || - sub_air_camera_video_page == current_page || - sub_air_camera_image_page == current_page || - sub_air_camera_recording_page == current_page || - sub_air_camera_isp_page == current_page || - sub_air_camera_fpv_page == current_page || - sub_air_telemetry_page == current_page || - sub_air_actions_page == current_page - ) { - lv_indev_set_group(indev_drv,main_group); - } - - objects_active = false; - } -} - -lv_obj_t * pp_header_create(lv_obj_t * screen) { - - lv_obj_t *header = lv_obj_create(screen); - - lv_obj_set_pos(header, 0, 0); - lv_obj_set_width(header,LV_PCT(100)); - lv_obj_set_height(header,LV_PCT(20)); - lv_obj_set_size(header, LV_PCT(100), LV_PCT(20)); - lv_obj_set_style_bg_color(header, lv_color_hex(0xff4c60d8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_side(header, LV_BORDER_SIDE_NONE, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(header, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *obj = lv_img_create(header); - lv_image_set_src(obj, find_resource_file("OpenIPC__OPENIPC_logo_white.png")); - lv_image_set_inner_align(obj, LV_IMAGE_ALIGN_CENTER); - lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100)); - lv_image_set_scale(obj, lv_disp_get_ver_res(NULL) / 30); - -} - -lv_obj_t * pp_menu_create(lv_obj_t * screen) -{ - main_group = lv_group_create(); - lv_group_set_default(main_group); - lv_indev_set_group(indev_drv,main_group); - - menu = lv_menu_create(screen); - lv_obj_set_pos(menu, 0, LV_PCT(20)); - lv_obj_set_size(menu, LV_PCT(100), LV_PCT(80)); - lv_menu_set_mode_root_back_button(menu, LV_MENU_ROOT_BACK_BUTTON_DISABLED); - lv_obj_add_style(menu, &style_rootmenu, LV_PART_MAIN); - - lv_obj_add_event_cb(menu, back_event_handler, LV_EVENT_CLICKED, menu); - lv_obj_add_event_cb(menu, handle_sub_page_load, LV_EVENT_VALUE_CHANGED, menu); - - lv_obj_t * section; - - /*Create sub pages*/ - sub_gs_main_page = lv_menu_page_create(menu, NULL); - lv_obj_set_style_pad_hor(sub_gs_main_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_main_page); - create_main_menu(sub_gs_main_page); - - sub_air_wfbng_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" WFB-NG"); - lv_obj_set_style_pad_hor(sub_air_wfbng_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_wfbng_page); - create_air_wfbng_menu(sub_air_wfbng_page); - - sub_air_alink_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" ALink"); - lv_obj_set_style_pad_hor(sub_air_alink_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_alink_page); - create_air_alink_menu(sub_air_alink_page); - - sub_air_aalink_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" AALink"); - lv_obj_set_style_pad_hor(sub_air_aalink_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_aalink_page); - create_air_aalink_menu(sub_air_aalink_page); - - sub_air_camera_video_page = lv_menu_page_create(menu, LV_SYMBOL_IMAGE" Video"); - lv_obj_set_style_pad_hor(sub_air_camera_video_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_video_page); - create_air_camera_video_menu(sub_air_camera_video_page); - - sub_air_camera_image_page = lv_menu_page_create(menu, LV_SYMBOL_IMAGE" Image"); - lv_obj_set_style_pad_hor(sub_air_camera_image_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_image_page); - create_air_camera_image_menu(sub_air_camera_image_page); - - sub_air_camera_recording_page = lv_menu_page_create(menu, LV_SYMBOL_VIDEO" Recording"); - lv_obj_set_style_pad_hor(sub_air_camera_recording_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_recording_page); - create_air_camera_recording_menu(sub_air_camera_recording_page); - - sub_air_camera_isp_page = lv_menu_page_create(menu, LV_SYMBOL_EDIT" ISP"); - lv_obj_set_style_pad_hor(sub_air_camera_isp_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_isp_page); - create_air_camera_isp_menu(sub_air_camera_isp_page); - - sub_air_camera_fpv_page = lv_menu_page_create(menu, LV_SYMBOL_EYE_OPEN" FPV"); - lv_obj_set_style_pad_hor(sub_air_camera_fpv_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_fpv_page); - create_air_camera_fpv_menu(sub_air_camera_fpv_page); - - sub_air_camera_page = lv_menu_page_create(menu, LV_SYMBOL_IMAGE" Camera"); - lv_obj_set_style_pad_hor(sub_air_camera_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_camera_page); - create_air_camera_menu(sub_air_camera_page, sub_air_camera_video_page, sub_air_camera_image_page, sub_air_camera_recording_page, sub_air_camera_isp_page, sub_air_camera_fpv_page); - - sub_air_telemetry_page = lv_menu_page_create(menu, LV_SYMBOL_DOWNLOAD" Drone Telemetry"); - lv_obj_set_style_pad_hor(sub_air_telemetry_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_telemetry_page); - create_air_telemetry_menu(sub_air_telemetry_page); - - sub_air_actions_page = lv_menu_page_create(menu, LV_SYMBOL_PLAY" Actions"); - lv_obj_set_style_pad_hor(sub_air_actions_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_air_actions_page); - create_air_actions_menu(sub_air_actions_page); - - sub_gs_dvr_page = lv_menu_page_create(menu, LV_SYMBOL_VIDEO" DVR"); - lv_obj_set_style_pad_hor(sub_gs_dvr_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_dvr_page); - create_gs_dvr_menu(sub_gs_dvr_page); - - sub_gs_wfbng_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" WFB-NG"); - lv_obj_set_style_pad_hor(sub_gs_wfbng_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_wfbng_page); - create_gs_wfbng_menu(sub_gs_wfbng_page); - - sub_gs_apfpv_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" APFPV"); - lv_obj_set_style_pad_hor(sub_gs_apfpv_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_apfpv_page); - create_apfpv_menu(sub_gs_apfpv_page); - - sub_gs_system_receiver_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" Receiver"); - lv_obj_set_style_pad_hor(sub_gs_system_receiver_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_system_receiver_page); - create_gs_system_receiver_menu(sub_gs_system_receiver_page); - - sub_gs_system_display_page = lv_menu_page_create(menu, LV_SYMBOL_IMAGE" Display"); - lv_obj_set_style_pad_hor(sub_gs_system_display_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_system_display_page); - create_gs_system_display_menu(sub_gs_system_display_page); - - sub_gs_system_dvr_page = lv_menu_page_create(menu, LV_SYMBOL_VIDEO" DVR"); - lv_obj_set_style_pad_hor(sub_gs_system_dvr_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_system_dvr_page); - create_gs_system_dvr_menu(sub_gs_system_dvr_page); - - sub_gs_system_page = lv_menu_page_create(menu, LV_SYMBOL_SETTINGS" System"); - lv_obj_set_style_pad_hor(sub_gs_system_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_system_page); - create_gs_system_menu(sub_gs_system_page, sub_gs_system_receiver_page, sub_gs_system_display_page, sub_gs_system_dvr_page); - - sub_wlan_page = lv_menu_page_create(menu, LV_SYMBOL_WIFI" WiFi"); - lv_obj_set_style_pad_hor(sub_wlan_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_wlan_page); - create_wifi_menu(sub_wlan_page); - - sub_gs_actions_page = lv_menu_page_create(menu, LV_SYMBOL_PLAY" Actions"); - lv_obj_set_style_pad_hor(sub_gs_actions_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - lv_menu_separator_create(sub_gs_actions_page); - create_gs_actions_menu(sub_gs_actions_page); - - /*Create a root page*/ - root_page = lv_menu_page_create(menu, "Menu"); - lv_obj_set_style_pad_hor(root_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0); - - create_text(root_page, NULL, "Drone Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(root_page); - lv_obj_add_style(section, &style_openipc_section, 0); - - air_wfbng_cont = create_text(section, LV_SYMBOL_WIFI, "WFB-NG", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_wfbng_cont); - lv_menu_set_load_page_event(menu, air_wfbng_cont, sub_air_wfbng_page); - lv_obj_add_event_cb(air_wfbng_cont,back_event_handler,LV_EVENT_KEY,NULL); - - air_alink_cont = create_text(section, LV_SYMBOL_WIFI, "ALink", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_alink_cont); - lv_menu_set_load_page_event(menu, air_alink_cont, sub_air_alink_page); - lv_obj_add_event_cb(air_alink_cont,back_event_handler,LV_EVENT_KEY,NULL); - - air_aalink_cont = create_text(section, LV_SYMBOL_WIFI, "AALink", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_aalink_cont); - lv_menu_set_load_page_event(menu, air_aalink_cont, sub_air_aalink_page); - lv_obj_add_event_cb(air_aalink_cont,back_event_handler,LV_EVENT_KEY,NULL); - - air_camera_cont = create_text(section, LV_SYMBOL_IMAGE, "Camera", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_camera_cont); - lv_menu_set_load_page_event(menu, air_camera_cont, sub_air_camera_page); - lv_obj_add_event_cb(air_camera_cont,back_event_handler,LV_EVENT_KEY,NULL); - - air_telemetry_cont = create_text(section, LV_SYMBOL_DOWNLOAD, "Telemetry", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_telemetry_cont); - lv_menu_set_load_page_event(menu, air_telemetry_cont, sub_air_telemetry_page); - lv_obj_add_event_cb(air_telemetry_cont,back_event_handler,LV_EVENT_KEY,NULL); - - air_actions_cont = create_text(section, LV_SYMBOL_PLAY, "Actions", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,air_actions_cont); - lv_menu_set_load_page_event(menu, air_actions_cont, sub_air_actions_page); - lv_obj_add_event_cb(air_actions_cont,back_event_handler,LV_EVENT_KEY,NULL); - - create_text(root_page, NULL, "GS Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - section = lv_menu_section_create(root_page); - lv_obj_add_style(section, &style_openipc_section, 0); - - gs_dvr_cont = create_text(section, LV_SYMBOL_VIDEO, "DVR-Player", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_dvr_cont); - lv_menu_set_load_page_event(menu, gs_dvr_cont, sub_gs_dvr_page); - lv_obj_add_event_cb(gs_dvr_cont,back_event_handler,LV_EVENT_KEY,NULL); - - gs_wfbng_cont = create_text(section, LV_SYMBOL_WIFI, "WFB-NG", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_wfbng_cont); - lv_menu_set_load_page_event(menu, gs_wfbng_cont, sub_gs_wfbng_page); - lv_obj_add_event_cb(gs_wfbng_cont,back_event_handler,LV_EVENT_KEY,NULL); - - gs_apfpv_cont = create_text(section, LV_SYMBOL_WIFI, "APFPV", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_apfpv_cont); - lv_menu_set_load_page_event(menu, gs_apfpv_cont, sub_gs_apfpv_page); - lv_obj_add_event_cb(gs_apfpv_cont,back_event_handler,LV_EVENT_KEY,NULL); - - gs_system_cont = create_text(section, LV_SYMBOL_SETTINGS, "System Settings", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_system_cont); - lv_menu_set_load_page_event(menu, gs_system_cont, sub_gs_system_page); - lv_obj_add_event_cb(gs_system_cont,back_event_handler,LV_EVENT_KEY,NULL); - - gs_wlan_cont = create_text(section, LV_SYMBOL_WIFI, "WiFi", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_wlan_cont); - lv_menu_set_load_page_event(menu, gs_wlan_cont, sub_wlan_page); - lv_obj_add_event_cb(gs_wlan_cont,back_event_handler,LV_EVENT_KEY,NULL); - - gs_actions_cont = create_text(section, LV_SYMBOL_PLAY, "Actions", NULL, NULL, false, LV_MENU_ITEM_BUILDER_VARIANT_1); - lv_group_add_obj(main_group,gs_actions_cont); - lv_menu_set_load_page_event(menu, gs_actions_cont, sub_gs_actions_page); - lv_obj_add_event_cb(gs_actions_cont,back_event_handler,LV_EVENT_KEY,NULL); - - lv_menu_set_sidebar_page(menu, root_page); - lv_menu_set_page(menu,sub_gs_main_page); - lv_menu_clear_history(menu); - - lv_timer_t * timer = lv_timer_create(check_connection_timer, 500, NULL); - last_value = gtotal_tunnel_data; - setenv("GSMENU_VTX_DETECTED" , "0", 1); - - gsmenu_toggle_rxmode(); - - lv_group_set_default(default_group); - return menu; -} - -static void back_event_handler(lv_event_t * e) -{ - lv_key_t key = lv_event_get_key(e); - if (key == LV_KEY_HOME) { - printf("Go Back\n"); - lv_menu_set_page(menu,NULL); - lv_menu_set_page(menu,sub_gs_main_page); - lv_obj_remove_state(air_wfbng_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_alink_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_aalink_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_camera_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_telemetry_cont, LV_STATE_CHECKED); - lv_obj_remove_state(air_actions_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_dvr_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_wfbng_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_apfpv_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_system_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_wlan_cont, LV_STATE_CHECKED); - lv_obj_remove_state(gs_actions_cont, LV_STATE_CHECKED); - lv_screen_load(pp_osd_screen); - lv_indev_set_group(indev_drv,osd_group); - menu_active = false; - } -} diff --git a/src/gsmenu/ui.h b/src/gsmenu/ui.h deleted file mode 100644 index 44683f29..00000000 --- a/src/gsmenu/ui.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "../../lvgl/lvgl.h" -#include "ui.h" - -typedef void (*ReloadFunc)(lv_obj_t * page, lv_obj_t * target); - -typedef struct { - const char *caption; - lv_obj_t *target; - ReloadFunc reload; -} PageEntry; - -typedef struct { - char type[100]; - char page[100]; - void (*page_load_callback)(lv_obj_t * page); - lv_group_t *indev_group; - size_t entry_count; - PageEntry *page_entries; -} menu_page_data_t; - -lv_obj_t * pp_header_create(lv_obj_t * screen); -lv_obj_t * pp_menu_create(lv_obj_t * screen); diff --git a/src/input.cpp b/src/input.cpp index c0254775..1b89c21a 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -13,7 +13,7 @@ #include "main.h" #include "lvgl/lvgl.h" #include "input.h" -#include "gsmenu/gs_system.h" +#include "menu.h" extern YAML::Node config; extern lv_group_t *main_group; @@ -540,6 +540,12 @@ void handle_keyboard_input(void) { else timer = lv_timer_create(simulate_traffic, 50, NULL); break; +#endif +#ifdef USE_SIMULATOR + case 'r': + case 'R': + toggle_rec_enabled(); /* simulate the hardware record button (sim only) */ + break; #endif case 'q': case 'Q': diff --git a/src/main.cpp b/src/main.cpp index 6f77bbc8..282dac17 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,9 +59,6 @@ extern "C" { #include "pixelpilot_config.h" #include #include "WiFiRSSIMonitor.hpp" -#include "gsmenu/gs_system.h" -#include "gsmenu/air_actions.h" -#include "gsmenu/gs_actions.h" #include "menu.h" @@ -644,6 +641,13 @@ extern "C" { osd_publish_bool_fact("dvr.recording", NULL, 0, false); } + /* C-callable wrapper so the menu (C) can set the raw DVR framerate without + * touching the C++ Dvr* directly. */ + void dvr_set_video_framerate(Dvr* dvr, int f); /* defined in dvr.cpp */ + void dvr_set_raw_fps(int fps) { + if (dvr_raw) dvr_set_video_framerate(dvr_raw, fps); + } + // Switch DVR mode at runtime. Stops any active recording. // mode: 0=raw, 1=reencode, 2=both void dvr_set_mode(int mode) { diff --git a/src/menu.c b/src/menu.c index 8fc4aba7..91315b2a 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1,34 +1,133 @@ #include +#include #include #include "lvgl/lvgl.h" #include "input.h" #include "menu.h" -#include "gsmenu/ui.h" +#include "gsmenu/helper.h" /* find_resource_file() */ #include "gsmenu/styles.h" #include "gsmenu/gs_dvrplayer.h" #include "gsmenu/air_txprofiles.h" +#include "gsmenu/colmenu.h" +#include "gsmenu/colmenu_pages.h" +#include "gsmenu/gs_connection_checker.h" /* update_network_status() */ #include "lvosd.h" +/* Focused by input.cpp's toggle_screen() to open the menu. Was in the old ui.c. */ +lv_group_t * main_group; +extern lv_group_t * osd_group; +extern bool menu_active; +extern uint64_t gtotal_tunnel_data; /* tunnel bytes from the VTX (drone) */ + +/* WFB vs APFPV receiver mode. Owned here (not in the old menu's gs_system.c) so + * it outlives the old menu. */ +enum RXMode RXMODE = WFB; + +/* DVR backend (defined in main.cpp / dvr.cpp). */ +extern int dvr_enabled; +void dvr_start_all(void); +void dvr_stop_all(void); + +/* Toggle DVR recording (hardware button, from input.cpp). Formerly poked an + * lv_switch that only the old menu created — which was NULL under the column + * menu — so drive the recorder directly instead. */ +void toggle_rec_enabled(void) +{ +#ifndef USE_SIMULATOR + if(dvr_enabled) dvr_stop_all(); + else dvr_start_all(); +#else + dvr_enabled = !dvr_enabled; + printf("toggle_rec_enabled -> %s\n", dvr_enabled ? "recording" : "stopped"); +#endif + /* keep the menu's "Enabled" switch in sync if the DVR page is open */ + colmenu_reflect_switch("rec_enabled", dvr_enabled != 0); +} + +/* Return the current recording state (runtime, not config). Called by colmenu to + * sync the Display page's Enabled switch on page open. */ +int menu_is_recording(void) +{ + return dvr_enabled; +} + +/* Drone detection: watch the tunnel-data counter; when it's flowing the drone + * is present and its pages become active, otherwise they grey out. Mirrors the + * old ui.c:check_connection_timer, but drives the column menu. + * + * Two modes: in WFB the WFB CLI thread fills gtotal_tunnel_data from tunnel + * traffic; in APFPV nothing does, so we refresh it here from the wlx wifi + * interface's RX bytes (update_network_status), same as the old timer. */ +static void drone_detect_timer(lv_timer_t * t) +{ + (void)t; + static uint64_t last_value = 0; + static uint32_t last_increase = 0; + static bool detected = false; + + if(RXMODE == APFPV) update_network_status(); + uint64_t cur = gtotal_tunnel_data; + bool now = detected; + if(cur > last_value) { last_increase = lv_tick_get(); now = true; } + else if(cur < last_value) { now = false; } /* reset/wrap */ + else if(detected && lv_tick_elaps(last_increase) > 2000){ now = false; } /* timeout */ + last_value = cur; + + if(now != detected) { + detected = now; + setenv("GSMENU_VTX_DETECTED", detected ? "1" : "0", 1); + colmenu_set_drone_detected(detected); + } +} + lv_obj_t * menu; lv_indev_t * indev_drv; lv_group_t * default_group; -lv_obj_t * pp_menu_screen; +lv_obj_t * pp_menu_screen; lv_obj_t * pp_osd_screen; lv_obj_t * dvr_screen; lv_obj_t * txprofiles_screen; +/* Back at the root column closes the menu (was ui.c:back_event_handler). */ +static void colmenu_return_to_osd(void * ctx) +{ + (void)ctx; + lv_screen_load(pp_osd_screen); + lv_indev_set_group(indev_drv, osd_group); + menu_active = false; +} + /** - * PP Main Menu + * PP Main Menu — the cascading column menu (colmenu). toggle_screen() in + * input.cpp loads pp_menu_screen and focuses main_group to open it. */ + +/* The OpenIPC header/logo bar at the top of the menu screen (was ui.c). */ +static lv_obj_t * pp_header_create(lv_obj_t * screen) { + lv_obj_t *header = lv_obj_create(screen); + lv_obj_set_pos(header, 0, 0); + lv_obj_set_size(header, LV_PCT(100), LV_PCT(20)); + lv_obj_set_style_bg_color(header, lv_color_hex(0x0b0e14), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(header, 210, LV_PART_MAIN | LV_STATE_DEFAULT); + /* thin accent line along the bottom edge for a modern header */ + lv_obj_set_style_border_side(header, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(header, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(header, lv_color_hex(0x4c60d8), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(header, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *obj = lv_img_create(header); + lv_image_set_src(obj, find_resource_file("OpenIPC__OPENIPC_logo_white.png")); + lv_image_set_inner_align(obj, LV_IMAGE_ALIGN_CENTER); + lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100)); + lv_image_set_scale(obj, lv_disp_get_ver_res(NULL) / 30); + return header; +} + void pp_menu_main(void) { - style_init(); - // create_virtual_keyboard indev_drv = create_virtual_keyboard(); - - // Create an input group default_group = lv_group_create(); lv_group_set_default(default_group); lv_indev_set_group(indev_drv, default_group); @@ -36,22 +135,25 @@ void pp_menu_main(void) pp_menu_screen = lv_obj_create(NULL); lv_obj_set_style_bg_opa(pp_menu_screen, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_clear_flag(pp_menu_screen, LV_OBJ_FLAG_SCROLLABLE); + pp_header_create(pp_menu_screen); - lv_obj_t * menu_cont = lv_obj_create(pp_menu_screen); - lv_obj_set_size(menu_cont,lv_obj_get_width(pp_menu_screen) / 4 * 3, - lv_obj_get_height(pp_menu_screen)/ 4 * 3); - lv_obj_center(menu_cont); - lv_obj_set_style_border_side(menu_cont, LV_BORDER_SIDE_NONE, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(menu_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(menu_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(menu_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(menu_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(menu_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_t * cm_cont = lv_obj_create(pp_menu_screen); + lv_obj_set_pos(cm_cont, 0, LV_PCT(20)); + lv_obj_set_size(cm_cont, LV_PCT(100), LV_PCT(80)); + lv_obj_set_style_bg_opa(cm_cont, LV_OPA_TRANSP, LV_PART_MAIN); + lv_obj_set_style_border_width(cm_cont, 0, 0); + lv_obj_set_style_pad_all(cm_cont, 0, 0); - pp_header_create(menu_cont); + colmenu_init(cm_cont); + colmenu_show(colmenu_pages_root()); + main_group = colmenu_root_group(); + colmenu_set_root_back(colmenu_return_to_osd, NULL); - pp_menu_create(menu_cont); + /* drone pages start greyed until the VTX is detected */ + setenv("GSMENU_VTX_DETECTED", "0", 1); + lv_timer_create(drone_detect_timer, 500, NULL); + /* Separate device screens the menu launches (DVR player, TX profiles). */ dvr_screen = lv_obj_create(NULL); lv_obj_set_style_bg_opa(dvr_screen, LV_OPA_TRANSP, LV_PART_MAIN); dvr_player_screen_init(); @@ -61,6 +163,5 @@ void pp_menu_main(void) create_table(txprofiles_screen); pp_osd_main(); - lv_screen_load(pp_osd_screen); -} \ No newline at end of file +} diff --git a/src/menu.h b/src/menu.h index 76f27a57..80939b61 100644 --- a/src/menu.h +++ b/src/menu.h @@ -13,4 +13,14 @@ typedef struct { char action[MAX_ACTION_LEN]; } MenuAction; +/* Receiver mode (WFB vs APFPV). Defined in menu.c. */ +enum RXMode { WFB, APFPV }; +extern enum RXMode RXMODE; + +/* Toggle DVR recording (hardware button). Defined in menu.c. */ +void toggle_rec_enabled(void); + +/* Query current recording state (for menu sync). */ +int menu_is_recording(void); + void pp_menu_main(void); \ No newline at end of file diff --git a/src/osd.cpp b/src/osd.cpp index c47eb906..8a1dd68f 100644 --- a/src/osd.cpp +++ b/src/osd.cpp @@ -1967,34 +1967,45 @@ cairo_surface_t * surface_from_embedded_png(const char * png, size_t length) } +/* LVGL renders into this cached (normal RAM) shadow buffer instead of the + * write-combined DRM dumb buffers. Alpha blending is read-modify-write, and + * uncached reads on ARM are extremely slow — rendering directly into the dumb + * buffers is what made the menu sluggish. The shadow always holds the complete + * current UI; my_flush_cb copies it to the off-screen DRM buffer and flips. */ +static uint8_t * lvgl_shadow; + void my_flush_cb(lv_display_t * display, const lv_area_t * area, uint8_t * px_map) { + (void)area; (void)px_map; + + /* Direct mode calls flush once per invalidated area. Acting on the + * intermediate calls used to flip the visible buffer mid-frame — the + * display thread commits on every video frame, so scanout could catch a + * half-rendered frame (rows blinking out during navigation, stale fade + * ghosts). Only publish once the frame is complete. */ + if (!lv_display_flush_is_last(display)) { + lv_display_flush_ready(display); + return; + } - struct modeset_buf *buf1 = &p->out->osd_bufs[0]; - struct modeset_buf *buf2 = &p->out->osd_bufs[1]; - int ret = pthread_mutex_lock(&osd_mutex); - assert(!ret); - if (px_map == buf1->map) { - p->out->osd_buf_switch = 0; - } else if (px_map == buf2->map) { - p->out->osd_buf_switch = 1; - } else { - spdlog::error("Unknown buffer being flushed"); - } + /* osd_buf_switch is only ever flipped on this thread (here and in the + * Cairo OSD path), so the back buffer is stable: copy without the lock. */ + int back = p->out->osd_buf_switch ^ 1; + struct modeset_buf *dst = &p->out->osd_bufs[back]; + memcpy(dst->map, lvgl_shadow, dst->size); + int ret = pthread_mutex_lock(&osd_mutex); + assert(!ret); + p->out->osd_buf_switch = back; if (enable_live_colortrans) { - p->out->osd_bufs[p->out->osd_buf_switch].gl_fb_id = osd_gl_process(&p->out->osd_bufs[p->out->osd_buf_switch], false); // LVGL: straight alpha + dst->gl_fb_id = osd_gl_process(dst, false); // LVGL: straight alpha } - ret = pthread_mutex_unlock(&osd_mutex); assert(!ret); - { - struct modeset_buf *osd_buf = &p->out->osd_bufs[p->out->osd_buf_switch]; - if (dvr_osd && frame_proc) - frame_proc->set_osd_blend(osd_buf->prime_fd, osd_buf->width, osd_buf->height, - osd_buf->stride / 4); - } + if (dvr_osd && frame_proc) + frame_proc->set_osd_blend(dst->prime_fd, dst->width, dst->height, + dst->stride / 4); // tell the display thread that we have a update ret = pthread_mutex_lock(&video_mutex); @@ -2017,28 +2028,30 @@ uint32_t my_get_milliseconds() { } lv_display_t * display; -static lv_draw_buf_t lvgl_draw_buf1, lvgl_draw_buf2; +static lv_draw_buf_t lvgl_draw_buf1; void setup_lvgl(osd_thread_params *p) { /* Initialize LVGL. */ lv_init(); - // Get the first two buffers from the OSD buffers struct modeset_buf *buf1 = &p->out->osd_bufs[0]; - struct modeset_buf *buf2 = &p->out->osd_bufs[1]; display = lv_display_create(buf1->width, buf1->height); lv_display_set_color_format(display, LV_COLOR_FORMAT_ARGB8888); - // Pass the actual DRM pitch (buf->stride) so LVGL's row offsets match the - // hardware-aligned scanline stride. lv_display_set_buffers() would recompute - // stride as width*bpp and misalign on non-power-of-2 widths like 1366px. + /* Single cached shadow buffer as the one draw buffer (see my_flush_cb). + * With one buffer LVGL renders in place and never runs its dual-buffer + * dirty-area sync (which read back from uncached DRM memory every frame). + * Keep the DRM pitch (buf->stride) so the shadow's row layout matches the + * hardware-aligned scanline stride and the flush copy is one flat memcpy. */ + size_t shadow_sz = ((size_t)buf1->size + 63) & ~(size_t)63; + lvgl_shadow = (uint8_t *)aligned_alloc(64, shadow_sz); + assert(lvgl_shadow); + memset(lvgl_shadow, 0, shadow_sz); lv_draw_buf_init(&lvgl_draw_buf1, buf1->width, buf1->height, - LV_COLOR_FORMAT_ARGB8888, buf1->stride, buf1->map, buf1->size); - lv_draw_buf_init(&lvgl_draw_buf2, buf2->width, buf2->height, - LV_COLOR_FORMAT_ARGB8888, buf2->stride, buf2->map, buf2->size); - lv_display_set_draw_buffers(display, &lvgl_draw_buf1, &lvgl_draw_buf2); + LV_COLOR_FORMAT_ARGB8888, buf1->stride, lvgl_shadow, buf1->size); + lv_display_set_draw_buffers(display, &lvgl_draw_buf1, NULL); lv_display_set_render_mode(display, LV_DISPLAY_RENDER_MODE_DIRECT); lv_display_set_flush_cb(display, my_flush_cb); diff --git a/src/simulator.c b/src/simulator.c index 3feac71a..93904836 100644 --- a/src/simulator.c +++ b/src/simulator.c @@ -2,12 +2,12 @@ #include #include #include +#include +#include #include "lvgl/lvgl.h" #include "menu.h" #include "input.h" #include "gsmenu/helper.h" -#include "gsmenu/air_actions.h" -#include "gsmenu/gs_actions.h" int dvr_enabled = 0; @@ -32,16 +32,36 @@ int dvr_reenc_get_bitrate(void) { return 8000; } int dvr_reenc_get_codec(void) { return 0; } int dvr_reenc_get_resolution(void) { return 1; } int dvr_get_max_size(void) { return 4000; } -bool restream_get_enabled() { return false; } -void restream_scan_clients(char* buf, size_t buf_len) { return; } -const char* restream_get_manual_ip() { return ""; } void my_log_cb(lv_log_level_t level, const char * buf) { printf("%s",buf); } +// Simulator stubs for restream API (real impl lives in gstrtpreceiver.cpp, +// which is not part of the simulator build) +bool restream_get_enabled() { return false; } +void restream_set_enabled(bool enabled) { (void)enabled; } +void restream_scan_clients(char* buf, size_t buf_len) { if (buf && buf_len) buf[0] = '\0'; } +const char* restream_get_manual_ip() { return ""; } +void restream_set_manual_ip(const char* ip) { (void)ip; } + int main(int argc, char **argv) { + /* The menu shells out to `gsmenu.sh` by bare name via popen/system, so it + * must be on PATH. In the sim the script lives in the working directory + * (the repo root), and sudo's secure_path drops any PATH sim.sh exports — + * so prepend the CWD to PATH here, inside the process, where popen/system + * children inherit it. */ + { + char cwd[1024]; + const char * old = getenv("PATH"); + if (getcwd(cwd, sizeof(cwd))) { + char newpath[2048]; + snprintf(newpath, sizeof(newpath), "%s:%s", cwd, old ? old : ""); + setenv("PATH", newpath, 1); + } + } + lv_init(); lv_disp_t * disp = lv_sdl_window_create(1920,1080); diff --git a/src/wfbcli.cpp b/src/wfbcli.cpp index 4992dc34..dd93f0f8 100644 --- a/src/wfbcli.cpp +++ b/src/wfbcli.cpp @@ -20,7 +20,7 @@ #include #include "spdlog/spdlog.h" -#include "gsmenu/gs_system.h" +#include "menu.h" #include "wfbcli.hpp" extern "C" { #include "osd.h"