Compare commits

...
19 Commits
Author SHA1 Message Date
Armel FAUVEAU 435192e3bf Merge pull request #511 from armel/feature_update_v5
Feature update v5
2026-08-14 02:27:10 +02:00
Armel FAUVEAU 9c86a7c349 Prepare v5.9.0 2026-08-14 02:21:59 +02:00
Armel FAUVEAU 0a61a6de12 Update rolling Fusion development firmware 2026-08-13 20:18:09 +02:00
EthanYan6 1021ee5e18 FM scan ignore channel RX 2026-08-13 20:04:41 +02:00
Armel FAUVEAU 6692b5191b Update rolling Fusion development firmware 2026-08-11 22:24:36 +02:00
Armel FAUVEAU c65db46bad Add side-key action picker (F + hold side key) 2026-08-11 22:12:25 +02:00
Armel FAUVEAU a38f611bd5 Update rolling Fusion development firmware 2026-08-09 23:51:51 +02:00
Armel FAUVEAU 528678a31b Hold scan resume during scan list name (memory scan only) 2026-08-09 23:44:40 +02:00
Armel FAUVEAU abc146a1b6 Add 2 digits 2026-08-09 17:45:20 +02:00
Armel FAUVEAU c7ff3eeb66 Update rolling Fusion development firmware 2026-08-09 16:55:51 +02:00
Armel FAUVEAU a3c72bdd93 Add menu categories with All fallback 2026-08-09 16:54:45 +02:00
Armel FAUVEAU e4408fcb55 Fix unused searchStart warning in chFrScanner.c 2026-08-09 16:46:58 +02:00
Armel FAUVEAU 5715cd27ef Fix compile-with-docker.sh: pass extra CMake args safely 2026-08-09 16:37:18 +02:00
Armel FAUVEAU 253104bb87 Update rolling Fusion development firmware 2026-08-08 23:37:55 +02:00
Armel FAUVEAU debf67a027 Add Fox Hunt / Beacon keypad lock (long-press F, Discussion #498) 2026-08-08 23:36:38 +02:00
Armel FAUVEAU 535ee80d2b Update rolling Fusion development firmware 2026-08-08 01:34:34 +02:00
Armel FAUVEAU 559460739e Fix Fox Hunt: disable SetOff auto power-off in this mode 2026-08-08 01:33:34 +02:00
Armel FAUVEAU 0d69388e6d Update rolling Fusion development firmware 2026-08-06 04:45:44 +02:00
Armel FAUVEAU 9bf075ea22 Add experiment BYP/BYP+ mode 2026-08-06 04:45:11 +02:00
19 changed files with 829 additions and 115 deletions

No files matched your search

+11
View File
@@ -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)
@@ -227,11 +233,16 @@ endif()
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
enable_feature(ENABLE_FEAT_F4HWN_LOGO)
enable_feature(ENABLE_FEAT_F4HWN_LOGO_SAV)
enable_feature(ENABLE_FEAT_F4HWN_MENU_CAT)
if(ENABLE_FEAT_F4HWN_LOGO_SAV AND NOT ENABLE_FEAT_F4HWN_LOGO)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_LOGO_SAV requires ENABLE_FEAT_F4HWN_LOGO.")
endif()
if(ENABLE_FEAT_F4HWN_MENU_CAT AND NOT ENABLE_FEAT_F4HWN)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MENU_CAT requires ENABLE_FEAT_F4HWN.")
endif()
# ---- DEBUGGING ----
enable_feature(ENABLE_AGC_SHOW_DATA)
+124 -35
View File
@@ -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
}
+7
View File
@@ -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
+54 -4
View File
@@ -312,6 +312,13 @@ static void CheckForIncoming(void)
if (!g_SquelchLost)
return; // squelch is closed
#ifdef ENABLE_FMRADIO
// FM scan in progress: ignore main-channel RX so scanning is not interrupted.
// Normal FM listening (FM_SCAN_OFF) still yields to channel signals as before.
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
return;
#endif
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
ScreenSaverExit();
#endif
@@ -444,6 +451,12 @@ static void HandleIncoming(void)
}
#endif
#ifdef ENABLE_FMRADIO
// Defensive: do not leave FM scan for a main-channel signal.
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
return;
#endif
APP_StartListening(gMonitor ? FUNCTION_MONITOR : FUNCTION_RECEIVE);
}
@@ -1245,9 +1258,11 @@ void APP_Update(void)
#endif
#ifdef ENABLE_VOICE
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed && gVoiceWriteIndex == 0)
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed && gVoiceWriteIndex == 0
&& !UI_MAIN_ShouldHoldScanResume())
#else
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed)
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed
&& !UI_MAIN_ShouldHoldScanResume())
#endif
{ // scanning
CHFRSCANNER_ContinueScanning();
@@ -1609,6 +1624,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
@@ -1808,6 +1830,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
@@ -2027,6 +2058,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) {
@@ -2267,7 +2302,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();
@@ -2304,6 +2343,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;
@@ -2605,7 +2652,10 @@ Skip:
gFlagRefreshSetting = false;
gMenuCountdown = menu_timeout_500ms;
MENU_ShowCurrentSetting();
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel != MENU_LEVEL_CAT)
#endif
MENU_ShowCurrentSetting();
}
if (gFlagPrepareTX) {
+2
View File
@@ -1133,7 +1133,9 @@ static void NextMemChannel(void)
if (!enabled || chan == 0xFFFF)
{
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
const uint16_t searchStart = gNextMrChannel;
#endif
chan = RADIO_FindNextChannel(gNextMrChannel + gScanStateDir, gScanStateDir, true, gEeprom.SCAN_LIST_DEFAULT);
if (chan == 0xFFFF)
{ // no valid channel found -> wrapping back to the first channel
+196 -52
View File
@@ -44,6 +44,14 @@
#define FOXHUNT_TICK_MS 50
#define FOXHUNT_TREND_TICKS 20 // compare the level roughly once per second
// Keypad lock: hold F for ~0.5 s to toggle it, mirroring the long-press F gesture of the
// main screen. Tracked as an elapsed-time accumulator (not a tick count) so the same hold
// fires in every loop — the 50 ms hunt/idle ticks and the 10 ms TX slices alike — letting
// the pad be locked or unlocked at any moment, mid-burst included. While locked every key
// is swallowed except this hold (and, in the hunt, the attenuation arrows), so a radio
// carried by a child cannot be knocked out of the mode or its settings changed.
#define FOXHUNT_LOCK_HOLD_MS 500
// Signal-history graph: a full-width scrolling trace of the level over the last
// seconds, on the same 13-level S scale as the staircase (FOXHUNT_FillCount). It is
// an alternate main gauge, toggled with the 2 key. The reading is EMA-smoothed each
@@ -118,17 +126,46 @@
#define FOXHUNT_MSG_LINE_ID 3
#define FOXHUNT_MSG_LINE_ONE 2
// Attenuator via the BK4829 PGA (REG_13 bits 0..2) — the last gain stage before
// the RSSI detector, so it scales the reading linearly. Reducing the early LNA
// alone barely moved the reading on strong signals (mixer + PGA re-amplify).
// pgaTab (bk4829.c) = {-33,-27,-21,-15,-9,-6,-3,0}; index 7 (= 0 dB) matches the
// RX default (0x03DF), so step 0 leaves the gain untouched. The PGA indices per step
// are {7,5,3,1} = 7 - 2*attStep (computed in FOXHUNT_ApplyAtt, no table needed).
static const uint8_t FOXHUNT_ATT_DB[4] = {0, 6, 15, 27}; // 0, -6, -15, -27 dB
// Front-end gain ladder (KEY_3), written straight to the BK4829 gain register REG_13.
// Fields (LSB->MSB): pga[2:0], mixer[4:3], lna[7:5], lnaS[9:8]; gain tables live in
// bk4829.c (BK4819_GetRxGain_dB). The RX default is 0x03DF (pga7/mixer3/lna6/lnaS3).
//
// Steps 0..3 attenuate the PGA only (the last stage) — {pga 7,5,3,1} = 0/-6/-15/-27 dB
// on top of the default — same behaviour as before. Steps 4..5 attenuate at the *front*
// of the chain instead: right on top of the fox the LNA saturates, so cutting after it
// (PGA) no longer tracks the field; reducing the LNA itself brings it back into its
// linear region and restores the gradient for the final approach.
//
// What "BYP" / "BYP+" actually are: a label of convenience, NOT a hardware bypass — the
// BK4829 has no LNA-bypass switch we flip here. Both steps simply keep the PGA at minimum
// and pull the front-end gain fields of REG_13 further down:
// BYP (step 4): LNA reduced (lna 6->3) — front-end starts giving
// BYP+ (step 5): LNA + LNA-short cut (lna 3->1, lnaS 3->1) — deepest front-end cut
//
// Total gain stays monotonic: -2/-8/-17/-29/-36/-70 dB. These values are a sane starting
// point — expect to trim them on the air against a strong close source. NB the shown dBm
// is not gain-compensated (bk4829.c), so the reading steps down when BYP engages; that is
// intended (it re-centres the S-meter).
#define FOXHUNT_ATT_COUNT 6 // 4 PGA steps + 2 front-end (BYP) steps
#define FOXHUNT_ATT_BYP0 4 // first step at which the front-end (LNA) is reduced
#define FOXHUNT_ATT_SETTLE_MS 40 // let the RSSI detector converge to the new gain before
// rebasing the holds/trend/history onto it (tune on air)
static const uint16_t FOXHUNT_ATT_REG13[FOXHUNT_ATT_COUNT] = {
0x03DF, // 0 pga7 ATT 0 dB (RX default)
0x03DD, // 1 pga5 ATT -6 dB
0x03DB, // 2 pga3 ATT -15 dB
0x03D9, // 3 pga1 ATT -27 dB
0x0379, // 4 pga1 lna3 BYP (front-end starts giving)
0x0139, // 5 pga1 lna1 lnaS1 BYP+ (deep front-end cut)
};
static const uint8_t FOXHUNT_ATT_DB[FOXHUNT_ATT_BYP0] = {0, 6, 15, 27}; // PGA-step labels
static KeyboardState kbd = {KEY_INVALID, KEY_INVALID, 0};
static bool foxRunning;
static bool foxLocked; // keypad lock (long-press F toggles it, hunt + beacon)
static uint16_t fHoldMs; // elapsed time the F key has been held (ms)
static bool fLongDone; // the long-press lock toggle already fired this hold
static uint8_t foxAudioMode;
static uint8_t foxGraphMode; // FOXHUNT_GRAPH_BAR / _HIST, cycled by the 2 key
static uint8_t attStep;
@@ -189,11 +226,14 @@ static int16_t FOXHUNT_ReadDbm(void)
return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band];
}
// Apply the selected attenuator step to the BK4829 PGA gain field.
// Apply the selected step's BK4829 gain preset to REG_13: the PGA for the ATT steps,
// plus the LNA/LNAs for the BYP steps. Read-modify-write masked to the known gain
// fields (bits 0..9), so the undocumented upper bits 10..15 are preserved rather than
// cleared. Every preset fits within 0x03FF, so only the gain fields ever change.
static void FOXHUNT_ApplyAtt(void)
{
uint16_t reg = BK4819_ReadRegister(BK4819_REG_13);
reg = (reg & ~0x7u) | (uint16_t)(7u - 2u * attStep); // PGA {7,5,3,1}, REG_13 bits 0..2
reg = (reg & ~0x03FFu) | FOXHUNT_ATT_REG13[attStep];
BK4819_WriteRegister(BK4819_REG_13, reg);
}
@@ -382,12 +422,14 @@ static void FOXHUNT_Tag(const char *s, uint8_t x, uint8_t line)
GUI_DisplaySmallestInverse(s, x, line, false, true, (uint8_t)(x + strlen(s) * 4));
}
// Mirror the firmware's status-bar F indicator: when the F key is armed, a number key
// steps its setting backwards. Same glyph and column as the main screens (status.c), so
// it reads identically — Fox Hunt just renders its own status line, hence the redraw.
// Mirror the firmware's status-bar indicator at the same column as the main screens
// (status.c), so it reads identically: the padlock when the keypad is locked, else the
// F glyph when the reverse-step arm is set. Lock takes priority, exactly as on status.c.
static void FOXHUNT_DrawFKey(void)
{
if (gWasFKeyPressed)
if (foxLocked)
memcpy(gStatusLine + 69, gFontKeyLock, sizeof(gFontKeyLock));
else if (gWasFKeyPressed)
memcpy(gStatusLine + 69, gFontF, sizeof(gFontF));
}
@@ -512,8 +554,12 @@ static void FOXHUNT_Draw(void)
GUI_DisplaySmallest("+40", 110, 41, false, true);
}
// Attenuator as an inverse label (left), tuned frequency (right).
sprintf(str, "ATT %ddB", FOXHUNT_ATT_DB[attStep]);
// Front-end ladder as an inverse label (left), tuned frequency (right). The PGA
// steps show "ATT xxdB"; the two front-end steps show "BYP" / "BYP+".
if (attStep < FOXHUNT_ATT_BYP0)
sprintf(str, "ATT %ddB", FOXHUNT_ATT_DB[attStep]);
else
strcpy(str, (attStep == FOXHUNT_ATT_BYP0) ? "BYP" : "BYP+");
FOXHUNT_Tag(str, 4, 6);
FOXHUNT_DrawFreqBR(gRxVfo->pRX->Frequency);
}
@@ -538,11 +584,44 @@ static uint8_t FOXHUNT_RangeStep(uint8_t v, uint8_t lo, uint8_t hi, uint8_t step
return (v <= lo) ? hi : (uint8_t)(v - step);
}
// Attenuator: 0 -> -6 -> -15 -> -27 dB (F reverses).
// Rebase the level-tracking state (peak/min hold, trend reference and the signal
// history) onto a fresh reading. Used at hunt entry and after any front-end gain
// change: the shown dBm is not gain-compensated, so without this a step change would
// leave a stale peak, an artificial min, a one-shot false trend spike and a cliff in
// the sparkline.
static void FOXHUNT_RebaseMeasurements(void)
{
curDbm = FOXHUNT_ReadDbm();
peakDbm = curDbm;
minDbm = curDbm;
trendRef = curDbm;
trendDelta = 0;
trendTick = 0;
// Prime the signal history flat at the current level so the sparkline scrolls in
// from a sensible baseline instead of showing the gain step as a cliff.
{
uint8_t lvl = FOXHUNT_FillCount(curDbm);
for (uint8_t i = 0; i < FOXHUNT_HIST_LEN; i++)
histBuf[i] = lvl;
}
histHead = 0;
histTick = 0;
histEma = (int16_t)(curDbm * 8);
}
// Front-end ladder: ATT 0 -> -6 -> -15 -> -27 dB, then BYP -> BYP+ (F reverses).
static void FOXHUNT_AttCycle(int8_t dir)
{
attStep = FOXHUNT_WrapStep(attStep, 4, dir);
attStep = FOXHUNT_WrapStep(attStep, FOXHUNT_ATT_COUNT, dir);
FOXHUNT_ApplyAtt();
// Wait for the RSSI to settle to the new gain, THEN rebase the holds/trend/history.
// Reading straight away would latch the old-gain level, and as the RSSI converges
// over the next ticks the very artifacts the rebase avoids (stuck peak, false trend
// spike, sparkline cliff) would reappear.
SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS);
FOXHUNT_RebaseMeasurements();
}
// Fox identifier: MOE -> MOI -> MOS -> MOH -> MO5 -> MO -> CALL (F reverses).
@@ -590,8 +669,14 @@ static void FOXHUNT_TickDelay(void)
}
// Idle housekeeping the foreground scheduler normally does but this modal loop
// bypasses: backlight timeout (BLTime) and, while hunting, the sleep auto power
// off. Cadenced on the 500 ms system tick.
// bypasses: the periodic battery sample, the config save and the backlight
// timeout (BLTime). Cadenced on the 500 ms system tick.
//
// The SetOff auto power-off is deliberately NOT run here. Fox Hunt is an active,
// attended mode that receives a signal continuously; the main loop never sleeps
// while receiving (it re-arms gSleepModeCountdown_500ms on RX), so the old
// hand-back-to-main scheme just ejected the user to the VFO instead of powering
// off. Both hunt and beacon therefore ignore SetOff and run until EXIT.
static void FOXHUNT_IdleHousekeeping(void)
{
if (!gNextTimeslice_500ms)
@@ -613,22 +698,37 @@ static void FOXHUNT_IdleHousekeeping(void)
// just a clean EXIT). No-op when nothing changed.
FOXHUNT_SaveConfig();
// Backlight timeout: switch it off when the countdown expires (both modes).
// Backlight timeout: at BLTime expiry drop from BLMax to BLMin (both modes).
if (gBacklightCountdown_500ms > 0
&& gEeprom.BACKLIGHT_TIME < 61
&& --gBacklightCountdown_500ms == 0)
BACKLIGHT_TurnOff();
}
#ifdef ENABLE_FEAT_F4HWN_SLEEP
// Auto power-off: only while hunting (a running beacon must stay alive). Hand
// back to the main loop just before expiry so it performs the actual sleep.
if (!foxBeacon && gSetting_set_off != 0 && gSleepModeCountdown_500ms > 0) {
if (gSleepModeCountdown_500ms > 1)
gSleepModeCountdown_500ms--;
else
foxRunning = false;
// Track a held F key and toggle the keypad lock once it crosses the long-press threshold.
// Fed the time elapsed since the last call (a hunt/idle tick, or a TX slice), so it fires
// after the same ~0.5 s hold in every loop and the pad can be (un)locked at any moment. A
// non-F key resets the accumulator; the toggle fires once per physical hold. Returns true
// exactly on the toggle so a caller that does not redraw every tick (the TX loop) can
// refresh the padlock immediately.
static bool FOXHUNT_LockHoldTrack(KEY_Code_t key, uint16_t elapsedMs)
{
if (key != KEY_F) {
fHoldMs = 0;
fLongDone = false;
return false;
}
#endif
if (fLongDone) // already toggled once for this hold
return false;
fHoldMs += elapsedMs;
if (fHoldMs < FOXHUNT_LOCK_HOLD_MS)
return false;
fLongDone = true;
foxLocked = !foxLocked;
gWasFKeyPressed = false; // a lock toggle is not a reverse-step arm
BACKLIGHT_TurnOn();
return true;
}
static void FOXHUNT_HandleKeys(void)
@@ -636,12 +736,25 @@ static void FOXHUNT_HandleKeys(void)
kbd.prev = kbd.current;
kbd.current = KEYBOARD_GetKey();
// Long-press F toggles the keypad lock; a short F press falls through to the reverse-
// step arm on its rising edge below. One hunt tick has elapsed since the last call.
FOXHUNT_LockHoldTrack(kbd.current, FOXHUNT_TICK_MS);
// Act only on the rising edge of a new key.
if (kbd.current == KEY_INVALID || kbd.current == kbd.prev)
return;
BACKLIGHT_TurnOn(); // any keypress wakes the screen and re-arms the timers
// While locked, swallow everything except the attenuation arrows — the lock is
// released only by the long-press F handled above. Keeping ATT reachable is the whole
// point: on the final approach the sensitivity still has to be pulled down by hand.
if (foxLocked) {
if (kbd.current == KEY_UP) FOXHUNT_AttCycle(+1); // more attenuation
if (kbd.current == KEY_DOWN) FOXHUNT_AttCycle(-1); // less attenuation
return;
}
if (kbd.current == KEY_F) { // arm / disarm a reverse step for the next number key
gWasFKeyPressed = !gWasFKeyPressed;
return;
@@ -664,9 +777,17 @@ static void FOXHUNT_HandleKeys(void)
audioTick = FOXHUNT_RATE_SLOW_TICKS; // blip promptly
break;
case KEY_3:
// Cycle the attenuator (0 -> -6 -> -15 -> -27 dB; F reverses).
// Cycle the front-end ladder (ATT 0/-6/-15/-27 dB, then BYP/BYP+; F reverses).
FOXHUNT_AttCycle(dir);
break;
case KEY_UP:
// Attenuation up one step (also the locked-mode control).
FOXHUNT_AttCycle(+1);
break;
case KEY_DOWN:
// Attenuation down one step.
FOXHUNT_AttCycle(-1);
break;
case KEY_MENU:
// Reset the peak / min hold and the trend reference (before each body scan).
peakDbm = curDbm;
@@ -697,24 +818,14 @@ static void FOXHUNT_EnterHunt(void)
FOXHUNT_ApplyAtt();
FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station
curDbm = FOXHUNT_ReadDbm();
peakDbm = curDbm;
minDbm = curDbm;
trendRef = curDbm;
trendDelta = 0;
trendTick = 0;
// 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.
// SetAudio already waits when audio is on, but not in the default audio-off case.
SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS);
FOXHUNT_RebaseMeasurements();
audioTick = (foxAudioMode == FOXHUNT_AUDIO_BEEP) ? FOXHUNT_RATE_SLOW_TICKS : 0;
// Prime the signal history flat at the current level so the sparkline scrolls
// in from a sensible baseline (also applies on return from the beacon).
{
uint8_t lvl = FOXHUNT_FillCount(curDbm);
for (uint8_t i = 0; i < FOXHUNT_HIST_LEN; i++)
histBuf[i] = lvl;
}
histHead = 0;
histTick = 0;
histEma = (int16_t)(curDbm * 8);
}
// Evaluate the TX gates for the beacon's TX VFO in the same precedence order as
@@ -806,11 +917,28 @@ static bool FOXHUNT_TxDelay(uint16_t ms)
kbd.prev = kbd.current;
kbd.current = KEYBOARD_GetKey();
// Track a held F across slices (fed the real slice time, so the ~0.5 s threshold
// matches hunt/idle). This runs before the rising-edge test below — a held key
// reads the same every slice and would otherwise be skipped — so the pad can be
// locked or unlocked mid-burst. On a toggle, repaint at once so the padlock
// appears without waiting for the next ID repaint.
if (FOXHUNT_LockHoldTrack(kbd.current, slice)) {
FOXHUNT_BeaconDraw(true, 0);
FOXHUNT_BlitScreen();
}
if (kbd.current == KEY_INVALID || kbd.current == kbd.prev)
continue; // rising edge only
BACKLIGHT_TurnOn(); // keep the screen awake during TX
// Locked: ignore every key for the rest of the burst; the long-press F unlock is
// handled above, before this gate, so the reverse-arm F and the setting keys stay
// inert until the pad is unlocked.
if (foxLocked)
continue;
if (kbd.current == KEY_EXIT) {
foxRunning = false; // abort the burst and leave Fox Hunt
return true;
@@ -1072,11 +1200,21 @@ static void FOXHUNT_BeaconKeys(void)
kbd.prev = kbd.current;
kbd.current = KEYBOARD_GetKey();
// Long-press F toggles the keypad lock (same gesture as the hunt screen). One idle
// tick has elapsed since the last call.
FOXHUNT_LockHoldTrack(kbd.current, FOXHUNT_TICK_MS);
if (kbd.current == KEY_INVALID || kbd.current == kbd.prev)
return;
BACKLIGHT_TurnOn(); // any keypress wakes the screen and re-arms the timers
// Locked: swallow every key (the beacon has no per-key control to keep live, unlike
// the hunt's attenuation arrows); release with the long-press F handled above. A burst
// in progress enforces the same lock in FOXHUNT_TxDelay.
if (foxLocked)
return;
if (kbd.current == KEY_F) { // arm / disarm a reverse step for the next number key
gWasFKeyPressed = !gWasFKeyPressed;
return;
@@ -1161,7 +1299,7 @@ static void FOXHUNT_BeaconTick(void)
beaconPhaseTx = true; // next tick transmits
}
FOXHUNT_IdleHousekeeping(); // backlight timeout (no sleep while beaconing)
FOXHUNT_IdleHousekeeping(); // battery sample + save + backlight timeout
FOXHUNT_TickDelay(); // tick delay + smooth backlight fade
}
@@ -1199,7 +1337,7 @@ static void FOXHUNT_LoadConfig(void)
PY25Q16_ReadBuffer(FOXHUNT_CFG_ADDR, cfg, sizeof(cfg));
if (cfg[0] == FOXHUNT_CFG_MAGIC) { // valid, saved config
if (cfg[1] <= 3) attStep = cfg[1];
if (cfg[1] < FOXHUNT_ATT_COUNT) attStep = cfg[1];
if (cfg[2] <= FOXHUNT_GRAPH_HIST) foxGraphMode = cfg[2];
if (cfg[3] <= FOXHUNT_AUDIO_STATION) foxAudioMode = cfg[3];
if (cfg[4] >= FOXHUNT_BEACON_IDLE_MIN && cfg[4] <= FOXHUNT_BEACON_IDLE_MAX
@@ -1285,6 +1423,12 @@ void APP_RunFoxHunt(void)
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.
foxLocked = false;
fHoldMs = 0;
fLongDone = false;
foxRunning = true;
while (foxRunning) {
#if defined(ENABLE_UART) || defined(ENABLE_USB)
@@ -1334,12 +1478,12 @@ void APP_RunFoxHunt(void)
: BK4819_AF_MUTE);
}
FOXHUNT_IdleHousekeeping(); // backlight timeout / sleep
FOXHUNT_IdleHousekeeping(); // battery sample + save + backlight timeout
FOXHUNT_TickDelay(); // tick delay + smooth backlight fade
}
// Persist the session's settings so they survive a power cycle (covers EXIT
// and the sleep auto power-off, both of which just clear foxRunning).
// 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
+16
View File
@@ -45,6 +45,7 @@
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/main.h"
#include "ui/menu.h"
#include "ui/ui.h"
#include <stdlib.h>
@@ -454,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);
@@ -835,6 +846,11 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
gFlagRefreshSetting = true;
gRequestDisplayScreen = DISPLAY_MENU;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
gMenuLevel = MENU_LEVEL_CAT;
UI_MENU_BuildCategoryScreen();
gMenuCursor = (gMenuCatCursor < gMenuListCount) ? gMenuCatCursor : 0;
#endif
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_MENU;
#endif
+74 -3
View File
@@ -85,10 +85,10 @@ void MENU_StartCssScan(void)
void MENU_CssScanFound(void)
{
if(gScanCssResultType == CODE_TYPE_DIGITAL || gScanCssResultType == CODE_TYPE_REVERSE_DIGITAL) {
gMenuCursor = UI_MENU_GetMenuIdx(MENU_R_DCS);
gMenuCursor = UI_MENU_GetViewPos(MENU_R_DCS);
}
else if(gScanCssResultType == CODE_TYPE_CONTINUOUS_TONE) {
gMenuCursor = UI_MENU_GetMenuIdx(MENU_R_CTCS);
gMenuCursor = UI_MENU_GetViewPos(MENU_R_CTCS);
}
MENU_ShowCurrentSetting();
@@ -1607,6 +1607,36 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gRequestDisplayScreen = DISPLAY_MENU;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
{ // saut-par-numero global : depuis l'ecran categories, entre dans All a l'item N
const uint8_t allCount = UI_MENU_CategoryItemCount(CAT_ALL);
uint16_t value;
if (gInputBoxIndex >= 2) {
gInputBoxIndex = 0;
value = (gInputBox[0] * 10) + gInputBox[1];
} else {
value = gInputBox[0];
}
if (value > 0 && value <= allCount)
{
gMenuCategory = CAT_ALL;
gMenuCatCursor = gMenuListCount - 1; // All = derniere entree de gCatOrder
UI_MENU_BuildView();
gMenuLevel = MENU_LEVEL_ITEMS;
gMenuCursor = value - 1;
gFlagRefreshSetting = true;
}
else if (gInputBoxIndex == 0)
{
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
}
return;
}
#endif
if (!gIsInSubMenu)
{
switch (gInputBoxIndex)
@@ -1821,6 +1851,22 @@ Skip:
return;
}
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_ITEMS)
{ // remonter aux categories au lieu de quitter le menu
gCatLastPos[gMenuCategory] = gMenuCursor; // memorise la position dans la categorie
gMenuLevel = MENU_LEVEL_CAT;
UI_MENU_BuildCategoryScreen();
gMenuCursor = gMenuCatCursor;
gRequestDisplayScreen = DISPLAY_MENU;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
gPttWasReleased = true;
return;
}
#endif
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
@@ -1854,13 +1900,28 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
gRequestDisplayScreen = DISPLAY_MENU;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
{ // niveau categories : MENU descend dans la categorie choisie
gMenuCatCursor = gMenuCursor;
gMenuCategory = gCatOrder[gMenuCursor];
UI_MENU_BuildView();
gMenuLevel = MENU_LEVEL_ITEMS;
gMenuCursor = (gCatLastPos[gMenuCategory] < gMenuListCount) ? gCatLastPos[gMenuCategory] : 0;
gIsInSubMenu = false;
gFlagRefreshSetting = true;
return;
}
#endif
if (!gIsInSubMenu)
{
const int m = UI_MENU_GetCurrentMenuId();
#ifdef ENABLE_VOICE
if (m != MENU_SCR)
gAnotherVoiceID = MenuList[gMenuCursor].voice_id;
gAnotherVoiceID = MenuList[gMenuIndices[gMenuCursor]].voice_id;
#endif
if (m == MENU_UPCODE
|| m == MENU_DWCODE
@@ -2060,6 +2121,16 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
{ // niveau categories : deplacement simple du curseur
gMenuCursor = NUMBER_AddWithWraparound(gMenuCursor, -Direction, 0, gMenuListCount - 1);
gFlagRefreshSetting = true; // rearme le timeout menu (ShowCurrentSetting saute au niveau CAT)
gRequestDisplayScreen = DISPLAY_MENU;
return;
}
#endif
if (!gEeprom.SET_NAV && gIsInSubMenu) {
Direction = -Direction;
}
+6 -8
View File
@@ -164,19 +164,17 @@ void Main(void)
#ifdef ENABLE_FEAT_F4HWN
gEeprom.KEY_LOCK = 0;
SETTINGS_SaveSettings();
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
gMenuLevel = MENU_LEVEL_ITEMS;
gMenuCategory = CAT_ALL;
#endif
gMenuCursor = UI_MENU_GetMenuIdx(FIRST_HIDDEN_MENU_ITEM);
gSubMenuSelection = gSetting_F_LOCK;
#endif
}
// count the number of menu items
gMenuListCount = 0;
while (MenuList[gMenuListCount].name[0] != '\0') {
if(!gF_LOCK && MenuList[gMenuListCount].menu_id == FIRST_HIDDEN_MENU_ITEM)
break;
gMenuListCount++;
}
// build the current menu view (Etape 1: vue = All, identite)
UI_MENU_BuildView();
// wait for user to release all butts before moving on
if (GPIO_IsPttPressed() ||
+53
View File
@@ -18,6 +18,9 @@
#include <stdlib.h> // abs()
#include "app/app.h"
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
#include "app/action.h"
#endif
#include "app/chFrScanner.h"
#include "app/dtmf.h"
@@ -240,6 +243,20 @@ void UI_MAIN_NotifyScanListChanged(void)
gUpdateDisplay = true;
}
// True while the "SCAN LIST xxx" name actually occupies the screen. Used to
// freeze the scan so it does not race ahead of the (hidden) progress gauge,
// which would make the bar snap forward when it reappears.
//
// The IS_MR_CHANNEL() test mirrors show_memory in UI_DrawScanProgress(): the
// name is only ever drawn during a memory (channel) scan. A frequency/range
// scan can still arm the countdown (F + UP/DOWN cycles lists on any scan), but
// no name is shown there, so it must NOT be held - otherwise the scanner would
// stall ~2 s with nothing on screen to explain the pause.
bool UI_MAIN_ShouldHoldScanResume(void)
{
return gScanListNameCountdown_500ms > 0 && IS_MR_CHANNEL(gNextMrChannel);
}
static inline void ScanProgress_SetBit(uint8_t *map, uint16_t ch)
{
map[ch >> 3] |= (uint8_t)(1u << (ch & 7));
@@ -1308,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];
@@ -1328,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
+3
View File
@@ -18,6 +18,7 @@
#define UI_MAIN_H
#include <stdint.h>
#include <stdbool.h>
enum center_line_t {
CENTER_LINE_NONE = 0,
@@ -56,9 +57,11 @@ void UI_DisplayMain(void);
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
void UI_MAIN_NotifyScanProgressDataChanged(void);
void UI_MAIN_NotifyScanListChanged(void);
bool UI_MAIN_ShouldHoldScanResume(void);
#else
static inline void UI_MAIN_NotifyScanProgressDataChanged(void) {}
static inline void UI_MAIN_NotifyScanListChanged(void) {}
static inline bool UI_MAIN_ShouldHoldScanResume(void) { return false; }
#endif
#ifdef ENABLE_AGC_SHOW_DATA
+237 -8
View File
@@ -535,9 +535,15 @@ const uint8_t gSubMenu_SIDEFUNCTIONS_size = ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS);
bool gIsInSubMenu;
uint8_t gMenuCursor;
uint8_t gMenuIndices[ARRAY_SIZE(MenuList)]; // Etape 1: table position affichee -> index MenuList (vue courante)
int UI_MENU_GetCurrentMenuId() {
if(gMenuCursor < ARRAY_SIZE(MenuList))
return MenuList[gMenuCursor].menu_id;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
return 0xFF; // pas d'item courant au niveau categories
#endif
if(gMenuCursor < gMenuListCount)
return MenuList[gMenuIndices[gMenuCursor]].menu_id;
return MenuList[ARRAY_SIZE(MenuList)-1].menu_id;
}
@@ -550,6 +556,221 @@ uint8_t UI_MENU_GetMenuIdx(uint8_t id)
return 0;
}
// Position dans la vue courante (gMenuIndices) du menu_id, ou gMenuCursor si absent.
// En vue All (identite) equivaut a UI_MENU_GetMenuIdx ; en vue categorie, donne la
// position filtree correcte.
uint8_t UI_MENU_GetViewPos(uint8_t id)
{
for (uint8_t i = 0; i < gMenuListCount; i++)
if (MenuList[gMenuIndices[i]].menu_id == id)
return i;
return gMenuCursor;
}
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
// --- Etape 2a : donnee du classement par categorie (cible Fusion) ---
// Chaque liste = les menu_id d'une categorie, DANS l'ordre d'affichage voulu
// (ex. SetPwr colle a Power). CAT_ALL n'a pas de liste : il reprend MenuList
// tel quel, donc ordre et numeros d'origine preserves.
const char *const CategoryNames[CAT_COUNT] = {
[CAT_CHANNELS] = "Channels",
[CAT_SCAN] = "Scan",
[CAT_KEYS] = "Keys",
[CAT_POWER] = "Power",
[CAT_DISPLAY] = "Display",
[CAT_TIMERS] = "Timers",
[CAT_AUDIO] = "Audio",
[CAT_RADIO] = "Radio",
[CAT_DTMF] = "DTMF",
[CAT_SERVICE] = "Service",
[CAT_ALL] = "All",
};
// Les menu_id de sous-features optionnelles sont gardes exactement comme dans
// l'enum (menu.h) : sur un preset qui ne les compile pas, ils ne sont pas
// references (sinon build KO, ex. preset Custom). Les autres MENU_SET_* sont
// sous ENABLE_FEAT_F4HWN, garanti par la dependance CMake (App/CMakeLists.txt).
static const uint8_t CatChannels[] = {
MENU_STEP, MENU_TXP, MENU_SET_PWR, MENU_R_DCS, MENU_R_CTCS, MENU_T_DCS,
MENU_T_CTCS, MENU_SFT_D, MENU_OFFSET, MENU_W_N,
#ifdef ENABLE_FEAT_F4HWN_NARROWER
MENU_SET_NFM,
#endif
MENU_BCL, MENU_COMPAND, MENU_AM, MENU_TX_LOCK, MENU_PTT_ID, MENU_LIST_CH,
MENU_MEM_CH, MENU_DEL_CH, MENU_MEM_NAME,
};
static const uint8_t CatScan[] = {
MENU_S_LIST, MENU_S_PRI, MENU_S_PRI_CH_1, MENU_S_PRI_CH_2, MENU_SC_REV,
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
MENU_SET_SCN,
#endif
};
static const uint8_t CatKeys[] = {
MENU_F1SHRT, MENU_F1LONG, MENU_F2SHRT, MENU_F2LONG, MENU_MLONG, MENU_AUTOLK, MENU_SET_LCK,
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
MENU_SET_KEY,
#endif
MENU_SET_PTT, MENU_1_CALL,
};
static const uint8_t CatPower[] = {
MENU_SAVE, MENU_BAT_TXT,
#ifdef ENABLE_FEAT_F4HWN_SLEEP
MENU_SET_OFF,
#endif
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
MENU_SET_SAV,
#endif
};
static const uint8_t CatDisplay[] = { MENU_MDF, MENU_PONMSG, MENU_ABR, MENU_ABR_MIN, MENU_ABR_MAX, MENU_ABR_ON_TX_RX, MENU_SET_CTR, MENU_SET_INV, MENU_SET_MET, MENU_SET_GUI, MENU_VOL };
static const uint8_t CatTimers[] = { MENU_TOT, MENU_SET_TOT, MENU_SET_EOT, MENU_SET_TMR };
static const uint8_t CatAudio[] = {
MENU_MIC, MENU_MIC_BAR, MENU_BEEP,
#ifdef ENABLE_FEAT_F4HWN_VOL
MENU_SET_VOL,
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
MENU_SET_AUD,
#endif
};
static const uint8_t CatRadio[] = { MENU_SQL, MENU_STE, MENU_RP_STE, MENU_ROGER, MENU_VOX, MENU_TDR };
static const uint8_t CatDtmf[] = { MENU_UPCODE, MENU_DWCODE, MENU_D_ST, MENU_D_PRE, MENU_D_LIVE_DEC };
static const uint8_t CatService[] = { MENU_F_LOCK, MENU_350EN, MENU_BATCAL, MENU_BATTYP, MENU_SET_NAV, MENU_RESET };
typedef struct { const uint8_t *ids; uint8_t len; } cat_list_t;
static const cat_list_t CategoryLists[CAT_COUNT] = {
[CAT_CHANNELS] = { CatChannels, ARRAY_SIZE(CatChannels) },
[CAT_SCAN] = { CatScan, ARRAY_SIZE(CatScan) },
[CAT_KEYS] = { CatKeys, ARRAY_SIZE(CatKeys) },
[CAT_POWER] = { CatPower, ARRAY_SIZE(CatPower) },
[CAT_DISPLAY] = { CatDisplay, ARRAY_SIZE(CatDisplay) },
[CAT_TIMERS] = { CatTimers, ARRAY_SIZE(CatTimers) },
[CAT_AUDIO] = { CatAudio, ARRAY_SIZE(CatAudio) },
[CAT_RADIO] = { CatRadio, ARRAY_SIZE(CatRadio) },
[CAT_DTMF] = { CatDtmf, ARRAY_SIZE(CatDtmf) },
[CAT_SERVICE] = { CatService, ARRAY_SIZE(CatService) },
[CAT_ALL] = { NULL, 0 },
};
uint8_t gMenuCategory = CAT_ALL;
// Index de 'id' dans MenuList, ou 0xFF si absent (item non compile).
static uint8_t menu_find_idx(uint8_t id)
{
for (uint8_t i = 0; MenuList[i].name[0] != '\0'; i++)
if (MenuList[i].menu_id == id)
return i;
return 0xFF;
}
uint8_t gMenuLevel = MENU_LEVEL_CAT;
uint8_t gCatOrder[CAT_COUNT];
uint8_t gMenuCatCursor = 0;
uint8_t gCatLastPos[CAT_COUNT]; // derniere position du curseur item, par categorie
// Nombre d'items presents (compiles) dans une categorie.
uint8_t UI_MENU_CategoryItemCount(uint8_t cat)
{
uint8_t n = 0;
if (cat == CAT_ALL)
{
for (uint8_t i = 0; MenuList[i].name[0] != '\0'; i++)
{
if (!gF_LOCK && MenuList[i].menu_id == FIRST_HIDDEN_MENU_ITEM)
break;
n++;
}
return n;
}
const cat_list_t *cl = &CategoryLists[cat];
for (uint8_t k = 0; k < cl->len; k++)
if (menu_find_idx(cl->ids[k]) != 0xFF)
n++;
return n;
}
// Construit l'ecran niveau categories : gCatOrder = categories visibles,
// gMenuListCount = leur nombre. Service n'apparait que si gF_LOCK.
void UI_MENU_BuildCategoryScreen(void)
{
gMenuListCount = 0;
for (uint8_t c = 0; c < CAT_COUNT; c++)
{
if (c == CAT_SERVICE && !gF_LOCK)
continue;
gCatOrder[gMenuListCount++] = c;
}
}
// Rendu de l'ecran des categories (niveau 1).
static void UI_MENU_DrawCategories(void)
{
char str[16];
const unsigned int sep = 64; // separateur decale a droite : noms longs (ex. "Channels")
const unsigned int x1 = sep + 2;
const unsigned int x2 = LCD_WIDTH - 1;
UI_DisplayClear();
UI_DrawLineBuffer(gFrameBuffer, sep, 0, sep, 55, 1);
for (uint8_t i = 0; i < sep; i += 2)
gFrameBuffer[5][i] = 0x40;
const int count = gMenuListCount;
const int cur = gMenuCursor;
int prev = cur - 1; if (prev < 0) prev = count - 1;
int next = cur + 1; if (next >= count) next = 0;
if (count > 1)
UI_PrintStringSmallNormal(CategoryNames[gCatOrder[prev]], 0, 0, 1);
UI_PrintString(CategoryNames[gCatOrder[cur]], 0, 0, 2, 8);
if (count > 1)
UI_PrintStringSmallNormal(CategoryNames[gCatOrder[next]], 0, 0, 4);
sprintf(str, "%02u/%02u", 1 + cur, count);
UI_PrintStringSmallNormal(str, 6, 0, 6);
sprintf(str, "%02u", UI_MENU_CategoryItemCount(gCatOrder[cur]));
UI_PrintString(str, x1, x2, 1, 8);
UI_PrintStringSmallNormal("items", x1, x2, 5);
ST7565_BlitFullScreen();
}
#endif
// Construit la "vue" courante = table position affichee -> index MenuList.
// Unique endroit qui fixe gMenuListCount + gMenuIndices.
// CAT_ALL (defaut) = liste plate, identite -> ordre/numeros d'origine preserves.
void UI_MENU_BuildView(void)
{
gMenuListCount = 0;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuCategory != CAT_ALL)
{
const cat_list_t *cl = &CategoryLists[gMenuCategory];
for (uint8_t k = 0; k < cl->len; k++)
{
uint8_t idx = menu_find_idx(cl->ids[k]);
if (idx != 0xFF)
gMenuIndices[gMenuListCount++] = idx;
}
return;
}
#endif
for (uint8_t i = 0; MenuList[i].name[0] != '\0'; i++)
{
if (!gF_LOCK && MenuList[i].menu_id == FIRST_HIDDEN_MENU_ITEM)
break;
gMenuIndices[gMenuListCount++] = i;
}
}
int32_t gSubMenuSelection;
// edit box
@@ -624,6 +845,14 @@ void UI_DisplayMenu(void)
char String[64]; // bigger cuz we can now do multi-line in one string (use '\n' char)
char top_right_badge[16];
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
{
UI_MENU_DrawCategories();
return;
}
#endif
const int m = UI_MENU_GetCurrentMenuId();
#ifdef ENABLE_DTMF_CALLING
@@ -647,7 +876,7 @@ void UI_DisplayMenu(void)
for (i = 0; i < 3; i++)
if (gMenuCursor > 0 || i > 0)
if ((gMenuListCount - 1) != gMenuCursor || i != 2)
UI_PrintString(MenuList[gMenuCursor + i - 1].name, 0, 0, i * 2, 8);
UI_PrintString(MenuList[gMenuIndices[gMenuCursor + i - 1]].name, 0, 0, i * 2, 8);
// invert the current menu list item pixels
for (i = 0; i < (8 * menu_list_width); i++)
@@ -683,17 +912,17 @@ void UI_DisplayMenu(void)
if (prev_index < 0) {
prev_index = menu_count - 1;
}
UI_PrintStringSmallNormal(MenuList[prev_index].name, 0, 0, 1);
UI_PrintStringSmallNormal(MenuList[gMenuIndices[prev_index]].name, 0, 0, 1);
// current menu item - keep big n fat
UI_PrintString(MenuList[menu_index].name, 0, 0, 2, 8);
UI_PrintString(MenuList[gMenuIndices[menu_index]].name, 0, 0, 2, 8);
// trailing menu item - small text
int next_index = menu_index + 1;
if (next_index >= menu_count) {
next_index = 0;
}
UI_PrintStringSmallNormal(MenuList[next_index].name, 0, 0, 4);
UI_PrintStringSmallNormal(MenuList[gMenuIndices[next_index]].name, 0, 0, 4);
// draw the menu index number/count
@@ -706,12 +935,12 @@ void UI_DisplayMenu(void)
{
// current menu item
// strcat(String, ":");
UI_PrintString(MenuList[menu_index].name, 0, 0, 0, 8);
UI_PrintString(MenuList[gMenuIndices[menu_index]].name, 0, 0, 0, 8);
// UI_PrintStringSmallNormal(String, 0, 0, 0);
}
#ifdef ENABLE_FEAT_F4HWN
sprintf(String, "%02u/%u", 1 + menu_index, menu_count);
sprintf(String, "%02u/%02u", 1 + menu_index, menu_count);
UI_PrintStringSmallNormal(String, 6, 0, 6);
#endif
}
+34
View File
@@ -169,6 +169,37 @@ enum
#endif
};
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
// Categories (niveau 1) — l'ordre = ordre d'affichage de l'ecran des categories.
enum {
CAT_CHANNELS = 0,
CAT_SCAN,
CAT_KEYS,
CAT_POWER,
CAT_DISPLAY,
CAT_TIMERS,
CAT_AUDIO,
CAT_RADIO,
CAT_DTMF,
CAT_SERVICE, // menu cache : n'apparait que si gF_LOCK
CAT_ALL, // liste plate complete, ordre et numeros d'origine
CAT_COUNT
};
#define MENU_LEVEL_CAT 0
#define MENU_LEVEL_ITEMS 1
extern const char *const CategoryNames[];
extern uint8_t gMenuCategory;
extern uint8_t gMenuLevel;
extern uint8_t gCatOrder[];
extern uint8_t gMenuCatCursor;
extern uint8_t gCatLastPos[];
void UI_MENU_BuildCategoryScreen(void);
uint8_t UI_MENU_CategoryItemCount(uint8_t cat);
#endif
extern const uint8_t FIRST_HIDDEN_MENU_ITEM;
extern const t_menu_item MenuList[];
@@ -249,6 +280,7 @@ extern const t_sidefunction gSubMenu_SIDEFUNCTIONS[];
extern bool gIsInSubMenu;
extern uint8_t gMenuCursor;
extern uint8_t gMenuIndices[];
extern int32_t gSubMenuSelection;
@@ -260,5 +292,7 @@ extern bool edit_is_uppercase;
void UI_DisplayMenu(void);
int UI_MENU_GetCurrentMenuId();
uint8_t UI_MENU_GetMenuIdx(uint8_t id);
uint8_t UI_MENU_GetViewPos(uint8_t id);
void UI_MENU_BuildView(void);
#endif
+5 -1
View File
@@ -74,16 +74,18 @@
"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,
"ENABLE_FEAT_F4HWN_LOGO": false,
"ENABLE_FEAT_F4HWN_LOGO_SAV": false,
"ENABLE_FEAT_F4HWN_MENU_CAT": false,
"ENABLE_AGC_SHOW_DATA": false,
"ENABLE_UART_RW_BK_REGS": false,
"ENABLE_SWD": false,
"VERSION_STRING_1": "v0.22",
"VERSION_STRING_2": "v5.8.0"
"VERSION_STRING_2": "v5.9.0"
}
},
{
@@ -209,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,
@@ -216,6 +219,7 @@
"ENABLE_FEAT_F4HWN_MEM": true,
"ENABLE_FEAT_F4HWN_LOGO": true,
"ENABLE_FEAT_F4HWN_LOGO_SAV": true,
"ENABLE_FEAT_F4HWN_MENU_CAT": true,
"ENABLE_SWD": true,
"EDITION_STRING": "Fusion",
"TARGET": "f4hwn.fusion"
+3 -1
View File
@@ -177,7 +177,8 @@ Fusion is the reference edition of the project. It provides an all-in-one firmwa
- scrolling signal-history graph,
- peak, minimum and trend indicators,
- 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).
- Integrated Morse Beacon transmitter with:
- `MOE`, `MOI`, `MOS`, `MOH`, `MO5` and `MO` identifiers,
- optional callsign identification,
@@ -185,6 +186,7 @@ Fusion is the reference edition of the project. It provides an all-in-one firmwa
- live TX and idle countdowns,
- persisted Beacon settings,
- interactive control during transmission,
- shared long-press `F` keypad lock,
- TX-lock, modulation and battery-safety checks.
- Fox Hunt and Beacon screens are mirrored to UV Studio's integrated K5Viewer.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -53,9 +53,10 @@ build_preset() {
docker run --rm \
-u $(id -u):$(id -g) \
-it -v "$PWD":/src -w /src "$IMAGE" \
bash -c "which arm-none-eabi-gcc && arm-none-eabi-gcc --version && \
cmake --preset ${preset} ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} && \
cmake --build --preset ${preset} -j"
bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version &&
cmake --preset "$1" "${@:2}" &&
cmake --build --preset "$1" -j' \
bash "${preset}" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}
echo "✅ Done: ${preset}"
}