From 0850420fdfd6d56f3b4ac38742769ba377f246f4 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 30 Aug 2026 23:12:27 +0200 Subject: [PATCH] Decouple FoxHunt and Beacon actions --- App/CMakeLists.txt | 8 +- App/app/action.c | 63 ++++++++++- App/app/app.c | 2 +- App/app/foxhunt.c | 222 +++++++++++++++++-------------------- App/app/foxhunt.h | 15 ++- App/apps/app_overlay.c | 54 +++++++++ App/apps/app_overlay.h | 7 ++ App/bitmaps.c | 2 +- App/bitmaps.h | 2 +- App/driver/eeprom_compat.c | 7 +- App/helper/battery.c | 2 +- App/helper/battery.h | 2 +- App/settings.h | 3 +- App/ui/menu.c | 3 +- App/ui/status.c | 2 +- App/ui/status.h | 2 +- CMakePresets.json | 4 + README.md | 5 +- 18 files changed, 258 insertions(+), 147 deletions(-) diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index cc4e029e..d3b6dd30 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -234,9 +234,11 @@ enable_feature(ENABLE_FEAT_F4HWN_BEAM enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG app/rxtx_log.c ) -enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT - app/foxhunt.c -) +enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT) +enable_feature(ENABLE_FEAT_F4HWN_BEACON) +if(ENABLE_FEAT_F4HWN_FOXHUNT OR ENABLE_FEAT_F4HWN_BEACON) + target_sources(App INTERFACE app/foxhunt.c) +endif() enable_feature(ENABLE_FEAT_F4HWN_ACTION_PICKER) if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN) diff --git a/App/app/action.c b/App/app/action.c index 5ed05887..9462bd7f 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -51,7 +51,7 @@ #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG #include "app/rxtx_log.h" #endif -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) #include "app/foxhunt.h" #endif #ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER @@ -119,9 +119,12 @@ void (*const action_opt_table[ACTION_OPT_LEN])(void) = { #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG [ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog, #endif -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) [ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt, #endif +#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) + [ACTION_OPT_BEACON] = &ACTION_Beacon, +#endif }; static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN); @@ -130,10 +133,33 @@ static_assert(ACTION_OPT_BEAM == 19); static_assert(ACTION_OPT_POWER_HIGH == 20); static_assert(ACTION_OPT_REMOVE_OFFSET == 21); static_assert(ACTION_OPT_FOXHUNT == 22); +static_assert(ACTION_OPT_BEACON == 23); bool ACTION_IsAvailable(uint8_t action) { - return action < ACTION_OPT_LEN && action_opt_table[action] != NULL; + if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL) + return false; + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS + switch (action) { +#ifdef ENABLE_FMRADIO + case ACTION_OPT_FM: + return (APP_OverlayShortcutMask() & APP_SHORTCUT_FM) != 0; +#endif +#ifndef ENABLE_FEAT_F4HWN_FOXHUNT + case ACTION_OPT_FOXHUNT: + return (APP_OverlayShortcutMask() & APP_SHORTCUT_FOXHUNT) != 0; +#endif +#ifndef ENABLE_FEAT_F4HWN_BEACON + case ACTION_OPT_BEACON: + return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEACON) != 0; +#endif + default: + break; + } +#endif + + return true; } void ACTION_Power(void) @@ -315,8 +341,11 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action) #ifdef ENABLE_FEAT_F4HWN_BEAM case ACTION_OPT_BEAM: #endif -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) case ACTION_OPT_FOXHUNT: +#endif +#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) + case ACTION_OPT_BEACON: #endif return true; @@ -466,6 +495,32 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) ACTION_Execute(func); } +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +void ACTION_FoxHunt(void) +{ +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + APP_RunFoxHunt(); + GUI_SelectNextDisplay(DISPLAY_MAIN); +#else + if (APP_LaunchOverlayByName("FoxHunt") != APP_OK) + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; +#endif +} +#endif + +#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +void ACTION_Beacon(void) +{ +#ifdef ENABLE_FEAT_F4HWN_BEACON + APP_RunBeacon(); + GUI_SelectNextDisplay(DISPLAY_MAIN); +#else + if (APP_LaunchOverlayByName("Beacon") != APP_OK) + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; +#endif +} +#endif + #ifdef ENABLE_FMRADIO void ACTION_FM(void) diff --git a/App/app/app.c b/App/app/app.c index 28282d8e..fd64c7fa 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1985,7 +1985,7 @@ void APP_TimeSlice500ms(void) if ((gBatteryCheckCounter & 1) == 0) { -#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) +#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) BATTERY_Sample(true); #else BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent); diff --git a/App/app/foxhunt.c b/App/app/foxhunt.c index 20e7be20..17dfe0c7 100644 --- a/App/app/foxhunt.c +++ b/App/app/foxhunt.c @@ -16,7 +16,7 @@ #include "app/foxhunt.h" -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) #if defined(ENABLE_UART) || defined(ENABLE_USB) #include "app/uart.h" @@ -86,7 +86,7 @@ #define FOXHUNT_AUDIO_BEEP 1 #define FOXHUNT_AUDIO_STATION 2 -// --- Beacon (fox) sub-mode --------------------------------------------------- +// --- Beacon (fox) app -------------------------------------------------------- // Turns the radio into the hidden transmitter: each cycle keys up on the TX VFO and // repeats the CW fox identifier (MOE..MO5, or " MOE") in Morse for the TX window, // then stays silent for the (adjustable) idle gap. The carrier stays up during the @@ -201,8 +201,7 @@ static const uint8_t FOXHUNT_MORSE_DIGIT[10] = { // Trailing character of each fox id: MOE / MOI / MOS / MOH / MO5 (index = FOXHUNT_FOX_*). static const char FOXHUNT_FOX_TAIL[5] = {'E', 'I', 'S', 'H', '5'}; -// Beacon sub-mode state. -static bool foxBeacon; // false = hunt (RX), true = beacon (TX) +// Beacon state. static bool beaconPhaseTx; // true = transmit this tick, false = idle gap static uint8_t beaconIdle; // configured silence between IDs (s) static uint8_t beaconIdleLeft; // seconds left in the current silence @@ -782,11 +781,6 @@ static void FOXHUNT_HandleKeys(void) minDbm = curDbm; trendRef = curDbm; break; - case KEY_SIDE1: - case KEY_SIDE2: - // Same shortcut that opened Fox Hunt now toggles to the beacon. - FOXHUNT_EnterBeacon(); - break; default: break; } @@ -794,22 +788,18 @@ static void FOXHUNT_HandleKeys(void) gWasFKeyPressed = false; // any non-F key consumes (or cancels) the reverse arm } -// (Re)enter the hunt (RX) sub-mode: fixed front-end gain, attenuator and audio mode -// re-applied, peak/trend reset. Called at start-up and when leaving the beacon. The -// audio mode is a hunt setting, so it is (re)applied here, not forced off — it must -// survive a trip through the beacon. +// Enter Fox Hunt with fixed front-end gain, the persisted attenuator/audio mode, +// and freshly rebased level tracking. static void FOXHUNT_EnterHunt(void) { - foxBeacon = false; - BK4819_SetAGC(false); FOXHUNT_ApplyAtt(); FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station // Let the RSSI settle to the gain just applied, then reset the peak/min hold, trend - // reference and signal history onto a fresh reading (also re-primes the sparkline on - // return from the beacon). Same settle as FOXHUNT_AttCycle: without it a restored - // BYP/BYP+ step at start-up or on beacon return would rebase onto the old-gain reading. + // reference and signal history onto a fresh reading. Same settle as + // FOXHUNT_AttCycle: without it a restored + // BYP/BYP+ step at start-up would rebase onto the old-gain reading. // SetAudio already waits when audio is on, but not in the default audio-off case. SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS); FOXHUNT_RebaseMeasurements(); @@ -836,9 +826,8 @@ static VfoState_t FOXHUNT_TxState(void) // Refuse feedback shown when a burst is barred: reuse the beacon screen layout and // the radio's own state label (VfoStateStr, e.g. "TX DISABLE" / "BAT LOW" / "VOLT -// HIGH"), same font as the main screen, for a beat; the caller then falls back to -// the hunt. The RX front-end and audio path are left untouched so the hunt keeps -// reading cleanly. +// HIGH"), same font as the main screen, for a beat; the caller then waits through +// a fresh idle gap before checking again. static void FOXHUNT_TxDeniedNotice(VfoState_t state) { // Frame (clear + BEACON tag + battery + barred TX frequency). @@ -855,12 +844,10 @@ static void FOXHUNT_TxDeniedNotice(VfoState_t state) FOXHUNT_TickDelay(); } -// Enter the beacon (fox) sub-mode. The TX gates are enforced per burst in -// FOXHUNT_BeaconTick (which runs immediately), so entry just arms the state. +// Enter the Beacon app. TX gates are enforced before each burst. static void FOXHUNT_EnterBeacon(void) { - foxBeacon = true; - AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt + AUDIO_AudioPathOff(); gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up beaconPhaseTx = true; } @@ -879,8 +866,8 @@ static void FOXHUNT_DrawTxSeconds(void) } // Sleep in short slices while watching the keypad, so a burst stays interactive and the -// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Fox Hunt and a -// side key aborts back to the hunt (both return true). The 1/2/3 keys edit the beacon +// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Beacon. The +// 1/2/3 keys edit the beacon // settings live, exactly as in the idle phase: the TX window (1) and idle gap (2) take // effect at the upcoming window / gap, and a FOX change (3) at the next repeat (the // message is rebuilt there, never under the running loop). @@ -926,17 +913,12 @@ static bool FOXHUNT_TxDelay(uint16_t ms) continue; if (kbd.current == KEY_EXIT) { - foxRunning = false; // abort the burst and leave Fox Hunt - return true; - } - if (kbd.current == KEY_SIDE1 || kbd.current == KEY_SIDE2) { - foxBeacon = false; // abort the burst and switch back to hunt + foxRunning = false; // abort the burst and leave Beacon return true; } if (kbd.current == KEY_MENU) { - // Cut the transmission short but stay in Beacon: aborting the burst without - // clearing foxBeacon/foxRunning drops the loop straight into a fresh idle - // gap of the configured IDLE length. + // Cut the transmission short but stay in Beacon: the loop drops straight + // into a fresh idle gap of the configured IDLE length. return true; } if (kbd.current == KEY_F) { // arm / disarm the reverse step @@ -1051,8 +1033,8 @@ static void FOXHUNT_BeaconTransmit(void) stop = FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 7); } - // Key down: mute, drop the PA and restore RX for the silent gap. If the burst - // was interrupted, FOXHUNT_TxDelay already set the target mode (quit or hunt). + // Key down: mute, drop the PA and restore RX for the silent gap. If EXIT + // interrupted the burst, FOXHUNT_TxDelay has already stopped the app. BK4819_EnterTxMute(); BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); RADIO_SetupRegisters(true); @@ -1208,11 +1190,7 @@ static void FOXHUNT_BeaconKeys(void) switch (kbd.current) { case KEY_EXIT: - foxRunning = false; // leave Fox Hunt entirely - break; - case KEY_SIDE1: - case KEY_SIDE2: - foxBeacon = false; // toggle back to the hunt + foxRunning = false; break; case KEY_MENU: // Restart a full idle interval now (the TX-phase M drops here too). @@ -1237,28 +1215,26 @@ static void FOXHUNT_BeaconTick(void) // LOCK / TX LOCK and modulation are fixed for the session, but the battery // is re-sampled over the idle gaps (FOXHUNT_IdleHousekeeping), so a beacon // left running stops keying up once the pack falls low or over-voltage - // instead of transmitting blind. Refuse, notify, and drop back to hunt. + // instead of transmitting blind. Refuse, notify, then retry after an idle gap. VfoState_t state = FOXHUNT_TxState(); if (state != VFO_STATE_NORMAL) { FOXHUNT_TxDeniedNotice(state); - foxBeacon = false; beaconPhaseTx = false; - FOXHUNT_EnterHunt(); + beaconIdleLeft = beaconIdle; + beaconIdleTick = 0; return; } // The burst repeats the ID for the whole TX window, redrawing the screen - // itself each repeat (remaining-window countdown); it may clear foxBeacon - // (side-key abort) or foxRunning (EXIT). + // itself each repeat (remaining-window countdown); EXIT may clear + // foxRunning and abort it. FOXHUNT_BeaconTransmit(); beaconPhaseTx = false; beaconIdleLeft = beaconIdle; beaconIdleTick = 0; - if (foxRunning && !foxBeacon) // side-key switched to hunt - FOXHUNT_EnterHunt(); - return; // EXIT (quit) falls through + return; } // Idle (silent) phase: responsive, editable countdown. @@ -1267,12 +1243,8 @@ static void FOXHUNT_BeaconTick(void) #endif FOXHUNT_BeaconKeys(); - if (!foxRunning) // EXIT: leave Fox Hunt + if (!foxRunning) return; - if (!foxBeacon) { // side-key: back to the hunt - FOXHUNT_EnterHunt(); - return; - } FOXHUNT_BeaconDraw(false, beaconIdleLeft); FOXHUNT_BlitScreen(); @@ -1340,7 +1312,7 @@ static void FOXHUNT_LoadConfig(void) // Write the settings to flash, but only when they changed since the last write. // Called on the 500 ms tick (so any change persists within ~0.5 s and survives a -// power-off, not just a clean EXIT) and once more on leaving Fox Hunt. The RAM +// power-off, not just a clean EXIT) and once more on leaving either app. The RAM // compare avoids re-reading the 4 KB sector on every quiet tick. static void FOXHUNT_SaveConfig(void) { @@ -1354,83 +1326,90 @@ static void FOXHUNT_SaveConfig(void) memcpy(foxCfgSaved, cfg, sizeof(cfg)); } -void APP_RunFoxHunt(void) +static void FOXHUNT_Begin(void) { - // Finish any pending backlight fade, then start with the screen on and a full - // BLTime window (BACKLIGHT_TurnOn also re-arms the sleep countdown). BACKLIGHT_UpdateTickless(); BACKLIGHT_TurnOn(); - // Hunt on the user-selected VFO: dual-watch / cross-band may have left the - // radio listening on the other VFO, so force RX on the selected one and retune. + // Both apps operate on the user-selected VFO. Dual watch may have left the + // receiver on the other one, so establish a deterministic starting point. gEeprom.RX_VFO = gEeprom.TX_VFO; gRxVfo = gTxVfo; gCurrentVfo = gTxVfo; RADIO_SetupRegisters(true); - // Beacon defaults (LoadConfig may override fox / tx / idle below). - beaconIdle = FOXHUNT_BEACON_IDLE_DEF; - beaconTx = FOXHUNT_BEACON_TX_DEF; - foxFox = FOXHUNT_FOX_CALL; // identified beacon by default (legal on ham) - beaconPhaseTx = false; - - // Read the callsign from the boot message (line 1), sanitised to what Morse can - // send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE). - { - char call[FOXHUNT_CALLSIGN_MAX + 1]; - uint8_t n = 0; - PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX); - call[FOXHUNT_CALLSIGN_MAX] = '\0'; - for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) { - char c = call[i]; - if (c == '\0' || (uint8_t)c == 0xFF) - break; - if (c >= 'a' && c <= 'z') - c -= 32; - if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') - foxCall[n++] = c; - } - foxCall[n] = '\0'; - } - - // Start in the hunt (RX) sub-mode, S-meter staircase gauge by default, then restore - // any persisted settings (attenuator, gauge, audio, beacon idle / fox / tx). + // Initialise the complete legacy seven-byte configuration before loading it. + // Keeping the layout unchanged preserves existing FieldOps settings and lets + // either independently compiled app update its fields without a migration. attStep = 0; foxGraphMode = FOXHUNT_GRAPH_BAR; foxAudioMode = FOXHUNT_AUDIO_OFF; - FOXHUNT_LoadConfig(); // may override att / gauge / audio / idle / fox / tx - FOXHUNT_EnterHunt(); // applies the restored attStep and audio mode - // beaconMsg is (re)built by FOXHUNT_BeaconTransmit before every burst, and nothing - // reads it before the first burst, so no need to assemble it here. + beaconIdle = FOXHUNT_BEACON_IDLE_DEF; + beaconTx = FOXHUNT_BEACON_TX_DEF; + foxFox = FOXHUNT_FOX_CALL; + beaconPhaseTx = false; + FOXHUNT_LoadConfig(); - // Prime the key state with whatever is held right now, so the side key that - // launched Fox Hunt (still down on a long-press assignment) is not taken for - // a fresh press and does not immediately toggle to the beacon. + // Ignore the key that launched the modal app until it has been released. kbd.prev = kbd.current = KEYBOARD_GetKey(); - gWasFKeyPressed = false; // start with the reverse-step arm cleared - - // Always enter unlocked: the keypad lock is a transient safety toggle, not a - // persisted setting, so a previous session must not leave the pad locked. + gWasFKeyPressed = false; foxLocked = false; fHoldMs = 0; fLongDone = false; - foxRunning = true; +} + +static void FOXHUNT_LoadCallsign(void) +{ + char call[FOXHUNT_CALLSIGN_MAX + 1]; + uint8_t n = 0; + + // Read the callsign from the boot message (line 1), sanitised to what Morse can + // send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE). + PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX); + call[FOXHUNT_CALLSIGN_MAX] = '\0'; + for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) { + char c = call[i]; + if (c == '\0' || (uint8_t)c == 0xFF) + break; + if (c >= 'a' && c <= 'z') + c -= 32; + if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') + foxCall[n++] = c; + } + foxCall[n] = '\0'; +} + +static void FOXHUNT_End(void) +{ + FOXHUNT_SaveConfig(); + gWasFKeyPressed = false; + AUDIO_AudioPathOff(); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); +} + +/* Both entry points stay in this translation unit so their small modal services + * can be shared when both features are enabled. The action table references only + * the enabled entry point(s); --gc-sections then drops the complete unused RX or + * Beacon call graph, including its private constants and bitmaps. */ +void APP_RunFoxHunt(void) +{ + FOXHUNT_Begin(); + + FOXHUNT_EnterHunt(); while (foxRunning) { #if defined(ENABLE_UART) || defined(ENABLE_USB) UART_ServiceCommands(); #endif - if (foxBeacon) { - FOXHUNT_BeaconTick(); - continue; - } #ifdef ENABLE_FEAT_F4HWN_K5VIEWER // Keep the K5Viewer link alive and pick up any remote key. K5VIEWER_ParseInput(); #endif FOXHUNT_HandleKeys(); - if (foxBeacon) // just switched to beacon: skip the RX work this tick - continue; + if (!foxRunning) + break; curDbm = FOXHUNT_ReadDbm(); if (curDbm > peakDbm) @@ -1468,24 +1447,23 @@ void APP_RunFoxHunt(void) FOXHUNT_TickDelay(); // tick delay + smooth backlight fade } - // Persist the session's settings on the way out (EXIT clears foxRunning) so - // they survive a later power cycle. - FOXHUNT_SaveConfig(); - - gWasFKeyPressed = false; // don't leak a pending reverse-step arm to the main screen - - // Mute any pending tone, drop the PA (safety), then restore the normal VFO - // selection and RX config. - AUDIO_AudioPathOff(); - BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); - RADIO_SelectVfos(); - RADIO_SetupRegisters(true); + FOXHUNT_End(); } -void ACTION_FoxHunt(void) +void APP_RunBeacon(void) { - APP_RunFoxHunt(); - GUI_SelectNextDisplay(DISPLAY_MAIN); + FOXHUNT_Begin(); + FOXHUNT_LoadCallsign(); + FOXHUNT_EnterBeacon(); + + while (foxRunning) { +#if defined(ENABLE_UART) || defined(ENABLE_USB) + UART_ServiceCommands(); +#endif + FOXHUNT_BeaconTick(); + } + + FOXHUNT_End(); } -#endif // ENABLE_FEAT_F4HWN_FOXHUNT +#endif // ENABLE_FEAT_F4HWN_FOXHUNT || ENABLE_FEAT_F4HWN_BEACON diff --git a/App/app/foxhunt.h b/App/app/foxhunt.h index 3743e6b1..a973d319 100644 --- a/App/app/foxhunt.h +++ b/App/app/foxhunt.h @@ -17,7 +17,7 @@ #ifndef APP_FOXHUNT_H #define APP_FOXHUNT_H -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) #include "keyboard_state.h" @@ -46,12 +46,21 @@ #include #include #include +#endif -// Entry point wired as an assignable side-key action (see action.c). +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +// Assignable resident/overlay launchers (see action.c). void ACTION_FoxHunt(void); +void ACTION_Beacon(void); +#endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT // Self-contained modal loop that owns the screen and keypad until EXIT. void APP_RunFoxHunt(void); +#endif + +#ifdef ENABLE_FEAT_F4HWN_BEACON +void APP_RunBeacon(void); +#endif -#endif // ENABLE_FEAT_F4HWN_FOXHUNT #endif // APP_FOXHUNT_H diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c index 650de0f8..6b90d454 100644 --- a/App/apps/app_overlay.c +++ b/App/apps/app_overlay.c @@ -633,8 +633,60 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header) return APP_OK; } +static bool app_shortcuts_cached; +static uint8_t app_shortcut_mask; +static uint8_t app_shortcut_slots[3]; + +static int8_t app_shortcut_index(const char *name) +{ + if (strncmp(name, "Broadcast FM", APP_NAME_LEN) == 0) return 0; + if (strncmp(name, "FoxHunt", APP_NAME_LEN) == 0) return 1; + if (strncmp(name, "Beacon", APP_NAME_LEN) == 0) return 2; + return -1; +} + +static void app_cache_shortcuts(void) +{ + if (app_shortcuts_cached) + return; + + app_shortcut_mask = 0; + const uint32_t overlay_vma = (uint32_t)PY25Q16_OverlayBuffer(); + + for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) { + app_header_t h; + if (APP_ValidateSlot(slot, &h) != APP_OK || h.link_vma != overlay_vma) + continue; + + const int8_t index = app_shortcut_index(h.name); + if (index >= 0) { + const uint8_t bit = (uint8_t)(1u << index); + if (!(app_shortcut_mask & bit)) { + app_shortcut_mask |= bit; + app_shortcut_slots[index] = slot; + } + } + } + + app_shortcuts_cached = true; +} + +uint8_t APP_OverlayShortcutMask(void) +{ + app_cache_shortcuts(); + return app_shortcut_mask; +} + uint8_t APP_LaunchOverlayByName(const char *name) { + const int8_t index = app_shortcut_index(name); + if (index >= 0) { + app_cache_shortcuts(); + return (app_shortcut_mask & (1u << index)) + ? APP_LaunchOverlay(app_shortcut_slots[index]) + : APP_ERR_MAGIC; + } + app_header_t h; for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) { @@ -840,6 +892,7 @@ uint8_t APP_SlotErase(uint8_t slot) for (uint32_t off = 0; off < APP_SLOT_STRIDE; off += APP_SECTOR_SIZE) PY25Q16_SectorErase(base + off); PY25Q16_InvalidateCache(); + app_shortcuts_cached = false; return APP_OK; } @@ -851,6 +904,7 @@ uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32 return APP_ERR_SIZE; PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false); PY25Q16_InvalidateCache(); + app_shortcuts_cached = false; return APP_OK; } diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h index 12465168..e2c2e778 100644 --- a/App/apps/app_overlay.h +++ b/App/apps/app_overlay.h @@ -70,6 +70,10 @@ #define APP_NAME_LEN 16 #define APP_VERSION_LEN 16 +#define APP_SHORTCUT_FM (1u << 0) +#define APP_SHORTCUT_FOXHUNT (1u << 1) +#define APP_SHORTCUT_BEACON (1u << 2) + typedef struct __attribute__((packed)) { uint32_t magic; /* APP_MAGIC */ uint16_t hdr_version; /* APP_HDR_VERSION */ @@ -106,6 +110,9 @@ uint8_t APP_LaunchOverlay(uint8_t slot); /* Find the first valid app with this exact header name and launch it. */ uint8_t APP_LaunchOverlayByName(const char *name); +/* Cached availability of apps exposed as resident quick actions. */ +uint8_t APP_OverlayShortcutMask(void); + /* Host-tool slot management (external flash only, never brick-critical). */ uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header); uint8_t APP_SlotErase(uint8_t slot); diff --git a/App/bitmaps.c b/App/bitmaps.c index 141a7026..21f8cff1 100644 --- a/App/bitmaps.c +++ b/App/bitmaps.c @@ -301,7 +301,7 @@ const uint8_t BITMAP_NOAA[12] = }; #endif -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) const uint8_t BITMAP_FoxHuntSignal[10] = { // point source + the speaker bitmap's two sound waves, oriented horizontally 0b00001000, diff --git a/App/bitmaps.h b/App/bitmaps.h index f64c332e..fba73398 100644 --- a/App/bitmaps.h +++ b/App/bitmaps.h @@ -45,7 +45,7 @@ extern const uint8_t BITMAP_compand[6]; extern const uint8_t BITMAP_NOAA[12]; -#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) extern const uint8_t BITMAP_FoxHuntSignal[10]; extern const uint8_t BITMAP_FoxHuntSpeaker[10]; extern const uint8_t BITMAP_FoxHuntUp[11]; diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index a2a2215b..fd36bb58 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -54,10 +54,9 @@ static const AddrMapping_t ADDR_MAPPINGS[] = { _MK_MAPPING(0x009000, 0x009000, 0x0090E7), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old // 0x90D6 bound was 10 B short: it truncated the // 470 MHz VFO1 record at 0x90D0 -> 0x90DF) - // Fox Hunt settings * 7 Bytes 0x90E0 -> 0x90E7 - // (ENABLE_FEAT_F4HWN_FOXHUNT, written directly by - // app/foxhunt.c; concatenated here so aircopy clones - // them together with the VFOs) + // Fox Hunt / Beacon settings * 7 Bytes 0x90E0 -> 0x90E7 + // (written directly by app/foxhunt.c; concatenated + // here so aircopy clones them with the VFOs) _MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010 // Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020 diff --git a/App/helper/battery.c b/App/helper/battery.c index d3fd2fa9..244078cd 100644 --- a/App/helper/battery.c +++ b/App/helper/battery.c @@ -130,7 +130,7 @@ unsigned int BATTERY_VoltsToPercent(const unsigned int voltage_10mV) return 0; } -#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) +#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) void BATTERY_Sample(const bool bDisplayBatteryLevel) { BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent); diff --git a/App/helper/battery.h b/App/helper/battery.h index 6b290ab5..28fa412e 100644 --- a/App/helper/battery.h +++ b/App/helper/battery.h @@ -45,7 +45,7 @@ typedef enum { unsigned int BATTERY_VoltsToPercent(unsigned int voltage_10mV); -#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) +#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) void BATTERY_Sample(bool bDisplayBatteryLevel); #endif void BATTERY_GetReadings(bool bDisplayBatteryLevel); diff --git a/App/settings.h b/App/settings.h index 22b003ff..4673a56c 100644 --- a/App/settings.h +++ b/App/settings.h @@ -126,8 +126,9 @@ enum ACTION_OPT_t { ACTION_OPT_POWER_HIGH = 20, ACTION_OPT_REMOVE_OFFSET = 21, ACTION_OPT_FOXHUNT = 22, + ACTION_OPT_BEACON = 23, - ACTION_OPT_LEN = 23 + ACTION_OPT_LEN = 24 }; #ifdef ENABLE_VOICE diff --git a/App/ui/menu.c b/App/ui/menu.c index 2e7a0a30..1c355706 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -495,7 +495,8 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = {"BEAM", ACTION_OPT_BEAM}, {"POWER\nHIGH", ACTION_OPT_POWER_HIGH}, {"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET}, - {"FOX HUNT\nBEACON", ACTION_OPT_FOXHUNT}, + {"FOX HUNT", ACTION_OPT_FOXHUNT}, + {"BEACON", ACTION_OPT_BEACON}, }; const uint8_t gSubMenu_SIDEFUNCTIONS_size = ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS); diff --git a/App/ui/status.c b/App/ui/status.c index 62e6e71b..fe24697b 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -54,7 +54,7 @@ static void convertTime(uint8_t *line, uint8_t type) #endif #endif -#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void UI_DrawStatusBattery(uint8_t *line, char *str) #else static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str) diff --git a/App/ui/status.h b/App/ui/status.h index f2e0d875..84bb71fa 100644 --- a/App/ui/status.h +++ b/App/ui/status.h @@ -19,7 +19,7 @@ #include -#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void UI_DrawStatusBattery(uint8_t *line, char *str); #endif void UI_DisplayStatus(); diff --git a/CMakePresets.json b/CMakePresets.json index ebe73235..5c422fd3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -82,6 +82,7 @@ "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false, "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_FEAT_F4HWN_ACTION_PICKER": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, @@ -132,6 +133,7 @@ "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true, "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": true, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true, @@ -155,6 +157,7 @@ "cacheVariables": { "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_FOXHUNT": true, + "ENABLE_FEAT_F4HWN_BEACON": true, "EDITION_STRING": "FieldOps", "TARGET": "f4hwn.fieldops" } @@ -175,6 +178,7 @@ "cacheVariables": { "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_AIRCOPY": true, "ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_GAME": false, diff --git a/README.md b/README.md index b399c058..78ebce31 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Specialized presets extend Fusion for specific uses: - selectable RF attenuation, - silent, Geiger-style and received-audio modes, - long-press `F` keypad lock (attenuation stays adjustable with the arrow keys). -- Integrated Morse Beacon transmitter with: +- Independent Morse Beacon transmitter with: - `MOE`, `MOI`, `MOS`, `MOH`, `MO5` and `MO` identifiers, - optional callsign identification, - configurable TX and idle periods, @@ -264,7 +264,8 @@ Specialized presets extend Fusion for specific uses: - offset removal, - BEAM, - RF Log, - - Fox Hunt / Beacon. + - Fox Hunt, + - Beacon. ### Reliability and optimization