Decouple FoxHunt and Beacon actions

This commit is contained in:
Armel FAUVEAU committed 2026-08-30 23:12:27 +02:00
1 parent 4c3847ef34
commit 0850420fdf
18 files changed
+258 -147

No files matched your search

+5 -3
View File
@@ -234,9 +234,11 @@ enable_feature(ENABLE_FEAT_F4HWN_BEAM
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
app/rxtx_log.c app/rxtx_log.c
) )
enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT)
app/foxhunt.c 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) enable_feature(ENABLE_FEAT_F4HWN_ACTION_PICKER)
if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN) if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN)
+59 -4
View File
@@ -51,7 +51,7 @@
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h" #include "app/rxtx_log.h"
#endif #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" #include "app/foxhunt.h"
#endif #endif
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER #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 #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
[ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog, [ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog,
#endif #endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT #if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
[ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt, [ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt,
#endif #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); 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_POWER_HIGH == 20);
static_assert(ACTION_OPT_REMOVE_OFFSET == 21); static_assert(ACTION_OPT_REMOVE_OFFSET == 21);
static_assert(ACTION_OPT_FOXHUNT == 22); static_assert(ACTION_OPT_FOXHUNT == 22);
static_assert(ACTION_OPT_BEACON == 23);
bool ACTION_IsAvailable(uint8_t action) 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) void ACTION_Power(void)
@@ -315,8 +341,11 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action)
#ifdef ENABLE_FEAT_F4HWN_BEAM #ifdef ENABLE_FEAT_F4HWN_BEAM
case ACTION_OPT_BEAM: case ACTION_OPT_BEAM:
#endif #endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT #if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
case ACTION_OPT_FOXHUNT: case ACTION_OPT_FOXHUNT:
#endif
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
case ACTION_OPT_BEACON:
#endif #endif
return true; return true;
@@ -466,6 +495,32 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
ACTION_Execute(func); 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 #ifdef ENABLE_FMRADIO
void ACTION_FM(void) void ACTION_FM(void)
+1 -1
View File
@@ -1985,7 +1985,7 @@ void APP_TimeSlice500ms(void)
if ((gBatteryCheckCounter & 1) == 0) 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); BATTERY_Sample(true);
#else #else
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent); BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
+100 -122
View File
@@ -16,7 +16,7 @@
#include "app/foxhunt.h" #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) #if defined(ENABLE_UART) || defined(ENABLE_USB)
#include "app/uart.h" #include "app/uart.h"
@@ -86,7 +86,7 @@
#define FOXHUNT_AUDIO_BEEP 1 #define FOXHUNT_AUDIO_BEEP 1
#define FOXHUNT_AUDIO_STATION 2 #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 // Turns the radio into the hidden transmitter: each cycle keys up on the TX VFO and
// repeats the CW fox identifier (MOE..MO5, or "<call> MOE") in Morse for the TX window, // repeats the CW fox identifier (MOE..MO5, or "<call> MOE") in Morse for the TX window,
// then stays silent for the (adjustable) idle gap. The carrier stays up during the // 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_*). // 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'}; static const char FOXHUNT_FOX_TAIL[5] = {'E', 'I', 'S', 'H', '5'};
// Beacon sub-mode state. // Beacon state.
static bool foxBeacon; // false = hunt (RX), true = beacon (TX)
static bool beaconPhaseTx; // true = transmit this tick, false = idle gap static bool beaconPhaseTx; // true = transmit this tick, false = idle gap
static uint8_t beaconIdle; // configured silence between IDs (s) static uint8_t beaconIdle; // configured silence between IDs (s)
static uint8_t beaconIdleLeft; // seconds left in the current silence static uint8_t beaconIdleLeft; // seconds left in the current silence
@@ -782,11 +781,6 @@ static void FOXHUNT_HandleKeys(void)
minDbm = curDbm; minDbm = curDbm;
trendRef = curDbm; trendRef = curDbm;
break; break;
case KEY_SIDE1:
case KEY_SIDE2:
// Same shortcut that opened Fox Hunt now toggles to the beacon.
FOXHUNT_EnterBeacon();
break;
default: default:
break; break;
} }
@@ -794,22 +788,18 @@ static void FOXHUNT_HandleKeys(void)
gWasFKeyPressed = false; // any non-F key consumes (or cancels) the reverse arm 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 // Enter Fox Hunt with fixed front-end gain, the persisted attenuator/audio mode,
// re-applied, peak/trend reset. Called at start-up and when leaving the beacon. The // and freshly rebased level tracking.
// audio mode is a hunt setting, so it is (re)applied here, not forced off — it must
// survive a trip through the beacon.
static void FOXHUNT_EnterHunt(void) static void FOXHUNT_EnterHunt(void)
{ {
foxBeacon = false;
BK4819_SetAGC(false); BK4819_SetAGC(false);
FOXHUNT_ApplyAtt(); FOXHUNT_ApplyAtt();
FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station 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 // 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 // reference and signal history onto a fresh reading. Same settle as
// return from the beacon). Same settle as FOXHUNT_AttCycle: without it a restored // FOXHUNT_AttCycle: without it a restored
// BYP/BYP+ step at start-up or on beacon return would rebase onto the old-gain reading. // 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. // SetAudio already waits when audio is on, but not in the default audio-off case.
SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS); SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS);
FOXHUNT_RebaseMeasurements(); 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 // 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 // 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 // HIGH"), same font as the main screen, for a beat; the caller then waits through
// the hunt. The RX front-end and audio path are left untouched so the hunt keeps // a fresh idle gap before checking again.
// reading cleanly.
static void FOXHUNT_TxDeniedNotice(VfoState_t state) static void FOXHUNT_TxDeniedNotice(VfoState_t state)
{ {
// Frame (clear + BEACON tag + battery + barred TX frequency). // Frame (clear + BEACON tag + battery + barred TX frequency).
@@ -855,12 +844,10 @@ static void FOXHUNT_TxDeniedNotice(VfoState_t state)
FOXHUNT_TickDelay(); FOXHUNT_TickDelay();
} }
// Enter the beacon (fox) sub-mode. The TX gates are enforced per burst in // Enter the Beacon app. TX gates are enforced before each burst.
// FOXHUNT_BeaconTick (which runs immediately), so entry just arms the state.
static void FOXHUNT_EnterBeacon(void) static void FOXHUNT_EnterBeacon(void)
{ {
foxBeacon = true; AUDIO_AudioPathOff();
AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt
gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up
beaconPhaseTx = true; 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 // 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 // TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Beacon. The
// side key aborts back to the hunt (both return true). The 1/2/3 keys edit the beacon // 1/2/3 keys edit the beacon
// settings live, exactly as in the idle phase: the TX window (1) and idle gap (2) take // 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 // 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). // message is rebuilt there, never under the running loop).
@@ -926,17 +913,12 @@ static bool FOXHUNT_TxDelay(uint16_t ms)
continue; continue;
if (kbd.current == KEY_EXIT) { if (kbd.current == KEY_EXIT) {
foxRunning = false; // abort the burst and leave Fox Hunt foxRunning = false; // abort the burst and leave Beacon
return true;
}
if (kbd.current == KEY_SIDE1 || kbd.current == KEY_SIDE2) {
foxBeacon = false; // abort the burst and switch back to hunt
return true; return true;
} }
if (kbd.current == KEY_MENU) { if (kbd.current == KEY_MENU) {
// Cut the transmission short but stay in Beacon: aborting the burst without // Cut the transmission short but stay in Beacon: the loop drops straight
// clearing foxBeacon/foxRunning drops the loop straight into a fresh idle // into a fresh idle gap of the configured IDLE length.
// gap of the configured IDLE length.
return true; return true;
} }
if (kbd.current == KEY_F) { // arm / disarm the reverse step 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); stop = FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 7);
} }
// Key down: mute, drop the PA and restore RX for the silent gap. If the burst // Key down: mute, drop the PA and restore RX for the silent gap. If EXIT
// was interrupted, FOXHUNT_TxDelay already set the target mode (quit or hunt). // interrupted the burst, FOXHUNT_TxDelay has already stopped the app.
BK4819_EnterTxMute(); BK4819_EnterTxMute();
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
RADIO_SetupRegisters(true); RADIO_SetupRegisters(true);
@@ -1208,11 +1190,7 @@ static void FOXHUNT_BeaconKeys(void)
switch (kbd.current) { switch (kbd.current) {
case KEY_EXIT: case KEY_EXIT:
foxRunning = false; // leave Fox Hunt entirely foxRunning = false;
break;
case KEY_SIDE1:
case KEY_SIDE2:
foxBeacon = false; // toggle back to the hunt
break; break;
case KEY_MENU: case KEY_MENU:
// Restart a full idle interval now (the TX-phase M drops here too). // 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 // 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 // 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 // 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(); VfoState_t state = FOXHUNT_TxState();
if (state != VFO_STATE_NORMAL) { if (state != VFO_STATE_NORMAL) {
FOXHUNT_TxDeniedNotice(state); FOXHUNT_TxDeniedNotice(state);
foxBeacon = false;
beaconPhaseTx = false; beaconPhaseTx = false;
FOXHUNT_EnterHunt(); beaconIdleLeft = beaconIdle;
beaconIdleTick = 0;
return; return;
} }
// The burst repeats the ID for the whole TX window, redrawing the screen // The burst repeats the ID for the whole TX window, redrawing the screen
// itself each repeat (remaining-window countdown); it may clear foxBeacon // itself each repeat (remaining-window countdown); EXIT may clear
// (side-key abort) or foxRunning (EXIT). // foxRunning and abort it.
FOXHUNT_BeaconTransmit(); FOXHUNT_BeaconTransmit();
beaconPhaseTx = false; beaconPhaseTx = false;
beaconIdleLeft = beaconIdle; beaconIdleLeft = beaconIdle;
beaconIdleTick = 0; beaconIdleTick = 0;
if (foxRunning && !foxBeacon) // side-key switched to hunt return;
FOXHUNT_EnterHunt();
return; // EXIT (quit) falls through
} }
// Idle (silent) phase: responsive, editable countdown. // Idle (silent) phase: responsive, editable countdown.
@@ -1267,12 +1243,8 @@ static void FOXHUNT_BeaconTick(void)
#endif #endif
FOXHUNT_BeaconKeys(); FOXHUNT_BeaconKeys();
if (!foxRunning) // EXIT: leave Fox Hunt if (!foxRunning)
return; return;
if (!foxBeacon) { // side-key: back to the hunt
FOXHUNT_EnterHunt();
return;
}
FOXHUNT_BeaconDraw(false, beaconIdleLeft); FOXHUNT_BeaconDraw(false, beaconIdleLeft);
FOXHUNT_BlitScreen(); 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. // 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 // 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. // compare avoids re-reading the 4 KB sector on every quiet tick.
static void FOXHUNT_SaveConfig(void) static void FOXHUNT_SaveConfig(void)
{ {
@@ -1354,83 +1326,90 @@ static void FOXHUNT_SaveConfig(void)
memcpy(foxCfgSaved, cfg, sizeof(cfg)); 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_UpdateTickless();
BACKLIGHT_TurnOn(); BACKLIGHT_TurnOn();
// Hunt on the user-selected VFO: dual-watch / cross-band may have left the // Both apps operate on the user-selected VFO. Dual watch may have left the
// radio listening on the other VFO, so force RX on the selected one and retune. // receiver on the other one, so establish a deterministic starting point.
gEeprom.RX_VFO = gEeprom.TX_VFO; gEeprom.RX_VFO = gEeprom.TX_VFO;
gRxVfo = gTxVfo; gRxVfo = gTxVfo;
gCurrentVfo = gTxVfo; gCurrentVfo = gTxVfo;
RADIO_SetupRegisters(true); RADIO_SetupRegisters(true);
// Beacon defaults (LoadConfig may override fox / tx / idle below). // Initialise the complete legacy seven-byte configuration before loading it.
beaconIdle = FOXHUNT_BEACON_IDLE_DEF; // Keeping the layout unchanged preserves existing FieldOps settings and lets
beaconTx = FOXHUNT_BEACON_TX_DEF; // either independently compiled app update its fields without a migration.
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).
attStep = 0; attStep = 0;
foxGraphMode = FOXHUNT_GRAPH_BAR; foxGraphMode = FOXHUNT_GRAPH_BAR;
foxAudioMode = FOXHUNT_AUDIO_OFF; foxAudioMode = FOXHUNT_AUDIO_OFF;
FOXHUNT_LoadConfig(); // may override att / gauge / audio / idle / fox / tx beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
FOXHUNT_EnterHunt(); // applies the restored attStep and audio mode beaconTx = FOXHUNT_BEACON_TX_DEF;
// beaconMsg is (re)built by FOXHUNT_BeaconTransmit before every burst, and nothing foxFox = FOXHUNT_FOX_CALL;
// reads it before the first burst, so no need to assemble it here. beaconPhaseTx = false;
FOXHUNT_LoadConfig();
// Prime the key state with whatever is held right now, so the side key that // Ignore the key that launched the modal app until it has been released.
// 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.
kbd.prev = kbd.current = KEYBOARD_GetKey(); kbd.prev = kbd.current = KEYBOARD_GetKey();
gWasFKeyPressed = false; // start with the reverse-step arm cleared gWasFKeyPressed = false;
// 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.
foxLocked = false; foxLocked = false;
fHoldMs = 0; fHoldMs = 0;
fLongDone = false; fLongDone = false;
foxRunning = true; 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) { while (foxRunning) {
#if defined(ENABLE_UART) || defined(ENABLE_USB) #if defined(ENABLE_UART) || defined(ENABLE_USB)
UART_ServiceCommands(); UART_ServiceCommands();
#endif #endif
if (foxBeacon) {
FOXHUNT_BeaconTick();
continue;
}
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER #ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Keep the K5Viewer link alive and pick up any remote key. // Keep the K5Viewer link alive and pick up any remote key.
K5VIEWER_ParseInput(); K5VIEWER_ParseInput();
#endif #endif
FOXHUNT_HandleKeys(); FOXHUNT_HandleKeys();
if (foxBeacon) // just switched to beacon: skip the RX work this tick if (!foxRunning)
continue; break;
curDbm = FOXHUNT_ReadDbm(); curDbm = FOXHUNT_ReadDbm();
if (curDbm > peakDbm) if (curDbm > peakDbm)
@@ -1468,24 +1447,23 @@ void APP_RunFoxHunt(void)
FOXHUNT_TickDelay(); // tick delay + smooth backlight fade FOXHUNT_TickDelay(); // tick delay + smooth backlight fade
} }
// Persist the session's settings on the way out (EXIT clears foxRunning) so FOXHUNT_End();
// 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);
} }
void ACTION_FoxHunt(void) void APP_RunBeacon(void)
{ {
APP_RunFoxHunt(); FOXHUNT_Begin();
GUI_SelectNextDisplay(DISPLAY_MAIN); 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
+12 -3
View File
@@ -17,7 +17,7 @@
#ifndef APP_FOXHUNT_H #ifndef APP_FOXHUNT_H
#define 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" #include "keyboard_state.h"
@@ -46,12 +46,21 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <string.h> #include <string.h>
#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_FoxHunt(void);
void ACTION_Beacon(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
// Self-contained modal loop that owns the screen and keypad until EXIT. // Self-contained modal loop that owns the screen and keypad until EXIT.
void APP_RunFoxHunt(void); void APP_RunFoxHunt(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_BEACON
void APP_RunBeacon(void);
#endif
#endif // ENABLE_FEAT_F4HWN_FOXHUNT
#endif // APP_FOXHUNT_H #endif // APP_FOXHUNT_H
+54
View File
@@ -633,8 +633,60 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header)
return APP_OK; 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) 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; app_header_t h;
for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) { 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) for (uint32_t off = 0; off < APP_SLOT_STRIDE; off += APP_SECTOR_SIZE)
PY25Q16_SectorErase(base + off); PY25Q16_SectorErase(base + off);
PY25Q16_InvalidateCache(); PY25Q16_InvalidateCache();
app_shortcuts_cached = false;
return APP_OK; 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; return APP_ERR_SIZE;
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false); PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false);
PY25Q16_InvalidateCache(); PY25Q16_InvalidateCache();
app_shortcuts_cached = false;
return APP_OK; return APP_OK;
} }
+7
View File
@@ -70,6 +70,10 @@
#define APP_NAME_LEN 16 #define APP_NAME_LEN 16
#define APP_VERSION_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)) { typedef struct __attribute__((packed)) {
uint32_t magic; /* APP_MAGIC */ uint32_t magic; /* APP_MAGIC */
uint16_t hdr_version; /* APP_HDR_VERSION */ 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. */ /* Find the first valid app with this exact header name and launch it. */
uint8_t APP_LaunchOverlayByName(const char *name); 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). */ /* 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_SlotInfo(uint8_t slot, app_header_t *out_header);
uint8_t APP_SlotErase(uint8_t slot); uint8_t APP_SlotErase(uint8_t slot);
+1 -1
View File
@@ -301,7 +301,7 @@ const uint8_t BITMAP_NOAA[12] =
}; };
#endif #endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT #if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
const uint8_t BITMAP_FoxHuntSignal[10] = const uint8_t BITMAP_FoxHuntSignal[10] =
{ // point source + the speaker bitmap's two sound waves, oriented horizontally { // point source + the speaker bitmap's two sound waves, oriented horizontally
0b00001000, 0b00001000,
+1 -1
View File
@@ -45,7 +45,7 @@ extern const uint8_t BITMAP_compand[6];
extern const uint8_t BITMAP_NOAA[12]; 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_FoxHuntSignal[10];
extern const uint8_t BITMAP_FoxHuntSpeaker[10]; extern const uint8_t BITMAP_FoxHuntSpeaker[10];
extern const uint8_t BITMAP_FoxHuntUp[11]; extern const uint8_t BITMAP_FoxHuntUp[11];
+3 -4
View File
@@ -54,10 +54,9 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
_MK_MAPPING(0x009000, 0x009000, 0x0090E7), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old _MK_MAPPING(0x009000, 0x009000, 0x0090E7), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old
// 0x90D6 bound was 10 B short: it truncated the // 0x90D6 bound was 10 B short: it truncated the
// 470 MHz VFO1 record at 0x90D0 -> 0x90DF) // 470 MHz VFO1 record at 0x90D0 -> 0x90DF)
// Fox Hunt settings * 7 Bytes 0x90E0 -> 0x90E7 // Fox Hunt / Beacon settings * 7 Bytes 0x90E0 -> 0x90E7
// (ENABLE_FEAT_F4HWN_FOXHUNT, written directly by // (written directly by app/foxhunt.c; concatenated
// app/foxhunt.c; concatenated here so aircopy clones // here so aircopy clones them with the VFOs)
// them together with the VFOs)
_MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010 _MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010
// Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020 // Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020
+1 -1
View File
@@ -130,7 +130,7 @@ unsigned int BATTERY_VoltsToPercent(const unsigned int voltage_10mV)
return 0; 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) void BATTERY_Sample(const bool bDisplayBatteryLevel)
{ {
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent); BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
+1 -1
View File
@@ -45,7 +45,7 @@ typedef enum {
unsigned int BATTERY_VoltsToPercent(unsigned int voltage_10mV); 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); void BATTERY_Sample(bool bDisplayBatteryLevel);
#endif #endif
void BATTERY_GetReadings(bool bDisplayBatteryLevel); void BATTERY_GetReadings(bool bDisplayBatteryLevel);
+2 -1
View File
@@ -126,8 +126,9 @@ enum ACTION_OPT_t {
ACTION_OPT_POWER_HIGH = 20, ACTION_OPT_POWER_HIGH = 20,
ACTION_OPT_REMOVE_OFFSET = 21, ACTION_OPT_REMOVE_OFFSET = 21,
ACTION_OPT_FOXHUNT = 22, ACTION_OPT_FOXHUNT = 22,
ACTION_OPT_BEACON = 23,
ACTION_OPT_LEN = 23 ACTION_OPT_LEN = 24
}; };
#ifdef ENABLE_VOICE #ifdef ENABLE_VOICE
+2 -1
View File
@@ -495,7 +495,8 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] =
{"BEAM", ACTION_OPT_BEAM}, {"BEAM", ACTION_OPT_BEAM},
{"POWER\nHIGH", ACTION_OPT_POWER_HIGH}, {"POWER\nHIGH", ACTION_OPT_POWER_HIGH},
{"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET}, {"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); const uint8_t gSubMenu_SIDEFUNCTIONS_size = ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS);
+1 -1
View File
@@ -54,7 +54,7 @@ static void convertTime(uint8_t *line, uint8_t type)
#endif #endif
#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) void UI_DrawStatusBattery(uint8_t *line, char *str)
#else #else
static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str) static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str)
+1 -1
View File
@@ -19,7 +19,7 @@
#include <stdint.h> #include <stdint.h>
#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); void UI_DrawStatusBattery(uint8_t *line, char *str);
#endif #endif
void UI_DisplayStatus(); void UI_DisplayStatus();
+4
View File
@@ -82,6 +82,7 @@
"ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false, "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false,
"ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_BEAM": false,
"ENABLE_FEAT_F4HWN_FOXHUNT": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false,
"ENABLE_FEAT_F4HWN_BEACON": false,
"ENABLE_FEAT_F4HWN_ACTION_PICKER": false, "ENABLE_FEAT_F4HWN_ACTION_PICKER": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
@@ -132,6 +133,7 @@
"ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true, "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true,
"ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_BEAM": false,
"ENABLE_FEAT_F4HWN_FOXHUNT": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false,
"ENABLE_FEAT_F4HWN_BEACON": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG": true, "ENABLE_FEAT_F4HWN_RXTX_LOG": true,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true,
@@ -155,6 +157,7 @@
"cacheVariables": { "cacheVariables": {
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"ENABLE_FEAT_F4HWN_FOXHUNT": true, "ENABLE_FEAT_F4HWN_FOXHUNT": true,
"ENABLE_FEAT_F4HWN_BEACON": true,
"EDITION_STRING": "FieldOps", "EDITION_STRING": "FieldOps",
"TARGET": "f4hwn.fieldops" "TARGET": "f4hwn.fieldops"
} }
@@ -175,6 +178,7 @@
"cacheVariables": { "cacheVariables": {
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"ENABLE_FEAT_F4HWN_FOXHUNT": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false,
"ENABLE_FEAT_F4HWN_BEACON": false,
"ENABLE_AIRCOPY": true, "ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_BEAM": true,
"ENABLE_FEAT_F4HWN_GAME": false, "ENABLE_FEAT_F4HWN_GAME": false,
+3 -2
View File
@@ -187,7 +187,7 @@ Specialized presets extend Fusion for specific uses:
- selectable RF attenuation, - selectable RF attenuation,
- silent, Geiger-style and received-audio modes, - silent, Geiger-style and received-audio modes,
- long-press `F` keypad lock (attenuation stays adjustable with the arrow keys). - 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, - `MOE`, `MOI`, `MOS`, `MOH`, `MO5` and `MO` identifiers,
- optional callsign identification, - optional callsign identification,
- configurable TX and idle periods, - configurable TX and idle periods,
@@ -264,7 +264,8 @@ Specialized presets extend Fusion for specific uses:
- offset removal, - offset removal,
- BEAM, - BEAM,
- RF Log, - RF Log,
- Fox Hunt / Beacon. - Fox Hunt,
- Beacon.
### Reliability and optimization ### Reliability and optimization