diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 0fdd20ce..9919cec1 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -70,6 +70,7 @@ if(ENABLE_FEAT_F4HWN) "AUTHOR_STRING_2=\"${AUTHOR_STRING_2}\"" "VERSION_STRING_1=\"${VERSION_STRING_1}\"" "VERSION_STRING_2=\"${VERSION_STRING_2}\"" + "DISPLAY_VERSION_STRING_2=\"${DISPLAY_VERSION_STRING_2}\"" "EDITION_STRING=\"${EDITION_STRING}\"" ALERT_TOT=10 ) @@ -210,6 +211,9 @@ 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_RXTX_LOG_WRAP) enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER) diff --git a/App/app/action.c b/App/app/action.c index 97da7c59..27186e6d 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -48,6 +48,9 @@ #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG #include "app/rxtx_log.h" #endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + #include "app/foxhunt.h" +#endif #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); @@ -61,7 +64,7 @@ inline static void ACTION_1750() { ACTION_AlarmOr1750(true); }; inline static void ACTION_ScanRestart() { ACTION_Scan(true); }; -void (*action_opt_table[])(void) = { +void (*const action_opt_table[])(void) = { [ACTION_OPT_NONE] = &FUNCTION_NOP, [ACTION_OPT_POWER] = &ACTION_Power, [ACTION_OPT_MONITOR] = &ACTION_Monitor, @@ -137,6 +140,9 @@ void (*action_opt_table[])(void) = { #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG [ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog, #endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + [ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt, +#endif }; static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN); @@ -243,7 +249,7 @@ void ACTION_Scan(bool bRestart) // channel mode. Keep scanning but toggle between scan lists RADIO_NextValidList(1); - UI_MAIN_NotifyScanProgressDataChanged(); + UI_MAIN_NotifyScanListChanged(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); @@ -382,6 +388,9 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) #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; @@ -416,6 +425,12 @@ void ACTION_FM(void) gMonitor = false; + if (gScanStateDir != SCAN_OFF) { + // Stop the channel/frequency scan before switching to the FM radio. + gScanKeepResult = false; + CHFRSCANNER_Stop(); + } + RADIO_SelectVfos(); RADIO_SetupRegisters(true); diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 8ff485e5..b9790346 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -90,10 +90,11 @@ DECLARE_AIRCOPY_BANK(1) static const AIRCOPY_Segment_t AIRCOPY_Segments_Settings[] = { { 0xA000, 0xA170, AIRCOPY_WRITE_BYTES }, { 0x880E, 0x886E, AIRCOPY_WRITE_BYTES }, - { 0x9000, 0x90D6, AIRCOPY_WRITE_BYTES }, // VFO area (so scan-range source frequencies are replicated) + { 0x9000, 0x90E5, AIRCOPY_WRITE_BYTES }, // VFO area (full 14 VFOs 0x9000..0x90E0) + + // Fox Hunt settings tail 0x90E0..0x90E5 }; -// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xD6/64) = 6 + 2 + 4 = 12 +// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xE5/64) = 6 + 2 + 4 = 12 static const AIRCOPY_TransferMap_t AIRCOPY_Map_Settings = { .segments = AIRCOPY_Segments_Settings, .num_segments = 3, diff --git a/App/app/app.c b/App/app/app.c index e81e09ba..175e52e3 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -107,7 +107,7 @@ static KEY_Code_t gScreenSaverWakeKey = KEY_INVALID; static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); -void (*ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) = { +void (*const ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) = { [DISPLAY_MAIN] = &MAIN_ProcessKeys, [DISPLAY_MENU] = &MENU_ProcessKeys, [DISPLAY_SCANNER] = &SCANNER_ProcessKeys, diff --git a/App/app/breakout.c b/App/app/breakout.c index 6059eb63..ea0be553 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -16,6 +16,10 @@ #include "app/breakout.h" +#if defined(ENABLE_UART) || defined(ENABLE_USB) +#include "app/uart.h" +#endif + #ifdef ENABLE_FEAT_F4HWN_K5VIEWER #include "k5viewer.h" #endif @@ -359,6 +363,9 @@ void APP_RunBreakout(void) { while(isInitialized) { +#if defined(ENABLE_UART) || defined(ENABLE_USB) + UART_ServiceCommands(); +#endif Tick(); if(!isPaused) { diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index 0d6a2758..862370b6 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -771,13 +771,17 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi #endif if (IS_MR_CHANNEL(gNextMrChannel)) - { + { + bool scanListChanged = false; if(!RADIO_CheckValidList(gEeprom.SCAN_LIST_DEFAULT)) { RADIO_NextValidList(1); - UI_MAIN_NotifyScanProgressDataChanged(); + scanListChanged = true; } + if (storeBackupSettings || scanListChanged) + UI_MAIN_NotifyScanListChanged(); + // channel mode if (storeBackupSettings) { initialFrqOrChan = gRxVfo->CHANNEL_SAVE; diff --git a/App/app/foxhunt.c b/App/app/foxhunt.c new file mode 100644 index 00000000..9246c5ba --- /dev/null +++ b/App/app/foxhunt.c @@ -0,0 +1,1361 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "app/foxhunt.h" + +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + +#if defined(ENABLE_UART) || defined(ENABLE_USB) +#include "app/uart.h" +#endif + +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER +#include "k5viewer.h" +#endif + +// Signal window mapped onto the RSSI bar, in dBm. +// Roughly S0 (empty) to S9 + 40 dB (full), IARU VHF/UHF scale. +#define FOXHUNT_DBM_FLOOR (-141) +#define FOXHUNT_DBM_CEIL (-53) + +// Gauge geometry inside the main frame buffer (y in 0..55, status line excluded). +// 13 segments on the IARU S-meter scale: S1..S9 are 6 dB apart, the four marks +// past S9 (+10..+40) are 10 dB apart, so the axis compresses at the top. +#define FOXHUNT_BAR_X0 6 // leftmost segment x (centres the 13 segments) +#define FOXHUNT_SEG_COUNT 13 // S1..S9 + (+10..+40) +#define FOXHUNT_SEG_PITCH 9 // px between segment starts +#define FOXHUNT_SEG_W 8 // segment width +#define FOXHUNT_SEG_BOTTOM 37 // base the staircase grows up from + +// Refresh period and trend sampling window. +#define FOXHUNT_TICK_MS 50 +#define FOXHUNT_TREND_TICKS 20 // compare the level roughly once per second + +// 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 +// tick; one sample is stored every FOXHUNT_HIST_DECIM ticks, so the visible window +// spans LEN * DECIM * TICK_MS (120 * 3 * 50 ms = 18 s). +#define FOXHUNT_HIST_LEN 120 // samples kept (also the graph width in px) +#define FOXHUNT_HIST_DECIM 3 // ticks between stored samples (3 * 50 ms = 150 ms) +#define FOXHUNT_GRAPH_X0 4 // left column of the graph (frame-buffer x) +#define FOXHUNT_GRAPH_TOP 27 // top row (level 13, S9+40) +#define FOXHUNT_GRAPH_BOT 45 // baseline (axis) row +#define FOXHUNT_GRAPH_FLOOR 2 // gap between the level-0 line and the baseline, so + // the lowest reading never sits on the axis + +// Main-gauge mode cycled by the 2 key. +#define FOXHUNT_GRAPH_BAR 0 // S-meter staircase (default) +#define FOXHUNT_GRAPH_HIST 1 // full-width signal history + +// Geiger audio: the blip RATE tracks the signal level (faster = closer), the +// pitch rises too as a secondary cue. Below the silence floor, no blips at all. +#define FOXHUNT_AUDIO_SETTLE_MS 60 // audio amplifier warm-up after enabling the speaker +#define FOXHUNT_BLIP_MS 50 // tone duration per blip +#define FOXHUNT_RATE_SLOW_TICKS 20 // ticks between blips at the floor (~1/s) +#define FOXHUNT_RATE_FAST_TICKS 2 // ticks between blips at the ceiling (fast) +#define FOXHUNT_SILENCE_DBM (-120) // stay silent below this level (noise floor) +#define FOXHUNT_TONE_MIN 400 // Hz at the floor of the window +#define FOXHUNT_TONE_MAX 2400 // Hz at the ceiling of the window + +// Audio mode cycled by the 1 key: silent -> Geiger beep -> received station. +#define FOXHUNT_AUDIO_OFF 0 +#define FOXHUNT_AUDIO_BEEP 1 +#define FOXHUNT_AUDIO_STATION 2 + +// --- Beacon (fox) sub-mode --------------------------------------------------- +// 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 +// window; only the tone modulation is keyed on/off (EnterTxMute/ExitTxMute) = MCW on FM. +#define FOXHUNT_BEACON_TONE_HZ 1000 // CW tone pitch (Hz) +#define FOXHUNT_MORSE_UNIT_MS 100 // one Morse time unit (~12 WPM, ARDF pace) +#define FOXHUNT_BEACON_IDLE_DEF 30 // default silence between IDs (s) +#define FOXHUNT_BEACON_IDLE_MIN 5 // shortest idle gap (s) +#define FOXHUNT_BEACON_IDLE_MAX 240 // longest idle gap (s) — 4 min = the ARDF + // silence of a 5-fox cycle (60 s TX x4) +#define FOXHUNT_BEACON_IDLE_STEP 5 // idle adjust step (s) +// TX window: the ID is repeated for this many seconds each cycle (a real ARDF fox keys +// up for a fixed slot, not a single one-shot). 60 s = the classic ARDF slot. +#define FOXHUNT_BEACON_TX_DEF 30 // default TX window (s) +#define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s) +#define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s) +#define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s) +#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash +#define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon + +// Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5 +// dits: E I S H 5); MO is the ARDF finish/home beacon (transmitted continuously on a real +// course — here it rides the normal TX/IDLE cycle, so set a short IDLE for a near-constant +// MO); CALL prefixes the operator's callsign (legal ID on the ham bands): " MOE". +// Pure MOE..MO5 / MO (no callsign) suit casual / PMR fox games. +#define FOXHUNT_FOX_MOE 0 +#define FOXHUNT_FOX_MOI 1 +#define FOXHUNT_FOX_MOS 2 +#define FOXHUNT_FOX_MOH 3 +#define FOXHUNT_FOX_MO5 4 +#define FOXHUNT_FOX_MO 5 +#define FOXHUNT_FOX_CALL 6 +#define FOXHUNT_FOX_COUNT 7 + +// Framebuffer lines for the transmitted message (gFontBig spans two lines each): +// CALL id shows the callsign on _CALL and the "MOx" below on _ID; a pure single-word +// fox (MOE..MO5) is centred on _ONE. +#define FOXHUNT_MSG_LINE_CALL 1 +#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 + +static KeyboardState kbd = {KEY_INVALID, KEY_INVALID, 0}; + +static bool foxRunning; +static uint8_t foxAudioMode; +static uint8_t foxGraphMode; // FOXHUNT_GRAPH_BAR / _HIST, cycled by the 2 key +static uint8_t attStep; +static int16_t curDbm; +static int16_t peakDbm; +static int16_t minDbm; // lowest level held since the last reset (body-scan null) +static int16_t trendRef; +static int16_t trendDelta; +static uint8_t trendTick; +static uint8_t audioTick; + +// Signal-history ring: FOXHUNT_FillCount levels (0..13), oldest at histHead. +static uint8_t histBuf[FOXHUNT_HIST_LEN]; +static uint8_t histHead; // index of the oldest sample (next write slot) +static uint8_t histTick; // decimation counter (0..FOXHUNT_HIST_DECIM-1) +static int16_t histEma; // EMA-smoothed level feed, dBm in 1/8 units (x8 fixed) + +static char str[14]; // widest write is the freq "1300.00000" (10 chars) + NUL, with slack + +// Sentinel-prefixed Morse patterns: after the leading 1 sentinel bit, each lower +// bit is one element read MSB-first (0 = dit, 1 = dah). 0 = unsupported char. +static const uint8_t FOXHUNT_MORSE_LETTER[26] = { + 0x05, 0x18, 0x1A, 0x0C, 0x02, 0x12, 0x0E, 0x10, 0x04, 0x17, // A..J + 0x0D, 0x14, 0x07, 0x06, 0x0F, 0x16, 0x1D, 0x0A, 0x08, 0x03, // K..T + 0x09, 0x11, 0x0B, 0x19, 0x1B, 0x1C, // U..Z +}; +static const uint8_t FOXHUNT_MORSE_DIGIT[10] = { + 0x3F, 0x2F, 0x27, 0x23, 0x21, 0x20, 0x30, 0x38, 0x3C, 0x3E, // 0..9 +}; + +// 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) +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 +static uint8_t beaconIdleTick; // tick counter that clocks the idle countdown +static char beaconMsg[17]; // CW message: 12-char call + " MOE" + NUL, e.g. "F4HWN MOE" +static uint8_t beaconCharsSent; // characters completed in the current burst +static uint8_t beaconTx; // TX window length (s): the ID repeats for this long +static uint16_t beaconTxMsLeft; // ms left in the current TX window (drained as it plays) +static uint8_t beaconTxSecShown; // whole-second value last painted on the TX line +static uint8_t foxFox; // selected fox identifier (FOXHUNT_FOX_*) +static char foxCall[FOXHUNT_CALLSIGN_MAX + 1]; // sanitised callsign for the CALL id + +static void FOXHUNT_EnterHunt(void); +static void FOXHUNT_EnterBeacon(void); +static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft); +static void FOXHUNT_UpdateBeaconProgress(uint8_t visibleChars); +static void FOXHUNT_BuildBeaconMsg(void); +static void FOXHUNT_SaveConfig(void); + +// Read the calibrated signal level of the current RX VFO, in dBm. +static int16_t FOXHUNT_ReadDbm(void) +{ + return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; +} + +// Apply the selected attenuator step to the BK4829 PGA gain field. +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 + BK4819_WriteRegister(BK4819_REG_13, reg); +} + +// Set the speaker/AF state for the current audio mode. +static void FOXHUNT_SetAudio(void) +{ + if (foxAudioMode == FOXHUNT_AUDIO_OFF) { + AUDIO_AudioPathOff(); + return; + } + // Speaker on, let the amplifier warm up, RX audio muted for now. BEEP keeps it + // muted (only the blips are heard); STATION un-mutes per level in the loop. + AUDIO_AudioPathOn(); + SYSTEM_DelayMs(FOXHUNT_AUDIO_SETTLE_MS); + BK4819_SetAF(BK4819_AF_MUTE); +} + +// Clamped linear map of dbm over the [FLOOR, CEIL] window onto [lo, hi] (hi may sit +// below lo for an inverted mapping). Shared by the Geiger pitch and blip-rate ramps. +static int32_t FOXHUNT_LerpDbm(int16_t dbm, int32_t lo, int32_t hi) +{ + if (dbm <= FOXHUNT_DBM_FLOOR) return lo; + if (dbm >= FOXHUNT_DBM_CEIL) return hi; + return lo + ((int32_t)(dbm - FOXHUNT_DBM_FLOOR) * (hi - lo)) / + (FOXHUNT_DBM_CEIL - FOXHUNT_DBM_FLOOR); +} + +// Map a dBm value to a Geiger tone frequency (higher signal, higher pitch). +static uint16_t FOXHUNT_DbmToTone(int16_t dbm) +{ + return (uint16_t)FOXHUNT_LerpDbm(dbm, FOXHUNT_TONE_MIN, FOXHUNT_TONE_MAX); +} + +// Ticks between two blips: fewer ticks = faster clicking as the signal rises. +static uint8_t FOXHUNT_BlipPeriod(int16_t dbm) +{ + return (uint8_t)FOXHUNT_LerpDbm(dbm, FOXHUNT_RATE_SLOW_TICKS, FOXHUNT_RATE_FAST_TICKS); +} + +// Emit one short blip at the given pitch, then hand the chip back to RX. +// The speaker path is already on and settled (see the key-1 handler); between +// blips TurnsOffTones_TurnsOnRX mutes the AF, so the RX stays silent. +static void FOXHUNT_Blip(uint16_t freq) +{ + BK4819_PrepareToPlayTone(true); + BK4819_PlayToneRaw(freq, FOXHUNT_BLIP_MS); + BK4819_TurnsOffTones_TurnsOnRX(); + + // TurnsOffTones_TurnsOnRX restores a normal RX front-end, so re-assert the + // fixed gain and attenuator the level reading relies on. + BK4819_SetAGC(false); + FOXHUNT_ApplyAtt(); +} + +// Number of lit segments for a level, on the IARU S-meter scale: +// S1..S9 are 6 dB apart, the four marks past S9 (+10..+40) are 10 dB apart. +static uint8_t FOXHUNT_FillCount(int16_t dbm) +{ + int16_t n; + if (dbm < -141) return 0; + if (dbm <= -93) + n = 1 + (dbm + 141) / 6; // S1 (-141) .. S9 (-93) -> 1..9 + else + n = 9 + (dbm + 93) / 10; // +10 (-83) .. +40 (-53) -> 10..13 + if (n > FOXHUNT_SEG_COUNT) + n = FOXHUNT_SEG_COUNT; + return (uint8_t)n; +} + +// Build the IARU S-meter reading string (e.g. "S7" or "S9+30"). +static void FOXHUNT_BuildS(char *out, int16_t dbm) +{ + if (dbm >= -93) { + int16_t over = dbm - (-93); + if (over > 40) over = 40; + sprintf(out, "S9+%02d", over); + } else if (dbm < -141) { + sprintf(out, "S0"); + } else { + sprintf(out, "S%d", (dbm + 147) / 6); + } +} + +// Fill a solid rectangle, column by column. +static void FOXHUNT_FillRect(int16_t x0, int16_t y0, int16_t x1, int16_t y1, bool black) +{ + for (int16_t x = x0; x <= x1; x++) + UI_DrawLineBuffer(gFrameBuffer, x, y0, x, y1, black); +} + +static void FOXHUNT_DrawBar(void) +{ + uint8_t nCur = FOXHUNT_FillCount(curDbm); + uint8_t i; + + // Continuous S-meter staircase (no frame): each segment is 1 px taller than + // the previous, from S1 (1 px) to S9+40 (13 px), growing up from the base. + for (i = 0; i < nCur; i++) { + int16_t sx = FOXHUNT_BAR_X0 + i * FOXHUNT_SEG_PITCH; + FOXHUNT_FillRect(sx, FOXHUNT_SEG_BOTTOM - i, sx + FOXHUNT_SEG_W - 1, FOXHUNT_SEG_BOTTOM, true); + } + + // Solid separator line between the staircase and the scale labels below, + // with a 1 px gap above (y38) and below (y40) it. + UI_DrawLineBuffer(gFrameBuffer, FOXHUNT_BAR_X0, FOXHUNT_SEG_BOTTOM + 2, + FOXHUNT_BAR_X0 + (FOXHUNT_SEG_COUNT - 1) * FOXHUNT_SEG_PITCH + FOXHUNT_SEG_W - 1, + FOXHUNT_SEG_BOTTOM + 2, true); +} + +// Push one EMA-smoothed reading into the signal history, decimated so the visible +// window spans several seconds. Called once per hunt tick. +static void FOXHUNT_HistSample(void) +{ + // First-order IIR (EMA) on the raw dBm, in x8 fixed point so the /4 step does + // not stall a few dB short of the input. Tau ~4 ticks (~200 ms): kills the RSSI + // snow while staying responsive. + histEma += ((int16_t)curDbm * 8 - histEma) / 4; + + if (++histTick < FOXHUNT_HIST_DECIM) + return; + histTick = 0; + + histBuf[histHead] = FOXHUNT_FillCount((int16_t)(histEma / 8)); + histHead = (uint8_t)((histHead + 1) % FOXHUNT_HIST_LEN); +} + +// Draw the full-width signal-history graph, spectrum-style: a solid crest contour +// over a checkerboard-filled body, on the 13-level S scale. Oldest sample on the +// left, newest ("now") on the right. Replaces the staircase when the 2 key selects +// it. The level-0 line sits FOXHUNT_GRAPH_FLOOR px above the baseline so the lowest +// reading never touches the axis. +static void FOXHUNT_DrawHist(void) +{ + const uint8_t floorY = FOXHUNT_GRAPH_BOT - FOXHUNT_GRAPH_FLOOR; // level-0 line + const uint8_t span = floorY - FOXHUNT_GRAPH_TOP; // px for 13 levels + + // Baseline (axis) across the full width, below the level-0 line. + UI_DrawLineBuffer(gFrameBuffer, FOXHUNT_GRAPH_X0, FOXHUNT_GRAPH_BOT, + FOXHUNT_GRAPH_X0 + FOXHUNT_HIST_LEN - 1, FOXHUNT_GRAPH_BOT, true); + + uint8_t prevY = 0; + uint8_t idx = histHead; // ring index, wrapped by increment (no per-column modulo) + for (uint8_t c = 0; c < FOXHUNT_HIST_LEN; c++) { + uint8_t lvl = histBuf[idx]; // oldest -> newest + if (++idx >= FOXHUNT_HIST_LEN) + idx = 0; + if (lvl > FOXHUNT_SEG_COUNT) + lvl = FOXHUNT_SEG_COUNT; + uint8_t x = FOXHUNT_GRAPH_X0 + c; + uint8_t y = (uint8_t)(floorY - (lvl * span) / FOXHUNT_SEG_COUNT); // crest top + + // Checkerboard body from just under the crest down to the level-0 line + // (same pattern as the spectrum: ((x + y) & 1) == 0). + for (uint8_t yy = y + 1; yy <= floorY; yy++) + if (((x + yy) & 1) == 0) + UI_DrawLineBuffer(gFrameBuffer, x, yy, x, yy, true); + + // Solid crest, bridged to the previous sample so it reads as one contour. + if (c == 0) { + UI_DrawLineBuffer(gFrameBuffer, x, y, x, y, true); + } else { + uint8_t ylo = (y < prevY) ? y : prevY; + uint8_t yhi = (y < prevY) ? prevY : y; + UI_DrawLineBuffer(gFrameBuffer, x, ylo, x, yhi, true); + } + prevY = y; + } +} + +// Push the status line and the full frame buffer to the LCD, then mirror to K5Viewer. +// Factored out because the same argument-less sequence runs after every full redraw +// (hunt loop, beacon idle, beacon burst, TX-denied notice). +static void FOXHUNT_BlitScreen(void) +{ + ST7565_BlitStatusLine(); + ST7565_BlitFullScreen(); +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + K5VIEWER_Update(false); +#endif +} + +// Draw a left-anchored inverse tag in the smallest font (the PK / MN / ATT / beacon +// setting style). The box spans the text width, so the caller only gives x and the line. +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. +static void FOXHUNT_DrawFKey(void) +{ + if (gWasFKeyPressed) + memcpy(gStatusLine + 69, gFontF, sizeof(gFontF)); +} + +// Draw a string right-aligned in the small (7 px) font: its right edge lands at rightX. +static void FOXHUNT_DrawRightSmall(const char *s, uint8_t rightX, uint8_t line) +{ + UI_PrintStringSmallNormal(s, (uint8_t)(rightX - strlen(s) * 7), 0, line); +} + +// Battery icon plus the optional voltage/percentage text, top-right of the status +// line — shared by the hunt and beacon screens. +static void FOXHUNT_DrawStatusBattery(void) +{ + unsigned int bx = LCD_WIDTH - sizeof(BITMAP_BatteryLevel1); + UI_DrawBattery(gStatusLine + bx, gBatteryDisplayLevel, gLowBatteryBlink); + if (gSetting_battery_text != 0) { + if (gSetting_battery_text == 1) { // voltage + const uint16_t v = MIN(gBatteryVoltageAverage, 999); + sprintf(str, "%u.%02u", v / 100, v % 100); + } else { // percentage + sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage)); + } + bx -= 7 * strlen(str); + UI_PrintStringSmallBufferNormal(str, gStatusLine + bx); + } +} + +// Right-aligned frequency on the bottom line (line 6), shared by the hunt and +// beacon screens. +static void FOXHUNT_DrawFreqBR(uint32_t freq) +{ + sprintf(str, "%u.%05u", freq / 100000, freq % 100000); + FOXHUNT_DrawRightSmall(str, 126, 6); +} + +// Common beacon-screen frame: clear, the BEACON tag, the battery, and the TX +// frequency. Shared by the beacon draw and the TX-denied notice. +static void FOXHUNT_BeaconChrome(void) +{ + UI_DisplayClear(); + UI_StatusClear(); + GUI_DisplaySmallestInverse("BEACON", 2, 0, true, true, 26); + FOXHUNT_DrawStatusBattery(); + FOXHUNT_DrawFKey(); + FOXHUNT_DrawFreqBR(gTxVfo->pTX->Frequency); +} + +static void FOXHUNT_Draw(void) +{ + const uint8_t *trendIcon; + char sMeter[8]; + char big[8]; + + UI_DisplayClear(); + UI_StatusClear(); + + // Title as an inverse label, in the scan-list tag style. + GUI_DisplaySmallestInverse("FOX HUNT", 2, 0, true, true, 34); + + // Battery (icon + optional percentage/voltage) top-right, as on the main screens. + FOXHUNT_DrawStatusBattery(); + FOXHUNT_DrawFKey(); + + // Gauge-mode icon (2 key), between the label and the audio icon: ascending + // bars for the S-meter staircase, a sine wave for the signal history. Each + // bitmap has its own width, so copy its own size. + if (foxGraphMode == FOXHUNT_GRAPH_HIST) + memcpy(gStatusLine + 38, BITMAP_FoxHuntGraph, sizeof(BITMAP_FoxHuntGraph)); + else + memcpy(gStatusLine + 38, BITMAP_FoxHuntBars, sizeof(BITMAP_FoxHuntBars)); + + // Audio state icon between the label and the battery: speaker for beep, + // headphones for station audio, nothing when silent. + if (foxAudioMode == FOXHUNT_AUDIO_BEEP) + memcpy(gStatusLine + 55, BITMAP_FoxHuntSignal, sizeof(BITMAP_FoxHuntSignal)); + else if (foxAudioMode == FOXHUNT_AUDIO_STATION) + memcpy(gStatusLine + 55, BITMAP_FoxHuntSpeaker, sizeof(BITMAP_FoxHuntSpeaker)); + + // Hero level, floating: the big number renders naturally (left-anchored) and + // the "dBm" unit follows right after it, so the block flows with the width. + sprintf(big, "%d", curDbm); + UI_DisplayFrequency(big, 2, 0, false); + UI_PrintStringSmallNormal("dBm", (uint8_t)(strlen(big) * 13 + 4), 0, 1); + + // Trend as an arrow on line 0 (up = nearer, down = farther, = stable), + // right-anchored; the signed delta since ~1 s ago sits on line 1. + trendIcon = (trendDelta > 0) ? BITMAP_FoxHuntUp + : (trendDelta < 0) ? BITMAP_FoxHuntDown + : BITMAP_FoxHuntFlat; + memcpy(gFrameBuffer[0] + (126 - 11), trendIcon, 11); + // Signed delta on line 1 — only when it actually moved (no "00 dBm" when + // stable). "dBm" right-anchored to x126, the value 2 px to its left (a real + // space would be 7 px), so they are drawn as two separate strings. + if (trendDelta != 0) { + sprintf(str, "%+03d", trendDelta); // sign + 2 digits, e.g. "+05" / "-12" + UI_PrintStringSmallNormal("dBm", 127 - 3 * 7, 0, 1); + FOXHUNT_DrawRightSmall(str, 127 - 3 * 7 - 2, 1); // value left of "dBm" + } + + // Context line: peak hold (left), min hold (centre) and S-meter (right) as small + // inverse labels (scan-list tag style, 4 px/char). PK and MN bracket a body-scan + // rotation — the deeper MN sits below PK, the sharper the null. + sprintf(str, "PK %d", peakDbm); + FOXHUNT_Tag(str, 4, 2); + sprintf(str, "MN %d", minDbm); + FOXHUNT_Tag(str, (uint8_t)((LCD_WIDTH - strlen(str) * 4) / 2), 2); + FOXHUNT_BuildS(sMeter, curDbm); + GUI_DisplaySmallestInverse(sMeter, 126 - strlen(sMeter) * 4, 2, false, true, 125); + + // Main gauge, toggled by the 2 key: the S-meter staircase, or a full-width + // scrolling history of the level over the last seconds (same 13-level S scale). + if (foxGraphMode == FOXHUNT_GRAPH_HIST) { + FOXHUNT_DrawHist(); + } else { + // Segmented S-meter staircase. + FOXHUNT_DrawBar(); + + // Scale under the gauge, aligned with segments 1 / 5 / 9 / 13. + GUI_DisplaySmallest("S1", 6, 41, false, true); + GUI_DisplaySmallest("S5", 42, 41, false, true); + GUI_DisplaySmallest("S9", 78, 41, false, true); + GUI_DisplaySmallest("+40", 110, 41, false, true); + } + + // Attenuator as an inverse label (left), tuned frequency (right). + sprintf(str, "ATT %ddB", FOXHUNT_ATT_DB[attStep]); + FOXHUNT_Tag(str, 4, 6); + FOXHUNT_DrawFreqBR(gRxVfo->pRX->Frequency); +} + +// One key = one setting, each press cycling to the next value and wrapping — the same +// scheme for the hunt gauges and the beacon settings, so there are no navigation arrows +// to learn. dir is +1 for a plain press, -1 when the F key armed a reverse step. The +// beacon values are read where they take effect (TX window at the burst start, idle at +// the burst end, fox at the next repeat), so cycling any of them is safe at any time. + +// Wrap a 0..count-1 index one step forward (dir > 0) or backward, both ways round. +static uint8_t FOXHUNT_WrapStep(uint8_t v, uint8_t count, int8_t dir) +{ + return (uint8_t)((v + (dir > 0 ? 1u : (unsigned)(count - 1u))) % count); +} + +// Step a lo..hi value by step, wrapping lo<->hi at the ends. +static uint8_t FOXHUNT_RangeStep(uint8_t v, uint8_t lo, uint8_t hi, uint8_t step, int8_t dir) +{ + if (dir > 0) + return (v >= hi) ? lo : (uint8_t)(v + step); + return (v <= lo) ? hi : (uint8_t)(v - step); +} + +// Attenuator: 0 -> -6 -> -15 -> -27 dB (F reverses). +static void FOXHUNT_AttCycle(int8_t dir) +{ + attStep = FOXHUNT_WrapStep(attStep, 4, dir); + FOXHUNT_ApplyAtt(); +} + +// Fox identifier: MOE -> MOI -> MOS -> MOH -> MO5 -> MO -> CALL (F reverses). +static void FOXHUNT_FoxCycle(int8_t dir) +{ + foxFox = FOXHUNT_WrapStep(foxFox, FOXHUNT_FOX_COUNT, dir); +} + +// TX window: 5..60 s (F reverses). +static void FOXHUNT_TxCycle(int8_t dir) +{ + beaconTx = FOXHUNT_RangeStep(beaconTx, FOXHUNT_BEACON_TX_MIN, FOXHUNT_BEACON_TX_MAX, + FOXHUNT_BEACON_TX_STEP, dir); +} + +// Idle gap: 5..240 s (F reverses). +static void FOXHUNT_IdleCycle(int8_t dir) +{ + beaconIdle = FOXHUNT_RangeStep(beaconIdle, FOXHUNT_BEACON_IDLE_MIN, FOXHUNT_BEACON_IDLE_MAX, + FOXHUNT_BEACON_IDLE_STEP, dir); +} + +// Apply a beacon number key in the given direction; returns true when it changed a +// setting so the caller can refresh. Keys follow the on-screen layout: 1 = TX (top-left), +// 2 = IDLE (below it), 3 = FOX (right). Shared by both beacon phases. +static bool FOXHUNT_BeaconKey(KEY_Code_t key, int8_t dir) +{ + switch (key) { + case KEY_1: FOXHUNT_TxCycle(dir); return true; + case KEY_2: FOXHUNT_IdleCycle(dir); return true; + case KEY_3: FOXHUNT_FoxCycle(dir); return true; + default: return false; + } +} + +// Sleep one UI tick, but in 10 ms slices stepping the backlight fade each slice, +// so the fade rate matches the main screen (BACKLIGHT_Update runs every 10 ms in +// APP_TimeSlice10ms; delaying the whole 50 ms tick at once made it ~5x slower). +static void FOXHUNT_TickDelay(void) +{ + for (uint8_t i = 0; i < FOXHUNT_TICK_MS / 10; i++) { + SYSTEM_DelayMs(10); + BACKLIGHT_Update(); + } +} + +// 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. +static void FOXHUNT_IdleHousekeeping(void) +{ + if (!gNextTimeslice_500ms) + return; + gNextTimeslice_500ms = false; + + // This modal loop bypasses the foreground scheduler that normally samples the + // battery, which would otherwise freeze gBatteryDisplayLevel for the whole + // session. Refresh it here — every caller of this function has the PA off (the + // hunt loop, and the beacon idle gap, never a burst), so the reading is not + // pulled down by TX load — keeping the status icon live and letting the + // beacon's battery gate react to a pack draining under a long run. + BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent); + if (gBatteryVoltageIndex > 3) + gBatteryVoltageIndex = 0; + BATTERY_GetReadings(false); + + // Persist any changed setting within ~0.5 s, so it survives a power-off (not + // just a clean EXIT). No-op when nothing changed. + FOXHUNT_SaveConfig(); + + // Backlight timeout: switch it off when the countdown expires (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; + } +#endif +} + +static void FOXHUNT_HandleKeys(void) +{ + kbd.prev = kbd.current; + kbd.current = KEYBOARD_GetKey(); + + // 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 + + if (kbd.current == KEY_F) { // arm / disarm a reverse step for the next number key + gWasFKeyPressed = !gWasFKeyPressed; + return; + } + const int8_t dir = gWasFKeyPressed ? -1 : 1; + + switch (kbd.current) { + case KEY_EXIT: + foxRunning = false; + break; + case KEY_1: + // Toggle the main gauge: S-meter staircase <-> full-width history. + foxGraphMode ^= 1; + break; + case KEY_2: + // Cycle the audio: silent -> Geiger beep -> station audio (F reverses). + foxAudioMode = FOXHUNT_WrapStep(foxAudioMode, 3, dir); + FOXHUNT_SetAudio(); + if (foxAudioMode == FOXHUNT_AUDIO_BEEP) + audioTick = FOXHUNT_RATE_SLOW_TICKS; // blip promptly + break; + case KEY_3: + // Cycle the attenuator (0 -> -6 -> -15 -> -27 dB; F reverses). + FOXHUNT_AttCycle(dir); + break; + case KEY_MENU: + // Reset the peak / min hold and the trend reference (before each body scan). + peakDbm = curDbm; + 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; + } + + 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. +static void FOXHUNT_EnterHunt(void) +{ + foxBeacon = false; + + BK4819_SetAGC(false); + 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; + 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 +// RADIO_PrepareTX (F LOCK / per-VFO TX LOCK, then battery, then modulation) and +// return the matching VfoState_t. VFO_STATE_NORMAL means the burst is clear to +// transmit. Reusing the radio's own states keeps the on-screen wording identical +// to the main display (VfoStateStr). +static VfoState_t FOXHUNT_TxState(void) +{ + if (TX_freq_check(gTxVfo->pTX->Frequency) != 0 && gTxVfo->TX_LOCK) + return VFO_STATE_TX_DISABLE; + if (gBatteryDisplayLevel == 0) + return VFO_STATE_BAT_LOW; + if (gBatteryDisplayLevel > 6) + return VFO_STATE_VOLTAGE_HIGH; +#ifndef ENABLE_TX_WHEN_AM + if (gTxVfo->Modulation != MODULATION_FM) + return VFO_STATE_TX_DISABLE; +#endif + return VFO_STATE_NORMAL; +} + +// 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. +static void FOXHUNT_TxDeniedNotice(VfoState_t state) +{ + // Frame (clear + BEACON tag + battery + barred TX frequency). + FOXHUNT_BeaconChrome(); + + // Centred, gFontBig — same wording and font as the main screen's VFO state, + // on line 1 so it sits exactly where the beacon's "TX" / "IDLE" text appears. + UI_PrintString(VfoStateStr[state], 0, 127, 1, 8); + + FOXHUNT_BlitScreen(); + + // Hold the notice ~1 s while keeping the backlight fade alive. + for (uint8_t i = 0; i < 20; i++) + 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. +static void FOXHUNT_EnterBeacon(void) +{ + foxBeacon = true; + AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt + gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up + beaconPhaseTx = true; +} + +// Paint the TX line (line 0): "TX" left, remaining-window seconds right. Kept separate +// so FOXHUNT_TxDelay can refresh just this line each second, live, without redrawing the +// whole screen mid-Morse. Records the value shown so the caller only repaints on change. +static void FOXHUNT_DrawTxSeconds(void) +{ + const uint8_t sec = (uint8_t)((beaconTxMsLeft + 999u) / 1000u); + memset(gFrameBuffer[0], 0, sizeof(gFrameBuffer[0])); + UI_PrintStringSmallNormal("TX", 1, 0, 0); + sprintf(str, "%02us", sec); + FOXHUNT_DrawRightSmall(str, 127, 0); + beaconTxSecShown = sec; +} + +// 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 +// 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). +static bool FOXHUNT_TxDelay(uint16_t ms) +{ + while (ms) { + const uint16_t slice = (ms > 10) ? 10 : ms; // 10 ms: same fade rate as idle + SYSTEM_DelayMs(slice); + ms -= slice; + beaconTxMsLeft = (beaconTxMsLeft > slice) ? (beaconTxMsLeft - slice) : 0; + BACKLIGHT_Update(); // keep any brightness fade moving in TX + + // Tick the remaining-window seconds live: repaint only line 0 when the whole + // second changes, so the countdown drains smoothly through a long ID instead of + // jumping once per repeat (a one-line blit barely dents the Morse timing). + if ((uint8_t)((beaconTxMsLeft + 999u) / 1000u) != beaconTxSecShown) { + FOXHUNT_DrawTxSeconds(); + ST7565_BlitLine(0); + } + + kbd.prev = kbd.current; + kbd.current = KEYBOARD_GetKey(); + if (kbd.current == KEY_INVALID || kbd.current == kbd.prev) + continue; // rising edge only + + BACKLIGHT_TurnOn(); // keep the screen awake during TX + + 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 + 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. + return true; + } + if (kbd.current == KEY_F) { // arm / disarm the reverse step + gWasFKeyPressed = !gWasFKeyPressed; + FOXHUNT_BeaconDraw(true, 0); // reflect the F indicator in the status line + FOXHUNT_BlitScreen(); + continue; + } + if (FOXHUNT_BeaconKey(kbd.current, gWasFKeyPressed ? -1 : 1)) { + gWasFKeyPressed = false; // consumed; also clears the F indicator + FOXHUNT_BeaconDraw(true, 0); // show the new value immediately + FOXHUNT_BlitScreen(); + } + } + return false; +} + +// Morse pattern of one character (letters, digits, '/'); 0 = space / unsupported. +static uint8_t FOXHUNT_MorseByte(char c) +{ + if (c >= 'a' && c <= 'z') c -= 32; + if (c >= 'A' && c <= 'Z') return FOXHUNT_MORSE_LETTER[c - 'A']; + if (c >= '0' && c <= '9') return FOXHUNT_MORSE_DIGIT[c - '0']; + if (c == '/') return 0x32; // -..-. + return 0; +} + +// Send one character as modulated CW: key the running TX tone on per element, +// muting the modulation (carrier stays up) between them. Returns true if aborted. +static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars) +{ + uint8_t code = FOXHUNT_MorseByte(c); + + if (code == 0) { // space / unknown -> word gap + if (FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 4)) + return true; + FOXHUNT_UpdateBeaconProgress(visibleChars); + return false; + } + + uint8_t bit = 0x80; + while (!(code & bit)) bit >>= 1; // skip to the sentinel bit + for (bit >>= 1; bit; bit >>= 1) { // then walk the elements, MSB first + const uint16_t on = (code & bit) ? (FOXHUNT_MORSE_UNIT_MS * 3) // dah + : FOXHUNT_MORSE_UNIT_MS; // dit + BK4819_ExitTxMute(); + if (FOXHUNT_TxDelay(on)) { BK4819_EnterTxMute(); return true; } + BK4819_EnterTxMute(); + if (FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS)) return true; // intra gap + } + + // Reveal the character once all its CW elements have been transmitted. The + // remaining two units keep the completed character visible during the rest + // of the standard three-unit inter-character gap. + FOXHUNT_UpdateBeaconProgress(visibleChars); + + // Inter-character gap is 3 units; one already elapsed after the last element. + return FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 2); +} + +// One beacon cycle: repeat the CW identification for the whole TX window, at the TX +// VFO's configured power. Carrier stays up across the window; the tone is keyed per +// Morse element (MCW), and the ID is re-sent until the window drains — a real ARDF fox +// keys up for a fixed slot rather than sending a single one-shot. +static void FOXHUNT_BeaconTransmit(void) +{ + RADIO_SetTxParameters(); // key up: carrier + PA + + // Prime the tone generator, then start silent before the first element. + BK4819_TransmitTone(false, FOXHUNT_BEACON_TONE_HZ); + BK4819_EnterTxMute(); + + beaconTxMsLeft = (uint16_t)beaconTx * 1000u; // drained inside FOXHUNT_TxDelay + + bool stop = false; + while (!stop && beaconTxMsLeft > 0) { + // Rebuild from the current fox at the repeat boundary — this is where a FOX + // change made live (during the previous repeat or the idle gap) takes effect, + // never mid-message. + FOXHUNT_BuildBeaconMsg(); + + // Redraw once per repeat (remaining-window countdown + reset progress), in the + // muted gap so it never stretches a Morse element. Local LCD only here — the + // K5Viewer frame is pushed after the ID (below), when the text is complete. + beaconCharsSent = 0; + FOXHUNT_BeaconDraw(true, 0); + ST7565_BlitStatusLine(); + ST7565_BlitFullScreen(); + + for (uint8_t i = 0; !stop && beaconMsg[i]; i++) { + stop = FOXHUNT_MorseChar(beaconMsg[i], i + 1u); + +#if defined(ENABLE_UART) || defined(ENABLE_USB) + // Service maintenance commands in the muted inter-character gap. + UART_ServiceCommands(); +#endif + } + +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + // Service the K5Viewer link in the muted gap (never during a keyed element, so + // the Morse timing stays intact): ParseInput refills the keep-alive that Update + // drains — without it the link dies within ~15 repeats over a long TX window — + // and Update then mirrors the now-complete ID frame plus the live seconds. + if (!stop) { + K5VIEWER_ParseInput(); + K5VIEWER_Update(false); + } +#endif + + // Word gap before the next repeat, only while the window still has room. + if (!stop && beaconTxMsLeft > 0) + 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). + BK4819_EnterTxMute(); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + RADIO_SetupRegisters(true); +} + +// Build the CW message from the current fox id: " MOE" for CALL (falls back to a +// bare MOE when no callsign is set), otherwise the pure "MOx" of the chosen fox. +static void FOXHUNT_BuildBeaconMsg(void) +{ + if (foxFox == FOXHUNT_FOX_CALL) { + if (foxCall[0] != '\0') + sprintf(beaconMsg, "%s MOE", foxCall); + else + strcpy(beaconMsg, "MOE"); + } else if (foxFox == FOXHUNT_FOX_MO) { + strcpy(beaconMsg, "MO"); // ARDF finish/home beacon + } else { + sprintf(beaconMsg, "MO%c", FOXHUNT_FOX_TAIL[foxFox]); + } +} + +// Short label for the FOX setting: "FOX CALL" or "FOX MOx". +static void FOXHUNT_FoxLabel(char *out) +{ + if (foxFox == FOXHUNT_FOX_CALL) + strcpy(out, "FOX CALL"); + else if (foxFox == FOXHUNT_FOX_MO) + strcpy(out, "FOX MO"); + else + sprintf(out, "FOX MO%c", FOXHUNT_FOX_TAIL[foxFox]); +} + +// Draw the transmitted portion of the message. A CALL id splits on the space: callsign +// on the upper line, "MOx" below. A pure fox (no space) is centred on one line. Each +// revealed prefix keeps the anchor of the full string, so characters appear in place +// instead of re-centring as they arrive. +static void FOXHUNT_DrawBeaconMessage(uint8_t visibleChars) +{ + const char *separator = strchr(beaconMsg, ' '); + + if (separator == NULL) { // pure fox id (MOE..MO5) + const size_t idLen = strlen(beaconMsg); + const size_t visLen = MIN((size_t)visibleChars, idLen); + char id[sizeof(beaconMsg)]; + memcpy(id, beaconMsg, visLen); + id[visLen] = '\0'; + UI_PrintString(id, (uint8_t)((LCD_WIDTH - idLen * 8u) / 2u), 0, + FOXHUNT_MSG_LINE_ONE, 8); + return; + } + + const size_t callLen = (size_t)(separator - beaconMsg); + const size_t visibleCallLen = MIN((size_t)visibleChars, callLen); + char call[FOXHUNT_CALLSIGN_MAX + 1]; + memcpy(call, beaconMsg, visibleCallLen); + call[visibleCallLen] = '\0'; + UI_PrintString(call, (uint8_t)((LCD_WIDTH - callLen * 8u) / 2u), 0, + FOXHUNT_MSG_LINE_CALL, 8); + + if ((size_t)visibleChars > callLen + 1u) { + const char *id = separator + 1; + const size_t idLen = strlen(id); + const size_t visibleIdLen = MIN((size_t)visibleChars - callLen - 1u, idLen); + char idbuf[8]; + memcpy(idbuf, id, visibleIdLen); + idbuf[visibleIdLen] = '\0'; + UI_PrintString(idbuf, (uint8_t)((LCD_WIDTH - idLen * 8u) / 2u), 0, + FOXHUNT_MSG_LINE_ID, 8); + } +} + +// Update only the line pair whose latest character changed so screen refreshes do not +// lengthen the Morse timing more than necessary. +static void FOXHUNT_UpdateBeaconProgress(uint8_t visibleChars) +{ + const char *separator = strchr(beaconMsg, ' '); + uint8_t topLine; + + beaconCharsSent = visibleChars; + + if (separator == NULL) { // pure fox id + topLine = FOXHUNT_MSG_LINE_ONE; + } else { + const size_t callLen = (size_t)(separator - beaconMsg); + if ((size_t)visibleChars == callLen + 1u) // the space: nothing new to show + return; + topLine = ((size_t)visibleChars <= callLen) + ? FOXHUNT_MSG_LINE_CALL + : FOXHUNT_MSG_LINE_ID; + } + + memset(gFrameBuffer[topLine], 0, sizeof(gFrameBuffer[topLine])); + memset(gFrameBuffer[topLine + 1], 0, sizeof(gFrameBuffer[topLine + 1])); + FOXHUNT_DrawBeaconMessage(beaconCharsSent); + ST7565_BlitLine(topLine); + ST7565_BlitLine(topLine + 1); +} + +static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft) +{ + // Frame (clear + BEACON tag + battery + TX frequency on the bottom-right). + FOXHUNT_BeaconChrome(); + + if (txNow) { + // On-air icon in the status bar (only while the carrier is up), then the top + // line "TX" + remaining window seconds (refreshed live in FOXHUNT_TxDelay). + memcpy(gStatusLine + 48, BITMAP_FoxHuntTx, sizeof(BITMAP_FoxHuntTx)); + FOXHUNT_DrawTxSeconds(); + FOXHUNT_DrawBeaconMessage(beaconCharsSent); + } else { + // Silence phase: big "IDLE" with the countdown to the next burst below it. + UI_PrintString("IDLE", 0, 127, 1, 10); + sprintf(str, "%02us", idleLeft); + UI_PrintString(str, 0, 127, 3, 10); + } + + // Bottom-left column: 1 = TX window over 2 = IDLE gap. Bottom-right: 3 = FOX id over + // the freq (drawn by FOXHUNT_BeaconChrome). All three are inverse tags; each has its + // own number key, editable in either phase, so there is no cursor to show. + sprintf(str, "TX %us", beaconTx); + FOXHUNT_Tag(str, 4, 5); + sprintf(str, "IDLE %us", beaconIdle); + FOXHUNT_Tag(str, 4, 6); + + FOXHUNT_FoxLabel(str); + FOXHUNT_Tag(str, (uint8_t)(125 - strlen(str) * 4), 5); +} + +static void FOXHUNT_BeaconKeys(void) +{ + kbd.prev = kbd.current; + kbd.current = KEYBOARD_GetKey(); + + if (kbd.current == KEY_INVALID || kbd.current == kbd.prev) + return; + + BACKLIGHT_TurnOn(); // any keypress wakes the screen and re-arms the timers + + if (kbd.current == KEY_F) { // arm / disarm a reverse step for the next number key + gWasFKeyPressed = !gWasFKeyPressed; + return; + } + + 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 + break; + case KEY_MENU: + // Restart a full idle interval now (the TX-phase M drops here too). + beaconIdleLeft = beaconIdle; + beaconIdleTick = 0; + break; + default: + // 1 = TX window, 2 = idle gap, 3 = fox (F reverses); same keys as during TX. + FOXHUNT_BeaconKey(kbd.current, gWasFKeyPressed ? -1 : 1); + break; + } + + gWasFKeyPressed = false; // any non-F key consumes (or cancels) the reverse arm +} + +// One iteration of the beacon loop: either transmit a burst, or count down the +// editable silent gap. Kept off the hot RX path so hunt stays lightweight. +static void FOXHUNT_BeaconTick(void) +{ + if (beaconPhaseTx) { + // Re-assert the TX gates before every burst (RADIO_PrepareTX parity). F + // 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. + VfoState_t state = FOXHUNT_TxState(); + if (state != VFO_STATE_NORMAL) { + FOXHUNT_TxDeniedNotice(state); + foxBeacon = false; + beaconPhaseTx = false; + FOXHUNT_EnterHunt(); + 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). + FOXHUNT_BeaconTransmit(); + + beaconPhaseTx = false; + beaconIdleLeft = beaconIdle; + beaconIdleTick = 0; + + if (foxRunning && !foxBeacon) // side-key switched to hunt + FOXHUNT_EnterHunt(); + return; // EXIT (quit) falls through + } + + // Idle (silent) phase: responsive, editable countdown. +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + K5VIEWER_ParseInput(); +#endif + FOXHUNT_BeaconKeys(); + + if (!foxRunning) // EXIT: leave Fox Hunt + return; + if (!foxBeacon) { // side-key: back to the hunt + FOXHUNT_EnterHunt(); + return; + } + + FOXHUNT_BeaconDraw(false, beaconIdleLeft); + FOXHUNT_BlitScreen(); + + if (++beaconIdleTick >= (1000 / FOXHUNT_TICK_MS)) { // one second elapsed + beaconIdleTick = 0; + if (beaconIdleLeft > 0) + beaconIdleLeft--; + if (beaconIdleLeft == 0) + beaconPhaseTx = true; // next tick transmits + } + + FOXHUNT_IdleHousekeeping(); // backlight timeout (no sleep while beaconing) + FOXHUNT_TickDelay(); // tick delay + smooth backlight fade +} + +// --- Persistence (external flash) -------------------------------------------- +// Concatenated right after the VFO data in the VFO sector. NB: the 14 VFOs really +// span 0x9000..0x90E0 (224 B); the old 0x90D6 mapping bound was 10 B short and +// overlapped the last VFO (470 MHz VFO1 @ 0x90D0..0x90DF), so the config sits at +// 0x90E0, just past the real end. Normal saves are read-modify-write (Append=false) +// so they preserve these bytes, and the VFO mapping is extended to 0x90E7 +// (eeprom_compat.c) so aircopy clones them with the VFOs. A factory reset clears it. +#define FOXHUNT_CFG_ADDR 0x0090E0u +#define FOXHUNT_CFG_MAGIC 0xF4u // tells a written config from erased flash (0xFF) +#define FOXHUNT_CFG_LEN 7 // magic + att + graph + audio + idle + fox + tx + +// RAM mirror of the bytes last written to flash, so FOXHUNT_SaveConfig only touches +// the flash when a value actually changed. +static uint8_t foxCfgSaved[FOXHUNT_CFG_LEN]; + +// Pack the live settings into the on-flash layout. +static void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN]) +{ + out[0] = FOXHUNT_CFG_MAGIC; + out[1] = attStep; + out[2] = foxGraphMode; + out[3] = foxAudioMode; + out[4] = beaconIdle; + out[5] = foxFox; + out[6] = beaconTx; +} + +// Restore persisted settings; erased/invalid flash leaves the defaults in place. +static void FOXHUNT_LoadConfig(void) +{ + uint8_t cfg[FOXHUNT_CFG_LEN]; + 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[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 + && (cfg[4] % FOXHUNT_BEACON_IDLE_STEP) == 0) + beaconIdle = cfg[4]; + if (cfg[5] < FOXHUNT_FOX_COUNT) foxFox = cfg[5]; + if (cfg[6] >= FOXHUNT_BEACON_TX_MIN && cfg[6] <= FOXHUNT_BEACON_TX_MAX + && (cfg[6] % FOXHUNT_BEACON_TX_STEP) == 0) + beaconTx = cfg[6]; + } + + FOXHUNT_ConfigPack(foxCfgSaved); // mirror the loaded (or default) state +} + +// 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 +// compare avoids re-reading the 4 KB sector on every quiet tick. +static void FOXHUNT_SaveConfig(void) +{ + uint8_t cfg[FOXHUNT_CFG_LEN]; + FOXHUNT_ConfigPack(cfg); + + if (memcmp(cfg, foxCfgSaved, sizeof(cfg)) == 0) + return; + + PY25Q16_WriteBuffer(FOXHUNT_CFG_ADDR, cfg, sizeof(cfg), false); + memcpy(foxCfgSaved, cfg, sizeof(cfg)); +} + +void APP_RunFoxHunt(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. + 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). + 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. + + // 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. + kbd.prev = kbd.current = KEYBOARD_GetKey(); + gWasFKeyPressed = false; // start with the reverse-step arm cleared + + foxRunning = true; + 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; + + curDbm = FOXHUNT_ReadDbm(); + if (curDbm > peakDbm) + peakDbm = curDbm; + if (curDbm < minDbm) + minDbm = curDbm; + + if (++trendTick >= FOXHUNT_TREND_TICKS) { + trendDelta = curDbm - trendRef; + trendRef = curDbm; + trendTick = 0; + } + + FOXHUNT_HistSample(); // feed the smoothed signal-history sparkline + + FOXHUNT_Draw(); + + FOXHUNT_BlitScreen(); // status + full frame + K5Viewer mirror + + // Beep mode: Geiger blip whose rate/pitch track the level (silent below floor). + if (foxAudioMode == FOXHUNT_AUDIO_BEEP && curDbm >= FOXHUNT_SILENCE_DBM) { + if (++audioTick >= FOXHUNT_BlipPeriod(curDbm)) { + audioTick = 0; + FOXHUNT_Blip(FOXHUNT_DbmToTone(curDbm)); + } + } + // Station mode: route the received audio, gated by the silence floor. + else if (foxAudioMode == FOXHUNT_AUDIO_STATION) { + BK4819_SetAF(curDbm >= FOXHUNT_SILENCE_DBM + ? (gRxVfo->Modulation == MODULATION_AM ? BK4819_AF_AM : BK4819_AF_FM) + : BK4819_AF_MUTE); + } + + FOXHUNT_IdleHousekeeping(); // backlight timeout / sleep + 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). + 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) +{ + APP_RunFoxHunt(); + GUI_SelectNextDisplay(DISPLAY_MAIN); +} + +#endif // ENABLE_FEAT_F4HWN_FOXHUNT diff --git a/App/app/foxhunt.h b/App/app/foxhunt.h new file mode 100644 index 00000000..3743e6b1 --- /dev/null +++ b/App/app/foxhunt.h @@ -0,0 +1,57 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef APP_FOXHUNT_H +#define APP_FOXHUNT_H + +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + +#include "keyboard_state.h" + +#include "../bitmaps.h" +#include "../board.h" +#include "py32f0xx.h" +#include "../driver/backlight.h" +#include "../driver/bk4819-regs.h" +#include "../driver/bk4819.h" +#include "../driver/gpio.h" +#include "../driver/keyboard.h" +#include "../driver/py25q16.h" +#include "../driver/st7565.h" +#include "../driver/system.h" +#include "../driver/systick.h" +#include "../external/printf/printf.h" +#include "../font.h" +#include "../helper/battery.h" +#include "../misc.h" +#include "../radio.h" +#include "../settings.h" +#include "../ui/battery.h" +#include "../ui/helper.h" +#include "../ui/ui.h" +#include "../audio.h" +#include +#include +#include + +// Entry point wired as an assignable side-key action (see action.c). +void ACTION_FoxHunt(void); + +// Self-contained modal loop that owns the screen and keypad until EXIT. +void APP_RunFoxHunt(void); + +#endif // ENABLE_FEAT_F4HWN_FOXHUNT +#endif // APP_FOXHUNT_H diff --git a/App/app/main.c b/App/app/main.c index 4d8938db..1550de58 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -105,7 +105,7 @@ static void toggle_chan_scanlist(void) gTxVfo->SCANLIST_PARTICIPATION = scanlist; - SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, true, true); + SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true); } gVfoConfigureMode = VFO_CONFIGURE; @@ -225,6 +225,12 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) case KEY_4: HideFKeyIcon(); + if (gScanStateDir != SCAN_OFF) { + // Stop the channel/frequency scan before saving the RX mode for Close Call. + gScanKeepResult = false; + CHFRSCANNER_Stop(); + } + gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX; gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF; @@ -317,7 +323,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) if (gScanStateDir != SCAN_OFF) { RADIO_NextValidList(isKeyUp ? 1 : -1); - UI_MAIN_NotifyScanProgressDataChanged(); + UI_MAIN_NotifyScanListChanged(); } else { // Adjust squelch: UP increments, DOWN decrements if (gSquelchLevelOriginal == 10) @@ -500,7 +506,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if (value == 0) { gEeprom.SCAN_LIST_DEFAULT = MR_CHANNELS_LIST + 1; - UI_MAIN_NotifyScanProgressDataChanged(); + UI_MAIN_NotifyScanListChanged(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); #endif @@ -519,7 +525,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; RADIO_NextValidList(1); } - UI_MAIN_NotifyScanProgressDataChanged(); + UI_MAIN_NotifyScanListChanged(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); @@ -918,6 +924,12 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) return; } #endif + if (gScanStateDir != SCAN_OFF) { + // Stop the channel/frequency scan before saving the RX mode for the CTCSS/DCS scan. + gScanKeepResult = false; + CHFRSCANNER_Stop(); + } + // scan the CTCSS/DCS code gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX; gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF; diff --git a/App/app/menu.c b/App/app/menu.c index 1155455e..9bede201 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -725,7 +725,7 @@ void MENU_AcceptSetting(void) case MENU_LIST_CH: gTxVfo->SCANLIST_PARTICIPATION = gSubMenuSelection; - SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, false, true); + SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true); gVfoConfigureMode = VFO_CONFIGURE; gFlagResetVfos = true; return; @@ -752,7 +752,7 @@ void MENU_AcceptSetting(void) case MENU_COMPAND: gTxVfo->Compander = gSubMenuSelection; - SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, false, true); + SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true); gVfoConfigureMode = VFO_CONFIGURE; gFlagResetVfos = true; // gRequestSaveChannel = 1; @@ -864,7 +864,7 @@ void MENU_AcceptSetting(void) #endif case MENU_DEL_CH: - SETTINGS_UpdateChannel(gSubMenuSelection, NULL, false, false, true); + SETTINGS_UpdateChannel(gSubMenuSelection, NULL, false); gVfoConfigureMode = VFO_CONFIGURE_RELOAD; gFlagResetVfos = true; return; diff --git a/App/app/rxtx_log.c b/App/app/rxtx_log.c index 3b3e9e87..fd952035 100644 --- a/App/app/rxtx_log.c +++ b/App/app/rxtx_log.c @@ -136,6 +136,22 @@ static uint16_t gLogCursor; static uint8_t gLogFilter; static uint8_t gLogDetailMode; +bool RXTX_LOG_IsEnabled(void) +{ + return gEeprom.KEY_1_SHORT_PRESS_ACTION == ACTION_OPT_RXTX_LOG || + gEeprom.KEY_1_LONG_PRESS_ACTION == ACTION_OPT_RXTX_LOG || + gEeprom.KEY_2_SHORT_PRESS_ACTION == ACTION_OPT_RXTX_LOG || + gEeprom.KEY_2_LONG_PRESS_ACTION == ACTION_OPT_RXTX_LOG || + gEeprom.KEY_M_LONG_PRESS_ACTION == ACTION_OPT_RXTX_LOG; +} + +static void RXTX_LOG_ResetActiveSession(void) +{ + gSessionActive = false; + gSessionSMeter = RXTX_LOG_SMETER_UNKNOWN; + gSessionBattVolt = RXTX_LOG_BATT_UNKNOWN; +} + static uint8_t RXTX_LOG_Crc8(const void *data, uint16_t size) { const uint8_t *p = (const uint8_t *)data; @@ -755,6 +771,7 @@ uint32_t RXTX_LOG_K5ViewerSignature(void) { uint32_t hash = 2166136261u; + hash = RXTX_LOG_K5ViewerMix(hash, RXTX_LOG_IsEnabled()); hash = RXTX_LOG_K5ViewerMix(hash, gSessionActive); hash = RXTX_LOG_K5ViewerMix(hash, gClearActive); hash = RXTX_LOG_K5ViewerMix(hash, gLogHasTraffic); @@ -863,6 +880,13 @@ void RXTX_LOG_SendK5ViewerPacket(void (*send)(const uint8_t *data, uint16_t size RXTX_LogK5ViewerRow_t row; uint8_t header[4] = {RXTX_LOG_K5VIEWER_VERSION, 0, RXTX_LOG_K5VIEWER_ROW_COUNT, 0}; + if (!RXTX_LOG_IsEnabled()) { + header[1] = RXTX_LOG_K5VIEWER_STATUS_DISABLED; + header[2] = 0; + send(header, sizeof(header)); + return; + } + if (gSessionActive) header[1] |= RXTX_LOG_K5VIEWER_STATUS_ACTIVE; if (gLogHasTraffic) @@ -898,7 +922,7 @@ uint32_t RXTX_LOG_SendK5ViewerHistoryPage(uint32_t beforeSeq, void (*send)(const static void RXTX_LOG_CaptureSession(uint8_t flags, const VFO_Info_t *vfo) { - if (gClearActive) + if (!RXTX_LOG_IsEnabled() || vfo == NULL || gClearActive) return; const uint32_t frequency = (flags & RXTX_LOG_FLAG_TX) ? vfo->pTX->Frequency : vfo->pRX->Frequency; @@ -984,15 +1008,13 @@ void RXTX_LOG_Init(void) // Skip the marker if the log already ends with one (e.g. repeated // reboots with no RX/TX in between) to avoid stacking empty separators. - if (!found || (lastEntryFlags & RXTX_LOG_FLAG_SESSION) == 0) + if (RXTX_LOG_IsEnabled() && + (!found || (lastEntryFlags & RXTX_LOG_FLAG_SESSION) == 0)) RXTX_LOG_WriteSessionMarker(); } void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function) { - if (vfo == NULL) - return; - uint8_t flags = 0; if (function == FUNCTION_MONITOR) flags |= RXTX_LOG_FLAG_MONITOR; @@ -1002,9 +1024,6 @@ void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function) void RXTX_LOG_BeginTx(const VFO_Info_t *vfo) { - if (vfo == NULL) - return; - RXTX_LOG_CaptureSession(RXTX_LOG_FLAG_TX, vfo); } @@ -1013,10 +1032,8 @@ void RXTX_LOG_EndActive(void) if (!gSessionActive) return; - if (gClearActive) { - gSessionActive = false; - gSessionSMeter = RXTX_LOG_SMETER_UNKNOWN; - gSessionBattVolt = RXTX_LOG_BATT_UNKNOWN; + if (!RXTX_LOG_IsEnabled() || gClearActive) { + RXTX_LOG_ResetActiveSession(); return; } @@ -1040,9 +1057,7 @@ void RXTX_LOG_EndActive(void) gLogHasTraffic = true; RXTX_LOG_InvalidateViewCache(); - gSessionActive = false; - gSessionSMeter = RXTX_LOG_SMETER_UNKNOWN; - gSessionBattVolt = RXTX_LOG_BATT_UNKNOWN; + RXTX_LOG_ResetActiveSession(); } void RXTX_LOG_Tick500ms(void) @@ -1056,6 +1071,9 @@ void RXTX_LOG_Tick500ms(void) void RXTX_LOG_Task10ms(void) { + if (gSessionActive && !RXTX_LOG_IsEnabled()) + RXTX_LOG_ResetActiveSession(); + if (gClearActive) { RXTX_LOG_StepClear(); } else if (gViewScanActive) { diff --git a/App/app/rxtx_log.h b/App/app/rxtx_log.h index 0c849bb3..5fcc2a69 100644 --- a/App/app/rxtx_log.h +++ b/App/app/rxtx_log.h @@ -42,6 +42,8 @@ typedef struct { #define RXTX_LOG_K5VIEWER_STATUS_ACTIVE (1u << 0) #define RXTX_LOG_K5VIEWER_STATUS_HAS_TRAFFIC (1u << 1) #define RXTX_LOG_K5VIEWER_STATUS_CLEARING (1u << 2) +#define RXTX_LOG_K5VIEWER_STATUS_DISABLED (1u << 3) +#define RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE 4u typedef struct __attribute__((packed)) { uint32_t frequency; @@ -54,11 +56,11 @@ typedef struct __attribute__((packed)) { char channelName[RXTX_LOG_K5VIEWER_NAME_LENGTH]; } RXTX_LogK5ViewerRow_t; -// On-wire packet layout: 4-byte header (version, status, rowCount, -// reserved), one live row, then RXTX_LOG_K5VIEWER_ROW_COUNT row slots, -// zero-padded past the last valid row. The packet is streamed row by row -// and never built whole in RAM, so only its size is defined here. -#define RXTX_LOG_K5VIEWER_PACKET_SIZE (4u + ((RXTX_LOG_K5VIEWER_ROW_COUNT + 1u) * sizeof(RXTX_LogK5ViewerRow_t))) +// On-wire packet layout: a disabled log sends only the 4-byte status header. +// An enabled log continues with one live row and +// RXTX_LOG_K5VIEWER_ROW_COUNT row slots, zero-padded past the last valid row. +// The packet is streamed row by row and never built whole in RAM. +#define RXTX_LOG_K5VIEWER_PACKET_SIZE (RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE + ((RXTX_LOG_K5VIEWER_ROW_COUNT + 1u) * sizeof(RXTX_LogK5ViewerRow_t))) #define RXTX_LOG_K5VIEWER_HISTORY_PACKET_SIZE (RXTX_LOG_K5VIEWER_HISTORY_ROW_COUNT * sizeof(RXTX_LogK5ViewerRow_t)) uint32_t RXTX_LOG_K5ViewerSignature(void); @@ -70,6 +72,7 @@ uint32_t RXTX_LOG_SendK5ViewerHistoryPage(uint32_t beforeSeq, void (*send)(const #endif void RXTX_LOG_Init(void); +bool RXTX_LOG_IsEnabled(void); void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function); void RXTX_LOG_BeginTx(const VFO_Info_t *vfo); void RXTX_LOG_EndActive(void); diff --git a/App/app/spectrum.c b/App/app/spectrum.c index 29b51a52..f86cb8a1 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -18,6 +18,10 @@ #include "audio.h" #include "misc.h" +#if defined(ENABLE_UART) || defined(ENABLE_USB) +#include "app/uart.h" +#endif + #ifdef ENABLE_SCAN_RANGES #include "chFrScanner.h" #endif @@ -73,7 +77,7 @@ static uint16_t blacklistFreqs[15]; static uint8_t blacklistFreqsIdx; #endif -const char *bwOptions[] = {"25", "12.5", "6.25"}; +const char *const bwOptions[] = {"25", "12.5", "6.25"}; const uint8_t modulationTypeTuneSteps[] = {100, 50, 10}; const uint8_t modTypeReg47Values[] = {1, 7, 5}; @@ -176,7 +180,7 @@ char freqInputString[11]; uint8_t menuState = 0; uint16_t listenT = 0; -RegisterSpec registerSpecs[] = { +const RegisterSpec registerSpecs[] = { {}, {"LNAs", BK4819_REG_13, 8, 0b11, 1}, {"LNA", BK4819_REG_13, 5, 0b111, 1}, @@ -2608,6 +2612,9 @@ void APP_RunSpectrum() while (isInitialized) { +#if defined(ENABLE_UART) || defined(ENABLE_USB) + UART_ServiceCommands(); +#endif Tick(); } diff --git a/App/app/uart.c b/App/app/uart.c index 7d14e65e..5c052430 100644 --- a/App/app/uart.c +++ b/App/app/uart.c @@ -866,4 +866,17 @@ void UART_HandleCommand(uint32_t Port) #ifdef ENABLE_FEAT_F4HWN_K5VIEWER gUART_LockK5Viewer = 20; // lock the K5Viewer stream #endif -} \ No newline at end of file +} + +void UART_ServiceCommands(void) +{ +#ifdef ENABLE_USB + if (UART_IsCommandAvailable(UART_PORT_VCP)) + UART_HandleCommand(UART_PORT_VCP); +#endif + +#ifdef ENABLE_UART + if (UART_IsCommandAvailable(UART_PORT_UART)) + UART_HandleCommand(UART_PORT_UART); +#endif +} diff --git a/App/app/uart.h b/App/app/uart.h index cac62817..45f5e5ce 100644 --- a/App/app/uart.h +++ b/App/app/uart.h @@ -32,6 +32,6 @@ enum bool UART_IsCommandAvailable(uint32_t Port); void UART_HandleCommand(uint32_t Port); +void UART_ServiceCommands(void); #endif - diff --git a/App/bitmaps.c b/App/bitmaps.c index 4d25d970..141a7026 100644 --- a/App/bitmaps.c +++ b/App/bitmaps.c @@ -301,6 +301,135 @@ const uint8_t BITMAP_NOAA[12] = }; #endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT +const uint8_t BITMAP_FoxHuntSignal[10] = +{ // point source + the speaker bitmap's two sound waves, oriented horizontally + 0b00001000, + 0b00011100, + 0b00011100, + 0b00001000, + 0b00000000, + 0b00100010, + 0b00011100, + 0b01000001, + 0b00100010, + 0b00011100 +}; + +const uint8_t BITMAP_FoxHuntSpeaker[10] = +{ // speaker cone + two sound waves (bit0 = top row) + 0b00011100, + 0b00011100, + 0b00111110, + 0b01111111, + 0b00000000, + 0b00100010, + 0b00011100, + 0b01000001, + 0b00100010, + 0b00011100 +}; + +const uint8_t BITMAP_FoxHuntUp[11] = +{ // filled up triangle (getting nearer) + 0b00100000, + 0b00110000, + 0b00111000, + 0b00111100, + 0b00111110, + 0b00111111, + 0b00111110, + 0b00111100, + 0b00111000, + 0b00110000, + 0b00100000 +}; + +const uint8_t BITMAP_FoxHuntDown[11] = +{ // filled down triangle (getting farther) + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b00011111, + 0b00001111, + 0b00000111, + 0b00000011, + 0b00000001 +}; + +const uint8_t BITMAP_FoxHuntFlat[11] = +{ // equals sign (stable) + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010, + 0b00010010 +}; + +const uint8_t BITMAP_FoxHuntBars[11] = +{ // ascending bars = S-meter (staircase) gauge mode (bit0 = top row) + 0b01000000, + 0b01000000, + 0b00000000, + 0b01110000, + 0b01110000, + 0b00000000, + 0b01111100, + 0b01111100, + 0b00000000, + 0b01111111, + 0b01111111 +}; + +const uint8_t BITMAP_FoxHuntGraph[15] = +{ // sine wave = signal-history gauge mode (bit0 = top row) + 0b00001000, + 0b00000100, + 0b00000010, + 0b00000100, + 0b00001000, + 0b00010000, + 0b00100000, + 0b00010000, + 0b00001000, + 0b00000100, + 0b00000010, + 0b00000100, + 0b00001000, + 0b00010000, + 0b00100000 +}; + +const uint8_t BITMAP_FoxHuntTx[16] = +{ + 0b00011100, + 0b00100010, + 0b01000001, + 0b00011100, + 0b00100010, + 0b00000000, + 0b00001000, + 0b00011100, + 0b00011100, + 0b00001000, + 0b00000000, + 0b00100010, + 0b00011100, + 0b01000001, + 0b00100010, + 0b00011100 +}; +#endif + #ifndef ENABLE_CUSTOM_MENU_LAYOUT const uint8_t BITMAP_CurrentIndicator[8] = { 0xFF, diff --git a/App/bitmaps.h b/App/bitmaps.h index 88d1a7bd..f64c332e 100644 --- a/App/bitmaps.h +++ b/App/bitmaps.h @@ -45,6 +45,17 @@ extern const uint8_t BITMAP_compand[6]; extern const uint8_t BITMAP_NOAA[12]; +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + extern const uint8_t BITMAP_FoxHuntSignal[10]; + extern const uint8_t BITMAP_FoxHuntSpeaker[10]; + extern const uint8_t BITMAP_FoxHuntUp[11]; + extern const uint8_t BITMAP_FoxHuntDown[11]; + extern const uint8_t BITMAP_FoxHuntFlat[11]; + extern const uint8_t BITMAP_FoxHuntBars[11]; + extern const uint8_t BITMAP_FoxHuntGraph[15]; + extern const uint8_t BITMAP_FoxHuntTx[16]; +#endif + #ifndef ENABLE_CUSTOM_MENU_LAYOUT extern const uint8_t BITMAP_CurrentIndicator[8]; #endif diff --git a/App/driver/backlight.c b/App/driver/backlight.c index 7c59ad84..bbf9bab2 100644 --- a/App/driver/backlight.c +++ b/App/driver/backlight.c @@ -54,6 +54,17 @@ static int16_t currentBrightness = 0; static int16_t targetBrightness = 0; static int16_t fadeStep = 0; +static void BACKLIGHT_SetHardwareBrightness(uint8_t brightness); + +#ifdef ENABLE_FEAT_F4HWN + // Power-on fade-in tuning. + // STEPS = number of intermediate brightness stops (smoothness). + // STEP_MS = pacing between stops. + // STEPS * STEP_MS ~= total fade duration in ms. + #define BL_STARTUP_FADE_STEPS 40 + #define BL_STARTUP_FADE_STEP_MS 12 +#endif + #ifdef ENABLE_FEAT_F4HWN const uint8_t value[] = { 0, // 0 off @@ -130,6 +141,37 @@ void BACKLIGHT_UpdateTickless(void) { } } +#ifdef ENABLE_FEAT_F4HWN +// Soft, progressive power-on fade-in. +// Ramps from the current brightness (0 at power-on) to targetBrightness using +// a smoothstep (3x^2 - 2x^3) easing curve. Easing gently at both ends reads as +// a smooth, progressive fade instead of the abrupt linear ramp, and stepping +// through many fine stops keeps it fluid even with the 32-level PWM. +static void BACKLIGHT_FadeInStartup(void) +{ + const int16_t from = currentBrightness; // 0 at power-on + const int16_t span = targetBrightness - from; // ramp amplitude + + if (span <= 0) { + gUpdateBacklight = false; + return; + } + + for (uint16_t s = 1; s < BL_STARTUP_FADE_STEPS; s++) { + // x in Q8 (0..256), e = smoothstep(x) in Q16 (0..65536) + uint32_t x = (uint32_t)s * 256 / BL_STARTUP_FADE_STEPS; + uint32_t e = (x * x * (768 - 2 * x)) >> 8; + currentBrightness = from + (int16_t)(((uint32_t)span * e) >> 16); + BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness); + SYSTEM_DelayMs(BL_STARTUP_FADE_STEP_MS); + } + + currentBrightness = targetBrightness; + BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness); + gUpdateBacklight = false; +} +#endif + void BACKLIGHT_TurnOn(void) { #ifdef ENABLE_FEAT_F4HWN_SLEEP @@ -163,10 +205,11 @@ void BACKLIGHT_TurnOn(void) if(startup) #endif { - BACKLIGHT_UpdateTickless(); #ifdef ENABLE_FEAT_F4HWN + BACKLIGHT_FadeInStartup(); BACKLIGHT_Sound(); #else + BACKLIGHT_UpdateTickless(); startup = false; #endif } diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index d880e97c..ed9d9c7c 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -51,7 +51,13 @@ static const AddrMapping_t ADDR_MAPPINGS[] = { _MK_MAPPING(0x008000, 0x008000, 0x00886E), // 1024 MR + 7 VFO Attributes * 2 Bytes (ex 0x002000) 0x008000 -> 0x00880E // List name * 4 Bytes 0x00880E -> 0x00886E - _MK_MAPPING(0x009000, 0x009000, 0x0090D6), // 14 VFO * 16 Bytes (ex 0x001000) + _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) _MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010 // Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020 diff --git a/App/driver/py25q16.c b/App/driver/py25q16.c index 11e5f737..3d9990a1 100644 --- a/App/driver/py25q16.c +++ b/App/driver/py25q16.c @@ -284,9 +284,13 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b if (0 != memcmp(pBuffer, (char *)SectorCache + SecOffset, SecSize)) { bool Erase = false; + const uint8_t *oldData = SectorCache + SecOffset; + const uint8_t *newData = (const uint8_t *)pBuffer; + for (uint32_t i = 0; i < SecSize; i++) { - if (0xff != SectorCache[SecOffset + i]) + // NOR flash programming can only change bits from 1 to 0. + if ((oldData[i] & newData[i]) != newData[i]) { Erase = true; break; @@ -491,4 +495,4 @@ void DMA1_Channel4_5_6_7_IRQHandler() TC_Flag = true; } -} \ No newline at end of file +} diff --git a/App/driver/st7565.c b/App/driver/st7565.c index c488bb68..23d93e48 100644 --- a/App/driver/st7565.c +++ b/App/driver/st7565.c @@ -260,7 +260,7 @@ void ST7565_FillScreen(uint8_t value) // D=0, display OFF #define ST7565_CMD_DISPLAY_ON_OFF 0xAE -uint8_t cmds[] = { +const uint8_t cmds[] = { ST7565_CMD_BIAS_SELECT | 0, // Select bias setting: 1/9 ST7565_CMD_COM_DIRECTION | (0 << 3), // Set output direction of COM: normal ST7565_CMD_SEG_DIRECTION | 1, // Set scan direction of SEG: reverse diff --git a/App/k5viewer.c b/App/k5viewer.c index 683b8859..5752ad34 100644 --- a/App/k5viewer.c +++ b/App/k5viewer.c @@ -123,6 +123,9 @@ static uint8_t K5VIEWER_StateFlags(void) #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER static bool K5VIEWER_HasPendingRfLogUpdate(void) { + if (!RXTX_LOG_IsEnabled()) + rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START; + if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_RESTART) != 0) { gSerialViewerFeatures &= ~SERIAL_VIEWER_FEATURE_RF_LOG_RESTART; rfLogSent = false; @@ -157,14 +160,18 @@ static void K5VIEWER_SendRfLog(void) previousRfLogSignature = RXTX_LOG_K5ViewerSignature(); rfLogSent = true; - K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG, RXTX_LOG_K5VIEWER_PACKET_SIZE); + const uint16_t size = RXTX_LOG_IsEnabled() + ? RXTX_LOG_K5VIEWER_PACKET_SIZE + : RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE; + K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG, size); RXTX_LOG_SendK5ViewerPacket(K5VIEWER_Send); K5VIEWER_SendRfLogFrameEnd(); } static bool K5VIEWER_HasPendingRfLogHistory(void) { - return (gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY) != 0 && + return RXTX_LOG_IsEnabled() && + (gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY) != 0 && rfLogHistoryBefore != 0; } diff --git a/App/radio.h b/App/radio.h index e6ae7f01..5d53b399 100644 --- a/App/radio.h +++ b/App/radio.h @@ -149,6 +149,9 @@ extern DCS_CodeType_t gCurrentCodeType; extern VfoState_t VfoState[2]; +// Human-readable label per VfoState_t (defined in ui/main.c), e.g. "TX DISABLE". +extern const char *const VfoStateStr[]; + bool RADIO_CheckValidList(uint8_t scanList); void RADIO_NextValidList(int8_t direction); bool RADIO_CheckValidChannel(uint16_t channel, bool checkScanList, uint8_t scanList); diff --git a/App/settings.c b/App/settings.c index 83fb8182..6cd43647 100644 --- a/App/settings.c +++ b/App/settings.c @@ -1210,7 +1210,7 @@ void SETTINGS_SaveChannel(uint16_t Channel, uint8_t VFO, const VFO_Info_t *pVFO, PY25Q16_WriteBuffer(OffsetVFO, Buf, 0x10, false); - SETTINGS_UpdateChannel(Channel, pVFO, true, true, true); + SETTINGS_UpdateChannel(Channel, pVFO, true); if (IS_MR_CHANNEL(Channel)) { #ifndef ENABLE_KEEP_MEM_NAME @@ -1241,58 +1241,32 @@ void SETTINGS_SaveChannelName(uint16_t channel, const char * name) PY25Q16_WriteBuffer(0x004000 + offset, buf, 0x10, false); } -void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep, bool check, bool save) +void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep) { #ifdef ENABLE_NOAA - if (!IS_NOAA_CHANNEL(channel)) + if (IS_NOAA_CHANNEL(channel)) + return; #endif - { - ChannelAttributes_t state; - ChannelAttributes_t att = { - .band = 0x7, - .compander = 0, - .unused_1 = 0, - .unused_2 = 0, - .exclude = 0, - .scanlist = 0, - }; // default attributes - // 0x0D60 - PY25Q16_ReadBuffer(0x008000 + (channel * 2), &state, 2); + ChannelAttributes_t att = { + .band = 0x7, + .compander = 0, + .unused_1 = 0, + .unused_2 = 0, + .exclude = 0, + .scanlist = 0, + }; - if (keep) { - att.band = pVFO->Band; - att.compander = pVFO->Compander; - att.unused_1 = 0; - att.unused_2 = 0; - att.exclude = 0; - att.scanlist = pVFO->SCANLIST_PARTICIPATION; - if (check && state.__val == att.__val) - return; // no change in the attributes - } - - state.__val = att.__val; - -#ifndef ENABLE_FEAT_F4HWN - save = true; -#endif - if(save) - { - uint16_t buf[MR_CHANNELS_MAX + 24]; - PY25Q16_ReadBuffer(0x008000, buf, sizeof(buf)); - buf[channel] = state.__val; - PY25Q16_WriteBuffer(0x008000, buf, sizeof(buf), false); - } - - MR_SetChannelAttributes(channel, &att); - - if (IS_MR_CHANNEL(channel)) { // it's a memory channel - if (!keep) { - // clear/reset the channel name - SETTINGS_SaveChannelName(channel, ""); - } - } + if (keep) { + att.band = pVFO->Band; + att.compander = pVFO->Compander; + att.scanlist = pVFO->SCANLIST_PARTICIPATION; } + + MR_SetChannelAttributes(channel, &att); + + if (IS_MR_CHANNEL(channel) && !keep) + SETTINGS_SaveChannelName(channel, ""); } void SETTINGS_WriteBuildOptions(void) diff --git a/App/settings.h b/App/settings.h index 1f0153f9..a4e4bcb2 100644 --- a/App/settings.h +++ b/App/settings.h @@ -132,6 +132,9 @@ enum ACTION_OPT_t { #endif #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG ACTION_OPT_RXTX_LOG, +#endif +#ifdef ENABLE_FEAT_F4HWN_FOXHUNT + ACTION_OPT_FOXHUNT, #endif ACTION_OPT_LEN }; @@ -346,7 +349,7 @@ void SETTINGS_SaveSettings(void); void SETTINGS_SaveChannelName(uint16_t channel, const char * name); void SETTINGS_SaveChannel(uint16_t Channel, uint8_t VFO, const VFO_Info_t *pVFO, uint8_t Mode); void SETTINGS_SaveBatteryCalibration(const uint16_t * batteryCalibration); -void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep, bool check, bool save); +void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep); void SETTINGS_WriteBuildOptions(void); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE void SETTINGS_WriteCurrentState(void); diff --git a/App/ui/main.c b/App/ui/main.c index b5c860ac..66b1204c 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -68,6 +68,7 @@ center_line_t center_line = CENTER_LINE_NONE; #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS #define SCAN_PROGRESS_MR_CHANNEL_BYTES ((MR_CHANNELS_MAX + 7u) / 8u) +#define SCAN_LIST_NAME_HOLD_500MS (2000u / 500u) static bool gScanProgressSessionActive; static bool gScanProgressSessionIsMemory; @@ -82,6 +83,7 @@ static bool gScanProgressPrevResetVfosFlag; static bool gScanProgressForceRebuild; static uint16_t gScanProgressLastMemoryIndex; static uint8_t gScanProgressPriorityState; +static uint8_t gScanListNameCountdown_500ms; #define SCAN_PROGRESS_PRIORITY_LABEL_MASK 0x03u #define SCAN_PROGRESS_PRIORITY_SEEN_SHIFT 2 #define SCAN_PROGRESS_PRIORITY_SEEN_MASK 0x1cu @@ -90,15 +92,24 @@ static uint8_t gScanProgressPriorityState; #define SCAN_PROGRESS_PRIORITY_HOLD_FRAMES 6 #endif +#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_SCAN_PROGRESS) +// Shared center-line renderer: clear the line and print bold text on it. +static void UI_MAIN_DrawCenterBoldLine(const char *text, uint8_t start) +{ +#ifdef ENABLE_FEAT_F4HWN + const uint8_t line = isMainOnly() ? 5 : 3; +#else + const uint8_t line = 3; +#endif + memset(gFrameBuffer[line], 0, LCD_WIDTH); + UI_PrintStringSmallBold(text, start, LCD_WIDTH - 1, line); +} +#endif + #ifdef ENABLE_FEAT_F4HWN_BEAM static void UI_MAIN_DrawBeamLine(void) { const char *text; -#ifdef ENABLE_FEAT_F4HWN - const unsigned int line = isMainOnly() ? 5 : 3; -#else - const unsigned int line = 3; -#endif switch (gBeamStatus) { case BEAM_STATUS_TX_WAIT: @@ -125,12 +136,11 @@ static void UI_MAIN_DrawBeamLine(void) break; } - memset(gFrameBuffer[line], 0, LCD_WIDTH); - UI_PrintStringSmallBold(text, 2, 127, line); + UI_MAIN_DrawCenterBoldLine(text, 2); } #endif -const char *VfoStateStr[] = { +const char *const VfoStateStr[] = { [VFO_STATE_NORMAL]="", [VFO_STATE_BUSY]="BUSY", [VFO_STATE_BAT_LOW]="BAT LOW", @@ -214,6 +224,7 @@ static void ScanProgress_ResetSession(void) gScanProgressForceRebuild = false; gScanProgressLastMemoryIndex = 0; gScanProgressPriorityState = 0; + gScanListNameCountdown_500ms = 0; } void UI_MAIN_NotifyScanProgressDataChanged(void) @@ -222,6 +233,13 @@ void UI_MAIN_NotifyScanProgressDataChanged(void) gUpdateStatus = true; } +void UI_MAIN_NotifyScanListChanged(void) +{ + UI_MAIN_NotifyScanProgressDataChanged(); + gScanListNameCountdown_500ms = SCAN_LIST_NAME_HOLD_500MS; + gUpdateDisplay = true; +} + static inline void ScanProgress_SetBit(uint8_t *map, uint16_t ch) { map[ch >> 3] |= (uint8_t)(1u << (ch & 7)); @@ -243,6 +261,33 @@ static uint8_t ScanProgress_GetActiveScanList(void) return scan_list; } +static void UI_MAIN_DrawScanListName(void) +{ + const uint8_t scan_list = ScanProgress_GetActiveScanList(); + char text[16]; + + // Manual formatting instead of snprintf: much smaller on a divide-less M0 core + strcpy(text, "SCAN LIST "); + char *p = text + 10; // sizeof("SCAN LIST ") - 1 + + if (scan_list > MR_CHANNELS_LIST) { + *p++ = 'A'; *p++ = 'L'; *p++ = 'L'; + } else { + const char *name = gListName[scan_list - 1]; + + if (IsEmptyName(name, sizeof(gListName[0]))) { + *p++ = (char)('0' + scan_list / 10); + *p++ = (char)('0' + scan_list % 10); + } else { + for (uint8_t i = 0; i < 3 && name[i]; i++) + *p++ = name[i]; + } + } + *p = '\0'; + + UI_MAIN_DrawCenterBoldLine(text, 0); +} + static bool ScanProgress_ChannelBelongsToList(uint16_t channel, const ChannelAttributes_t *att, uint8_t scan_list) { if (!IS_MR_CHANNEL(channel)) @@ -490,6 +535,12 @@ static bool UI_DrawScanProgress(void) return false; } + // Right after a scan-list change, briefly show its name instead of the progress bar + if (show_memory && gScanListNameCountdown_500ms > 0) { + UI_MAIN_DrawScanListName(); + return true; + } + if (show_memory) { const uint8_t scan_list = ScanProgress_GetActiveScanList(); const bool reset_vfos_edge = gFlagResetVfos && !gScanProgressPrevResetVfosFlag; @@ -1167,6 +1218,10 @@ void UI_MAIN_PrintAGC(bool now) void UI_MAIN_TimeSlice500ms(void) { if(gScreenToDisplay==DISPLAY_MAIN) { +#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS + if (gScanListNameCountdown_500ms > 0 && --gScanListNameCountdown_500ms == 0) + gUpdateDisplay = true; +#endif #ifdef ENABLE_AGC_SHOW_DATA UI_MAIN_PrintAGC(true); return; @@ -1526,7 +1581,11 @@ void UI_DisplayMain(void) } } else { - if(RxOnVfofrequency == frequency && !isMainOnly()) { + if(RxOnVfofrequency == frequency && !isMainOnly() +#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI) + && !CHFRSCANNER_HasScanRssiSparkline() +#endif + ) { //UI_PrintStringSmallNormal(">>", 8, 0, line); //memcpy(p_line0 + 14, BITMAP_VFO_Default, sizeof(BITMAP_VFO_Default)); GUI_DisplaySmallest(">>", 8, RxLine == 0 ? 1 : 33, false, true); @@ -1538,11 +1597,6 @@ void UI_DisplayMain(void) #endif } -#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI) - if (vfo_num == gEeprom.RX_VFO && gScanStateDir != SCAN_OFF && !FUNCTION_IsRx()) - UI_MAIN_DrawScanRssiSparkline(line); -#endif - if((gScanStateDir == SCAN_OFF || vfo_num != gEeprom.RX_VFO) && TX_freq_check(frequency) != 0 && gEeprom.VfoInfo[vfo_num].TX_LOCK == true) { if (!FUNCTION_IsRx() || RxOnVfofrequency != frequency) @@ -1726,8 +1780,6 @@ void UI_DisplayMain(void) #endif #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - { - } } #endif @@ -2055,7 +2107,11 @@ void UI_DisplayMain(void) #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS const char dir_list[][2] = {"", "+", "-", "D"}; - if(gTxVfo->TX_OFFSET_FREQUENCY_DIRECTION != 0 && gTxVfo->pTX == &gTxVfo->freq_config_RX && !vfoInfo->FrequencyReverse) + if(gRemoveOffset && + vfoInfo == gTxVfo && + gTxVfo->TX_OFFSET_FREQUENCY_DIRECTION != 0 && + gTxVfo->pTX == &gTxVfo->freq_config_RX && + !vfoInfo->FrequencyReverse) { i = 3; } @@ -2198,6 +2254,11 @@ void UI_DisplayMain(void) #endif } +#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI) + if (gScanStateDir != SCAN_OFF && !FUNCTION_IsRx()) + UI_MAIN_DrawScanRssiSparkline(isMainOnly() ? 0 : (uint8_t)(gEeprom.RX_VFO * 4u)); +#endif + #ifdef ENABLE_AGC_SHOW_DATA center_line = CENTER_LINE_IN_USE; UI_MAIN_PrintAGC(false); diff --git a/App/ui/main.h b/App/ui/main.h index 43abb3aa..777b26f1 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -55,8 +55,10 @@ void UI_DisplayMain(void); #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS void UI_MAIN_NotifyScanProgressDataChanged(void); +void UI_MAIN_NotifyScanListChanged(void); #else static inline void UI_MAIN_NotifyScanProgressDataChanged(void) {} +static inline void UI_MAIN_NotifyScanListChanged(void) {} #endif #ifdef ENABLE_AGC_SHOW_DATA diff --git a/App/ui/menu.c b/App/ui/menu.c index 65975b1a..e171cce5 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -525,6 +525,9 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG {"RF LOG", ACTION_OPT_RXTX_LOG}, #endif + #ifdef ENABLE_FEAT_F4HWN_FOXHUNT + {"FOX HUNT\nBEACON", ACTION_OPT_FOXHUNT}, + #endif #endif }; @@ -1187,7 +1190,7 @@ void UI_DisplayMenu(void) if (page == p++) { // Page 0: firmware identity. #ifdef ENABLE_FEAT_F4HWN - sprintf(String, "%s\n%s", AUTHOR_STRING_2, VERSION_STRING_2); + sprintf(String, "%s\n%s", AUTHOR_STRING_2, DISPLAY_VERSION_STRING_2); UI_PrintStringSmallNormal(Edition, menu_item_x1 - 1, menu_item_x2, 6); #else sprintf(String, "%u.%02uV\n%u%%", diff --git a/App/ui/ui.c b/App/ui/ui.c index 116fa962..489c7613 100644 --- a/App/ui/ui.c +++ b/App/ui/ui.c @@ -48,7 +48,7 @@ bool gAskToSave; bool gAskToDelete; -void (*UI_DisplayFunctions[])(void) = { +void (*const UI_DisplayFunctions[])(void) = { [DISPLAY_MAIN] = &UI_DisplayMain, [DISPLAY_MENU] = &UI_DisplayMenu, [DISPLAY_SCANNER] = &UI_DisplayScanner, diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 798526c5..0547f468 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -20,6 +20,7 @@ #include "driver/py25q16.h" #include "driver/st7565.h" #include "external/printf/printf.h" +#include "font.h" #include "helper/battery.h" #include "settings.h" #include "misc.h" @@ -312,17 +313,33 @@ void UI_DisplayWelcome(void) UI_PrintString(WelcomeString1, 0, 127, 2, 10); #ifdef ENABLE_FEAT_F4HWN - UI_PrintStringSmallNormal(Version, 0, 128, 4); + const size_t version_width = strlen(DisplayVersion) * (ARRAY_SIZE(gFontSmall[0]) + 1u); + const uint8_t version_x = version_width < LCD_WIDTH + ? (uint8_t)((LCD_WIDTH - version_width + 1u) / 2u) + : 0u; + const uint8_t capsule_left = version_x > 2u ? (uint8_t)(version_x - 3u) : 0u; + const size_t capsule_right_candidate = version_x + version_width + 2u; + const uint8_t capsule_right = capsule_right_candidate < LCD_WIDTH + ? (uint8_t)capsule_right_candidate + : (LCD_WIDTH - 1u); - UI_DrawLineBuffer(gFrameBuffer, 0, 35, 18, 35, 1); - gFrameBuffer[4][19] ^= 0x7F; - for (uint8_t x = 20; x < 108; x++) + UI_PrintStringSmallNormal(DisplayVersion, version_x, 0, 4); + + if (capsule_left > 0u) + { + UI_DrawLineBuffer(gFrameBuffer, 0, 35, capsule_left - 1u, 35, 1); + } + gFrameBuffer[4][capsule_left] ^= 0x7F; + for (uint8_t x = capsule_left + 1u; x < capsule_right; x++) { gFrameBuffer[4][x] ^= 0xFF; gFrameBuffer[3][x] ^= 0x80; } - gFrameBuffer[4][108] ^= 0x7F; - UI_DrawLineBuffer(gFrameBuffer, 109, 35, 127, 35, 1); + gFrameBuffer[4][capsule_right] ^= 0x7F; + if (capsule_right < LCD_WIDTH - 1u) + { + UI_DrawLineBuffer(gFrameBuffer, capsule_right + 1u, 35, LCD_WIDTH - 1u, 35, 1); + } /* #ifdef ENABLE_FEAT_F4HWN_MEM diff --git a/App/version.c b/App/version.c index 5b854a05..31ef85af 100644 --- a/App/version.c +++ b/App/version.c @@ -6,11 +6,12 @@ #endif #ifdef ENABLE_FEAT_F4HWN - const char Version[] = AUTHOR_STRING_2 " " VERSION_STRING_2; - const char Edition[] = EDITION_STRING; - const char BuildDate[] = __DATE__; - const char BuildTime[] = __TIME__; - const char BuildCommit[] = BUILD_COMMIT; + const char Version[] = AUTHOR_STRING_2 " " VERSION_STRING_2; + const char DisplayVersion[] = AUTHOR_STRING_2 " " DISPLAY_VERSION_STRING_2; + const char Edition[] = EDITION_STRING; + const char BuildDate[] = __DATE__; + const char BuildTime[] = __TIME__; + const char BuildCommit[] = BUILD_COMMIT; #else const char Version[] = AUTHOR_STRING VER; #endif diff --git a/App/version.h b/App/version.h index 80467895..7b2a55c1 100644 --- a/App/version.h +++ b/App/version.h @@ -23,6 +23,7 @@ extern const char UART_Version[]; #endif #ifdef ENABLE_FEAT_F4HWN +extern const char DisplayVersion[]; extern const char Edition[]; extern const char BuildDate[]; extern const char BuildTime[]; diff --git a/CMakeLists.txt b/CMakeLists.txt index eb6ad2ed..0c13acd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,8 @@ message("Build type: " ${CMAKE_BUILD_TYPE}) enable_language(C ASM) +option(DEV "Prepare the rolling Fusion development firmware" OFF) + find_package(Git QUIET) set(BUILD_COMMIT "unknown") if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") @@ -71,7 +73,17 @@ else() set(EXE_NAME "firmware") endif() +if(DEV) + if(NOT "${TARGET}" STREQUAL "f4hwn.fusion") + message(FATAL_ERROR "DEV is only supported for the Fusion target") + endif() + set(DISPLAY_VERSION_STRING_2 "${VERSION_STRING_2} Dev") +else() + set(DISPLAY_VERSION_STRING_2 "${VERSION_STRING_2}") +endif() + message("EXE_NAME = " ${EXE_NAME}) +message("Development build = " ${DEV}) add_executable(${EXE_NAME}) @@ -110,6 +122,17 @@ add_custom_command( COMMENT "Generating .bin file" ) +if(DEV) + add_custom_command( + TARGET ${EXE_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_BINARY_DIR}/${EXE_NAME}.bin" + "${CMAKE_SOURCE_DIR}/archive/f4hwn.fusion.development.bin" + COMMENT "Preparing archive/f4hwn.fusion.development.bin" + ) +endif() + # Generate .hex file add_custom_command( TARGET ${EXE_NAME} diff --git a/CMakePresets.json b/CMakePresets.json index 960fed01..ec73c156 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,6 +9,7 @@ "toolchainFile": "${sourceDir}/cmake/gcc-arm-none-eabi.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", + "DEV": false, "ENABLE_LTO": false, "ENABLE_FMRADIO": false, "ENABLE_UART": true, @@ -72,6 +73,7 @@ "ENABLE_FEAT_F4HWN_SCAN_RSSI": false, "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false, "ENABLE_FEAT_F4HWN_BEAM": false, + "ENABLE_FEAT_F4HWN_FOXHUNT": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": false, @@ -81,7 +83,7 @@ "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, "VERSION_STRING_1": "v0.22", - "VERSION_STRING_2": "v5.7.0" + "VERSION_STRING_2": "v5.8.0" } }, { @@ -206,6 +208,7 @@ "ENABLE_FEAT_F4HWN_SCAN_RSSI": true, "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true, "ENABLE_FEAT_F4HWN_BEAM": true, + "ENABLE_FEAT_F4HWN_FOXHUNT": true, "ENABLE_FEAT_F4HWN_RXTX_LOG": true, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true, diff --git a/README.md b/README.md index 5617c0ab..6b9e0511 100644 --- a/README.md +++ b/README.md @@ -36,151 +36,238 @@ Anyway, have fun. > _FR - A propos de CHIRP, comme pour beaucoup d'autres firmwares, vous devez utiliser un pilote dédié. Le driver CHIRP correspondant est désormais fourni avec chaque release de ce dépôt, ce qui permet de récupérer ensemble le firmware et son pilote depuis la page des [Releases](https://github.com/armel/uv-k1-k5v3-firmware-custom/releases)._ > [!CAUTION] -> EN - I recommend to backup your calibration data with [uvtools2](https://armel.github.io/uvtools2/) just after flashing this firmware. It's a good reflex to have. +> EN - I recommend backing up your calibration data with [UV Studio](https://armel.github.io/uvstudio/#dump-calib) immediately after flashing this firmware. It is a good habit to adopt. > -> _FR - Je recommande de sauvegarder vos données de calibration avec [uvtools2](https://armel.github.io/uvtools2/) juste après avoir flashé ce firmware. C'est un bon réflexe à avoir._ +> _FR - Je recommande de sauvegarder vos données de calibration avec [UV Studio](https://armel.github.io/uvstudio/#dump-calib) juste après avoir flashé ce firmware. C'est un bon réflexe à adopter._ # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C / F4WFS (3 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN, Marcin Kusaj, Flavio Cottarelli, Bob N1MLZ and Carlos EA1IJ for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C / F4WFS (3 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN, Marcin Kusaj, Flavio Cottarelli, Bob N1MLZ, Carlos EA1IJ, Brian M7YLF, Giuseppe IT9LLH and 邓 月 for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents -* [My Features](#main-features) +* [Main features and improvements from F4HWN](#main-features-and-improvements-from-f4hwn) * [Main Features from Egzumer](#main-features-from-egzumer) * [Manual](#manual) * [Compiling and Building from Docker](#compiling-and-Building-from-docker) -* [Flashing the Firmware with UVTools2](#flashing-the-firmware-with-uvtools2) +* [Flashing the Firmware with UV Studio](#flashing-the-firmware-with-uv-studio) * [Credits](#credits) * [Other sources of information](#other-sources-of-information) * [License](#license) -## Main features and improvements from F4HWN: +## Main features and improvements from F4HWN -* Fusion is now the reference edition of the project: - * all-in-one firmware for UV-K1 and UV-K5 V3, - * spectrum analyzer made by Fagci, - * commercial FM radio support, - * Vox and Aircopy support, - * screenshots and K5Viewer support, - * advanced RX audio profiles and Audio Scope, - * first-responder oriented options, - * small breakout game, -* improve default power settings level: - * Low1 to Low5 (<~20mW, ~125mW, ~250mW, ~500mW, ~1W), - * Mid ~2W, - * High ~5W, - * User (see SetPwr), -* improve S-Meter (IARU Region 1 Technical Recommendation R.1 for VHF/UHF - [read more](https://hamwaves.com/decibel/en/)), - * S-Meter (S0/S9) Level EEPROM settings that were introduced in the Egzumer firmware are now ignored and replaced by hardcoded values to comply with the IARU Recommendation. -* improve bandscope (Spectrum Analyser): - * add channel name, - * add save of some spectrum parameters, -* improve UI: - * menu index is always visible, even if a menu is selected, - * s-meter new design (Classic or Tiny), - * MAIN ONLY screen mode, - * DUAL and CROSS screen mode, - * RX blink on VFO RX, - * RX LED blink, - * Squelch level and Monitor, - * Step value, - * CTCSS or DCS value, - * KeyLock message, - * last RX, - * move BatTxt menu from 34/63 to 30/63 (just after BatSave menu 29/63), - * rename BackLt to BLTime, - * rename BltTRX to BLTxRx, - * improve memory channel input, - * improve keyboard frequency input, - * add percent and gauge to Air Copy, - * improve audio bar, - * add backlight fading, - * add Audio Scope on TX, - * and more... -* new menu entries and changes: - * add SetPwr menu to set User power (<20mW, 125mW, 250mW, 500mW, 1W, 2W or 5W), - * add SetPTT menu to set PTT mode (Classic or OnePush), - * add SetTOT menu to set TOT alert (Off, Sound, Visual, All), - * add SetCtr menu to set contrast (0 to 15), - * add SetInv menu to set screen in invert mode (Off or On), - * add SetEOT menu to set EOT (End Of Transmission) alert (Off, Sound, Visual, All), - * add SetMet menu to set s-meter style (Classic or Tiny), - * add SetLck menu to set what is locked (Keys or Keys + PTT), - * add SetGUI menu to set font size on the VFO baseline (Classic or Tiny), - * add SetRxA menu to select RX audio profiles, - * add TXLock menu to open TX on channel, - * add SetTmr menu to set RX and TX timers (Off or On), - * add SetOff menu to set the delay before the transceiver goes into deep sleep (Off or 1 minute to 2 hours), - * add SetNFM menu to set Narrow width (12.5kHz or 6.25kHz), - * add SetVol menu to adjust RX audio volume, - * add SetScn menu to set Scan mode - * rename BatVol menu (52/63) to SysInf, which displays the firmware version in addition to the battery status, - * improve PonMsg menu, - * improve BackLt menu, - * improve TxTOut menu, - * improve ScnRev menu (CARRIER from 250ms to 20s, STOP, TIMEOUT from 5s to 2m) - * improve KeyLck menu (OFF, delay from 15s to 10m) - * add HAM CA F Lock band (for Canadian zone), - * add PMR 446 F Lock band, - * add FRS/GMRS/MURS F Lock band, - * add SetNav hidden menu to select the navigation layout according to the radio model, - * remove blink and SOS functionality, - * remove AM Fix menu (AM Fix is ENABLED by default), - * add support of 3500mAh battery, -* improve status bar: - * add SetPtt mode in status bar, - * change font and bitmaps, - * move USB icon to left of battery information, - * add RX and TX timers, -* improve channel scanning: - * support up to 24 scan lists, - * each memory channel can be assigned to `OFF`, to one list (`01` to `24`), or to `ALL`, - * `ALL` scans every channel except those set to `OFF`, - * named scan lists are shown in the UI and status bar when available, - * if the selected scan list is empty or invalid, the firmware automatically jumps to the next valid one, - * very fast scan mode (around 150 freq/s), - * frequencies exclusions, -* add resume mode on startup (scan, spectrum analyzer and FM radio), -* improve VFO persistence and restore behavior: - * save the Squelch level adjusted with F + UP or F + DOWN, - * restore the full VFO state on long press of EXIT, -* new actions: - * RX MODE, - * MAIN ONLY, - * PTT, - * WIDE NARROW, - * 1750Hz, - * MUTE, - * POWER HIGH, - * REMOVE OFFSET, - * BEAM, -* new key combinations: - * add the F + UP or F + DOWN key combination to dynamically change the Squelch level, - * add the F + F1 or F + F2 key combination to dynamically change the Step, - * add F + 8 to quickly switch backlight between BLMin and BLMax on demand (this bypass BackLt strategy), - * add F + 9 to return to BackLt strategy, - * add long press on MENU, in * SCAN mode, to exclude the current memory channel, - * add direct scan list selection while scanning with two digits (`00` = `ALL`, `01` to `24` = scan list). -* many fix: - * squelch, - * s-meter, - * DTMF overlaying, - * scan range limit, - * clean display on startup, - * no more PWM noise, - * K5Viewer/serial key handling, - * spectrum freeze on USB-C unplug, - * Audio Scope behavior in OnePush mode and after DTMF/1750, - * and more... -* enabled AIR COPY -* disabled ENABLE_DTMF_CALLING, -* disabled SCRAMBLER, -* remove 200Tx, 350Tx and 500Tx, -* unlock TX on all bands needs only to be repeat 3 times, -* code refactoring and many memory optimization, -* stream the live screen of the Quansheng K5 to K5Viewer and capture screenshots over a USB-to-Serial cable, -* and more... +### Fusion edition + +Fusion is the reference edition of the project. It provides an all-in-one firmware for the UV-K1 and UV-K5 V3, including: + +- Fagci's spectrum analyzer, +- broadcast FM radio, +- VOX and AirCopy, +- BEAM wireless channel transfer, +- [UV Studio](https://armel.github.io/uvstudio/) with integrated K5Viewer screen mirroring, screenshots and remote keyboard control, +- advanced RX audio profiles and Audio Scope, +- first-responder-oriented controls, +- the Breakout game, +- automatic RX/TX activity logging with RF Log, +- full Fox Hunt and Morse Beacon support. + +### Radio and signal handling + +- Reworked output-power levels: + - `Low 1`: below approximately 20 mW, + - `Low 2`: approximately 125 mW, + - `Low 3`: approximately 250 mW, + - `Low 4`: approximately 500 mW, + - `Low 5`: approximately 1 W, + - `Mid`: approximately 2 W, + - `High`: approximately 5 W, + - `User`: configurable through `SetPwr`. +- S-meter calibrated according to the [IARU Region 1 recommendation for VHF/UHF](https://hamwaves.com/decibel/en/): + - fixed S0 to S9+ values replace the former EEPROM S-meter thresholds, + - Classic and Tiny display styles are available. +- Configurable 12.5 kHz or 6.25 kHz narrow-FM bandwidth. +- Per-channel TX lock. +- Adjustable RX audio volume. +- Advanced RX audio profiles for FM and AM reception. +- Regional frequency-lock profiles for amateur bands, PMR446, FRS, GMRS and MURS. +- Support for 1600, 2200 and 3500 mAh battery profiles. + +### Spectrum analyzer + +- Channel names displayed in the spectrum view. +- Persistent spectrum settings. +- Faster and smoother spectrum rendering. +- Improved behavior when freezing the spectrum or disconnecting USB-C. +- Spectrum analyzer state can be restored automatically at startup. + +### Scanning + +- Support for up to 24 named scan lists. +- Each memory channel can be assigned to: + - `OFF`, + - one scan list from `01` to `24`, + - `ALL`. +- The `ALL` list scans every channel except those assigned to `OFF`. +- Automatic selection of the next valid list when the requested list is empty. +- Direct scan-list selection while scanning: + - `00` selects `ALL`, + - `01` to `24` select the corresponding list. +- Long press on `MENU` while scanning to exclude the current memory channel. +- Up to 64 frequency exclusions. +- Very fast scanning mode, reaching approximately 150 frequencies per second. +- Scan progress, RSSI and detected CTCSS/DCS information. +- Configurable scan resume behavior. +- Scan state can be restored automatically at startup. + +### User interface + +- Improved VFO screen with: + - Classic and Tiny S-meter styles, + - Classic and Tiny frequency-information layouts, + - `MAIN ONLY`, `DUAL` and `CROSS` display modes, + - RX activity indication on the active VFO, + - optional RX LED blinking, + - squelch, monitor, step and CTCSS/DCS information, + - last-RX indication, + - RX and TX timers. +- Improved status bar with updated fonts and icons. +- Menu index remains visible while editing an entry. +- Improved frequency and memory-channel input. +- Improved audio-level display. +- AirCopy progress percentage and gauge. +- Smooth backlight fading. +- Manual backlight controls for quickly switching between minimum and maximum brightness. +- Configurable contrast and inverted-display mode. +- Configurable navigation layout for the different radio models. +- Improved power-on message and optional startup logo. +- System-information pages for: + - firmware version and build information, + - battery information, + - Flash and SRAM usage, + - project and documentation QR codes. + +### Audio and transmission controls + +- Classic and OnePush PTT modes. +- Configurable timeout-timer alerts: + - disabled, + - sound, + - visual, + - sound and visual. +- Configurable end-of-transmission alerts using the same modes. +- Audio Scope during RX and TX. +- Improved Audio Scope behavior with OnePush PTT, DTMF and the 1750 Hz tone. +- Configurable automatic deep-sleep timeout. +- Quick actions for: + - RX mode, + - main-VFO-only display, + - PTT, + - wide/narrow bandwidth, + - 1750 Hz tone, + - mute, + - RX audio profile, + - maximum power, + - offset removal. + +### Fox Hunt and Beacon + +- Dedicated Fox Hunt receiver with: + - calibrated S-meter display, + - scrolling signal-history graph, + - peak, minimum and trend indicators, + - selectable RF attenuation, + - silent, Geiger-style and received-audio modes. +- Integrated Morse Beacon transmitter with: + - `MOE`, `MOI`, `MOS`, `MOH`, `MO5` and `MO` identifiers, + - optional callsign identification, + - configurable TX and idle periods, + - live TX and idle countdowns, + - persisted Beacon settings, + - interactive control during transmission, + - TX-lock, modulation and battery-safety checks. +- Fox Hunt and Beacon screens are mirrored to UV Studio's integrated K5Viewer. + +### RF Log + +- Automatic logging of RX and TX activity when RF Log is enabled. +- Logs stored in the radio's external Flash memory. +- Recorded information includes: + - RX or TX direction, + - frequency and channel, + - channel name, + - activity duration, + - S-meter level, + - battery voltage. +- On-radio history with RX/TX filtering and detailed views. +- Live RF Log dashboard and history access through UV Studio. + +### Connectivity and data transfer + +- Live screen streaming to [UV Studio](https://armel.github.io/uvstudio/) over a USB serial connection. +- Screenshot capture and download. +- Remote keyboard control through the integrated K5Viewer. +- Automatic reconnection after a USB disconnect. +- RF Log monitoring, analytics and CSV export. +- Firmware flashing, calibration backup and restore, and boot-logo management from the same interface. +- BEAM transfer of complete channel settings between compatible radios. +- Improved AirCopy interface and progress reporting. + +### Settings and controls + +- New or extended menu entries: + - `SetPwr`: configurable User output power, + - `SetPTT`: Classic or OnePush PTT, + - `SetTOT`: timeout-timer alert, + - `SetEOT`: end-of-transmission alert, + - `SetCtr`: display contrast, + - `SetInv`: inverted display, + - `SetLck`: keypad or keypad-and-PTT lock, + - `SetMet`: S-meter style, + - `SetGUI`: VFO information style, + - `SetRxA`: RX audio profile, + - `SetTmr`: RX and TX timers, + - `SetOff`: automatic deep-sleep delay, + - `SetNFM`: narrow-FM bandwidth, + - `SetVol`: RX audio volume, + - `SetScn`: scan mode, + - `SetNav`: radio-specific navigation layout. +- Improved `PonMsg`, `BackLt`, `TxTOut`, `ScnRev` and `KeyLck` menus. +- Full VFO state restoration with a long press on `EXIT`. +- Squelch changes made with `F + UP` or `F + DOWN` are persisted. + +### Keyboard shortcuts and assignable actions + +- `F + UP` or `F + DOWN`: adjust the squelch level. +- `F + F1` or `F + F2`: adjust the frequency step. +- `F + 8`: temporarily switch the backlight between minimum and maximum brightness. +- `F + 9`: return to the configured backlight strategy. +- Configurable short- and long-press actions include: + - RX mode, + - main-VFO-only display, + - virtual PTT, + - wide/narrow bandwidth, + - 1750 Hz tone, + - mute, + - RX audio profile, + - maximum power, + - offset removal, + - BEAM, + - RF Log, + - Fox Hunt / Beacon. + +### Reliability and optimization + +- Improved squelch and S-meter behavior. +- Fixed DTMF overlay issues. +- Fixed scan-range limits. +- Cleaner startup display. +- Removed PWM-related audio noise. +- Improved serial and K5Viewer key handling. +- Improved VFO persistence and restoration. +- Extensive code refactoring and memory optimization. +- DTMF calling and the scrambler remain disabled in Fusion. +- Legacy AM Fix code is not used by this firmware. ## Main features from Egzumer: * many of OneOfEleven mods: @@ -284,35 +371,46 @@ Examples: ./compile-with-docker.sh Fusion -DSQL_TONE=600 ``` +To prepare the rolling development firmware: + +```bash +./compile-with-docker.sh Fusion -DDEV=ON +``` + +This keeps the regular build output in `build/Fusion` and also updates +`archive/f4hwn.fusion.development.bin`. The development build is identified as +`DEV` in the firmware information screen. Publishing the updated archive file +remains an explicit Git operation. + ### Notes - The first run may take a few minutes while Docker builds the base image. - Each build runs inside Docker, so your host environment remains clean. -## Flashing the Firmware with UVTools2 +## Flashing the Firmware with UV Studio -You can flash the UV-K5 V3 and UV-K1 directly from your web browser using the cross-platform WebSerial-based [UVTools2](https://armel.github.io/uvtools2/). +You can flash the UV-K5 V3 and UV-K1 directly from your web browser using the Web Serial-based [UV Studio](https://armel.github.io/uvstudio/). -It works on Chrome, Chromium and Edge (desktop versions), and does not require installing any driver or software on your computer. +UV Studio combines firmware flashing, calibration maintenance, boot-logo management, K5Viewer and RF Log in a single interface. It requires no application installation, server or account. Use a desktop browser with Web Serial support, such as Chrome, Brave, Edge, Opera or Firefox 151+. ## Steps to flash the firmware -- Open UVTools2 in [flash](https://armel.github.io/uvtools2/?mode=flash) mode (or click the Flash Firmware tab). +- Open the [Flash Firmware](https://armel.github.io/uvstudio/#flash) view in UV Studio. - Connect your radio to your computer using a compatible USB programming cable (USB-C or Baofeng/Kenwood like double jack USB cable). - Make sure your radio is in **DFU mode (flash mode)**. -- Select the firmware .bin file on your computer. +- Select an official F4HWN Fusion release from the catalog or load a local `.bin` firmware file. - Click on `Flash Firmware`, then select the serial port associated with your radio. - The progress bar will guide you through the flashing steps. -Once finished, your radio restart with the new firmware. +Once finished, your radio restarts with the new firmware. ## Steps to dump or restore calibration data -[UVTools2](https://armel.github.io/uvtools2/) can also dump and restore calibration data, which is highly recommended. It’s best to create a dump right after installing F4HWN firmware, and to restore it before installing another firmware (or when returning to the stock firmware, for example). +[UV Studio](https://armel.github.io/uvstudio/) can also dump and restore calibration data, which is highly recommended. It is best to create a dump immediately after installing the F4HWN firmware, and to restore it before installing another firmware or returning to the stock firmware. ### Dump -- Open UVTools2 in [dump](https://armel.github.io/uvtools2/?mode=dump) mode (or click the Dump Calib tab). +- Open the [Dump Calibration](https://armel.github.io/uvstudio/#dump-calib) view in UV Studio. - Power on your radio in **normal mode**. - Click `Dump Calibration Data`. @@ -323,9 +421,9 @@ When the process is complete, click `Download calibration.dat` to save the file ### Restore -- Open UVTools2 in [restore](https://armel.github.io/uvtools2/?mode=restore) mode (or click the Restore Calib tab). +- Open the [Restore Calibration](https://armel.github.io/uvstudio/#restore-calib) view in UV Studio. - Power on your radio in **normal mode**. -- Select your calibration.dat file on your computer. +- Select your `calibration.dat` file on your computer. Click `Restore Calibration Data` and wait until the process fully completes. diff --git a/archive/f4hwn.fusion.development.bin b/archive/f4hwn.fusion.development.bin new file mode 100755 index 00000000..9461d5b3 Binary files /dev/null and b/archive/f4hwn.fusion.development.bin differ diff --git a/archive/f4hwn.fusion.v5.8.0.bin b/archive/f4hwn.fusion.v5.8.0.bin new file mode 100755 index 00000000..04e70d51 Binary files /dev/null and b/archive/f4hwn.fusion.v5.8.0.bin differ diff --git a/archive/f4hwn.fusion.v5.8.0.sa818.bin b/archive/f4hwn.fusion.v5.8.0.sa818.bin new file mode 100755 index 00000000..d4c86c66 Binary files /dev/null and b/archive/f4hwn.fusion.v5.8.0.sa818.bin differ diff --git a/compile-with-docker.sh b/compile-with-docker.sh index 3dfd0b1f..aca105a6 100755 --- a/compile-with-docker.sh +++ b/compile-with-docker.sh @@ -8,6 +8,7 @@ set -euo pipefail # ./compile-with-docker.sh Custom # ./compile-with-docker.sh Bandscope -DENABLE_SPECTRUM=ON # ./compile-with-docker.sh Broadcast -DENABLE_FEAT_F4HWN_GAME=ON -DENABLE_NOAA=ON +# ./compile-with-docker.sh Fusion -DDEV=ON # ./compile-with-docker.sh All # Default preset: "Custom" # ---------------------------------------------