From c65db46baddfa3ed3dfa0c183548957c55742f47 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 11 Aug 2026 22:12:25 +0200 Subject: [PATCH] Add side-key action picker (F + hold side key) --- App/CMakeLists.txt | 6 ++ App/app/action.c | 159 +++++++++++++++++++++++++++++++++++---------- App/app/action.h | 7 ++ App/app/app.c | 34 +++++++++- App/app/main.c | 10 +++ App/ui/main.c | 39 +++++++++++ CMakePresets.json | 2 + 7 files changed, 221 insertions(+), 36 deletions(-) diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 62221df6..3cddbf7f 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -214,6 +214,12 @@ enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT app/foxhunt.c ) +enable_feature(ENABLE_FEAT_F4HWN_ACTION_PICKER) + +if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN) + message(FATAL_ERROR "ENABLE_FEAT_F4HWN_ACTION_PICKER requires ENABLE_FEAT_F4HWN.") +endif() + enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP) enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER) diff --git a/App/app/action.c b/App/app/action.c index 27186e6d..2428b965 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -51,6 +51,9 @@ #ifdef ENABLE_FEAT_F4HWN_FOXHUNT #include "app/foxhunt.h" #endif +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + #include "ui/menu.h" +#endif #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); @@ -299,6 +302,120 @@ void ACTION_SwitchDemodul(void) } +#ifdef ENABLE_FMRADIO +inline static bool ACTION_IsBlockedInFM(uint8_t action) +{ + switch (action) { + case ACTION_OPT_POWER: + case ACTION_OPT_MONITOR: + case ACTION_OPT_A_B: + case ACTION_OPT_VFO_MR: + case ACTION_OPT_SWITCH_DEMODUL: +#ifdef ENABLE_VOX + case ACTION_OPT_VOX: +#endif +#ifdef ENABLE_FEAT_F4HWN + case ACTION_OPT_RXMODE: + case ACTION_OPT_MAINONLY: + case ACTION_OPT_WN: + #ifdef ENABLE_FEAT_F4HWN_AUDIO + case ACTION_OPT_RXA: + #endif + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + case ACTION_OPT_POWER_HIGH: + case ACTION_OPT_REMOVE_OFFSET: + #endif +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + case ACTION_OPT_BEAM: +#endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + case ACTION_OPT_FOXHUNT: +#endif + return true; + + default: + return false; + } +} +#endif + +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER +static void ACTION_Execute(uint8_t action) +{ + if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL) { + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + return; + } + +#ifdef ENABLE_FMRADIO + if (gFmRadioMode && ACTION_IsBlockedInFM(action)) { + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + return; + } +#endif + + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + action_opt_table[action](); +} + +uint8_t gActionPickerKey; +uint8_t gActionPickerSelection[2] = {1, 1}; +uint8_t gActionPickerTimeout_500ms; + +bool ACTION_PickerProcessKey(KEY_Code_t key, bool isPressed, bool isHeld) +{ + if (gActionPickerKey == 0) + return false; + if (isPressed) + gActionPickerTimeout_500ms = ACTION_PICKER_TIMEOUT_500MS; + uint8_t *selection = &gActionPickerSelection[gActionPickerKey - 1]; + + switch (key) { + case KEY_UP: + case KEY_DOWN: + if (isPressed && !isHeld) { + if (key == KEY_UP) { + if (--*selection == 0) + *selection = gSubMenu_SIDEFUNCTIONS_size - 1; + } + else if (++*selection >= gSubMenu_SIDEFUNCTIONS_size) { + *selection = 1; + } + + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + gUpdateDisplay = true; + } + return true; + + case KEY_MENU: + if (!isPressed && !isHeld) { + const uint8_t action = gSubMenu_SIDEFUNCTIONS[*selection].id; + gActionPickerKey = 0; + gUpdateDisplay = true; + ACTION_Execute(action); + } + return true; + + case KEY_EXIT: + case KEY_F: + if (!isPressed) { + gActionPickerKey = 0; + gUpdateDisplay = true; + } + return true; + + case KEY_PTT: + gActionPickerKey = 0; + gUpdateDisplay = true; + return false; + + default: + return true; + } +} +#endif + void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { HideFKeyIcon(); @@ -360,48 +477,20 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } // held or released after short press - +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + ACTION_Execute(func); +#else gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; #ifdef ENABLE_FMRADIO - if (gFmRadioMode) { // do not run these actions in FM radio mode - switch (func) { - case ACTION_OPT_POWER: - case ACTION_OPT_MONITOR: - case ACTION_OPT_A_B: - case ACTION_OPT_VFO_MR: - case ACTION_OPT_SWITCH_DEMODUL: - #ifdef ENABLE_VOX - case ACTION_OPT_VOX: - #endif - #ifdef ENABLE_FEAT_F4HWN - case ACTION_OPT_RXMODE: - case ACTION_OPT_MAINONLY: - case ACTION_OPT_WN: - #ifdef ENABLE_FEAT_F4HWN_AUDIO - case ACTION_OPT_RXA: - #endif - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - case ACTION_OPT_POWER_HIGH: - case ACTION_OPT_REMOVE_OFFSET: - #endif - #endif - #ifdef ENABLE_FEAT_F4HWN_BEAM - case ACTION_OPT_BEAM: - #endif - #ifdef ENABLE_FEAT_F4HWN_FOXHUNT - case ACTION_OPT_FOXHUNT: - #endif - gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; - return; - - default: - break; - } + if (gFmRadioMode && ACTION_IsBlockedInFM(func)) { + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + return; } #endif action_opt_table[func](); +#endif } diff --git a/App/app/action.h b/App/app/action.h index d70c0efd..2684d0e9 100644 --- a/App/app/action.h +++ b/App/app/action.h @@ -55,6 +55,13 @@ void ACTION_SwitchDemodul(void); #endif #endif +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER +#define ACTION_PICKER_TIMEOUT_500MS 10u +extern uint8_t gActionPickerKey; +extern uint8_t gActionPickerSelection[2]; +extern uint8_t gActionPickerTimeout_500ms; +bool ACTION_PickerProcessKey(KEY_Code_t key, bool isPressed, bool isHeld); +#endif void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); #endif diff --git a/App/app/app.c b/App/app/app.c index 92774366..bbe1ffc4 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1611,6 +1611,13 @@ void APP_TimeSlice10ms(void) if (gCurrentFunction != FUNCTION_POWER_SAVE || !gRxIdleMode) CheckRadioInterrupts(); +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + if (gActionPickerKey != 0 && FUNCTION_IsRx()) { + gActionPickerKey = 0; + gUpdateDisplay = true; + } +#endif + if (gCurrentFunction == FUNCTION_TRANSMIT) { // transmitting @@ -1810,6 +1817,15 @@ void cancelUserInputModes(void) void APP_TimeSlice500ms(void) { gNextTimeslice_500ms = false; +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + if (gActionPickerKey != 0 && gActionPickerTimeout_500ms > 0 && + --gActionPickerTimeout_500ms == 0) + { + gActionPickerKey = 0; + gUpdateDisplay = true; + } +#endif + bool exit_menu = false; // Skipped authentic device check @@ -2029,6 +2045,10 @@ void APP_TimeSlice500ms(void) { gEeprom.KEY_LOCK = true; // lock the keyboard gUpdateStatus = true; // lock symbol needs showing +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + gActionPickerKey = 0; + gUpdateDisplay = true; +#endif } if (exit_menu) { @@ -2269,7 +2289,11 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) BACKLIGHT_TurnOn(); } - if (Key == KEY_EXIT && bKeyHeld) { // exit key held pressed + if (Key == KEY_EXIT && bKeyHeld +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + && gActionPickerKey == 0 +#endif + ) { // exit key held pressed // clear the live DTMF decoder if (gDTMF_RX_live[0] != 0) { DTMF_clear_input_box_memory(); @@ -2306,6 +2330,14 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } bool lowBatPopup = gLowBattery && !gLowBatteryConfirmed && gScreenToDisplay == DISPLAY_MAIN; +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + if (gActionPickerKey != 0 && + (gEeprom.KEY_LOCK || lowBatPopup || gScreenToDisplay != DISPLAY_MAIN)) + gActionPickerKey = 0; + if (ACTION_PickerProcessKey(Key, bKeyPressed, bKeyHeld)) + goto Skip; +#endif + #ifdef ENABLE_FEAT_F4HWN // Disable PTT if KEY_LOCK bool lck_condition = (gEeprom.KEY_LOCK || lowBatPopup) && gCurrentFunction != FUNCTION_TRANSMIT; diff --git a/App/app/main.c b/App/app/main.c index f023e186..b44bd4d7 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -455,6 +455,16 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gRequestDisplayScreen = DISPLAY_MAIN; } +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + if (gWasFKeyPressed && (Key == KEY_SIDE1 || Key == KEY_SIDE2)) { + gActionPickerKey = (Key == KEY_SIDE1) ? 1 : 2; + gActionPickerTimeout_500ms = ACTION_PICKER_TIMEOUT_500MS; + gUpdateDisplay = true; + HideFKeyIcon(); + return; + } +#endif + HideFKeyIcon(); processFKeyFunction(Key, true); diff --git a/App/ui/main.c b/App/ui/main.c index 72f6e881..4202d29f 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -18,6 +18,9 @@ #include // abs() #include "app/app.h" +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + #include "app/action.h" +#endif #include "app/chFrScanner.h" #include "app/dtmf.h" @@ -1322,6 +1325,23 @@ static void UI_PrintScanRangeCss(char *String, uint8_t LabelX, uint8_t ValueX, u } #endif +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER +static void UI_PrintActionPickerLabel(uint8_t index, uint8_t line, bool big) +{ + char label[20]; + strcpy(label, gSubMenu_SIDEFUNCTIONS[index].name); + + char *newline = strchr(label, '\n'); + if (newline != NULL) + *newline = ' '; + + if (big) + UI_PrintString(label, 0, LCD_WIDTH, line, 8); + else + UI_PrintStringSmallNormal(label, 0, LCD_WIDTH, line); +} +#endif + void UI_DisplayMain(void) { char String[22]; @@ -1342,6 +1362,25 @@ void UI_DisplayMain(void) return; } +#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER + if (gActionPickerKey != 0) { + const uint8_t selection = gActionPickerSelection[gActionPickerKey - 1]; + uint8_t previous = selection - 1; + uint8_t next = selection + 1; + + if (previous == 0) + previous = gSubMenu_SIDEFUNCTIONS_size - 1; + if (next >= gSubMenu_SIDEFUNCTIONS_size) + next = 1; + + UI_PrintActionPickerLabel(previous, 1, false); + UI_PrintActionPickerLabel(selection, 2, true); + UI_PrintActionPickerLabel(next, 4, false); + ST7565_BlitFullScreen(); + return; + } +#endif + #ifndef ENABLE_FEAT_F4HWN if (gEeprom.KEY_LOCK && gKeypadLocked > 0) { // tell user how to unlock the keyboard diff --git a/CMakePresets.json b/CMakePresets.json index c5a643d6..4bc5baed 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -74,6 +74,7 @@ "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false, "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_ACTION_PICKER": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": false, @@ -210,6 +211,7 @@ "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true, "ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_FOXHUNT": true, + "ENABLE_FEAT_F4HWN_ACTION_PICKER": true, "ENABLE_FEAT_F4HWN_RXTX_LOG": true, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true,