From e9181c22876d854bdaaaa97a83fc4dc86fe8c8ed Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 2 Aug 2026 05:01:20 +0200 Subject: [PATCH] Add Fox Hunt signal-history gauge and persistent settings --- App/app/aircopy.c | 5 +- App/app/foxhunt.c | 223 +++++++++++++++++++++++++++++++++---- App/bitmaps.c | 22 ++++ App/bitmaps.h | 2 + App/driver/eeprom_compat.c | 8 +- 5 files changed, 237 insertions(+), 23 deletions(-) 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/foxhunt.c b/App/app/foxhunt.c index 12522aae..37fc6e27 100644 --- a/App/app/foxhunt.c +++ b/App/app/foxhunt.c @@ -40,6 +40,23 @@ #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 @@ -80,6 +97,7 @@ 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; @@ -88,6 +106,12 @@ 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[16]; // Sentinel-prefixed Morse patterns: after the leading 1 sentinel bit, each lower @@ -112,6 +136,7 @@ static char beaconMsg[24]; // CW message, e.g. "F4HWN BEACON" static void FOXHUNT_EnterHunt(void); static void FOXHUNT_EnterBeacon(void); static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft); +static void FOXHUNT_SaveConfig(void); // Read the calibrated signal level of the current RX VFO, in dBm. static int16_t FOXHUNT_ReadDbm(void) @@ -231,6 +256,63 @@ static void FOXHUNT_DrawBar(void) 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; + for (uint8_t c = 0; c < FOXHUNT_HIST_LEN; c++) { + uint8_t lvl = histBuf[(histHead + c) % FOXHUNT_HIST_LEN]; // oldest -> newest + 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; + } +} + // 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) @@ -264,6 +346,14 @@ static void FOXHUNT_Draw(void) // Battery (icon + optional percentage/voltage) top-right, as on the main screens. FOXHUNT_DrawStatusBattery(); + // 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) @@ -292,21 +382,27 @@ static void FOXHUNT_Draw(void) UI_PrintStringSmallNormal(str, 127 - 3 * 7 - 2 - strlen(str) * 7, 0, 1); } - // Context line: peak hold and S-meter as small inverse labels (scan-list tag - // style, 4 px/char), PK anchored left and S anchored right. + // Context line: peak hold (left) and S-meter (right) as small inverse labels + // (scan-list tag style, 4 px/char). sprintf(str, "PK %d", peakDbm); GUI_DisplaySmallestInverse(str, 4, 2, false, true, 4 + strlen(str) * 4); FOXHUNT_BuildS(sMeter, curDbm); GUI_DisplaySmallestInverse(sMeter, 126 - strlen(sMeter) * 4, 2, false, true, 125); - // Segmented S-meter staircase. - FOXHUNT_DrawBar(); + // 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); + // 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]); @@ -376,6 +472,10 @@ static void FOXHUNT_IdleHousekeeping(void) 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 @@ -432,6 +532,10 @@ static void FOXHUNT_HandleKeys(void) if (foxAudioMode == FOXHUNT_AUDIO_BEEP) audioTick = FOXHUNT_RATE_SLOW_TICKS; // blip promptly break; + case KEY_2: + // Toggle the main gauge: S-meter staircase <-> full-width history. + foxGraphMode ^= 1; + break; case KEY_SIDE1: case KEY_SIDE2: // Same shortcut that opened Fox Hunt now toggles to the beacon. @@ -442,23 +546,35 @@ static void FOXHUNT_HandleKeys(void) } } -// (Re)enter the hunt (RX) sub-mode: fixed front-end gain, attenuator re-applied, -// audio silenced, peak/trend reset. Called at start-up and when leaving beacon. +// (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; - foxAudioMode = FOXHUNT_AUDIO_OFF; - AUDIO_AudioPathOff(); + foxBeacon = false; BK4819_SetAGC(false); FOXHUNT_ApplyAtt(); + FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station curDbm = FOXHUNT_ReadDbm(); peakDbm = curDbm; trendRef = curDbm; trendDelta = 0; trendTick = 0; - audioTick = 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 @@ -515,8 +631,7 @@ static void FOXHUNT_TxDeniedNotice(VfoState_t state) static void FOXHUNT_EnterBeacon(void) { foxBeacon = true; - foxAudioMode = FOXHUNT_AUDIO_OFF; - AUDIO_AudioPathOff(); + AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up beaconPhaseTx = true; } @@ -734,6 +849,64 @@ static void FOXHUNT_BeaconTick(void) 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 0x90E5 +// (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) + +// 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[5]; + +// Pack the live settings into the 5-byte on-flash layout. +static void FOXHUNT_ConfigPack(uint8_t out[5]) +{ + out[0] = FOXHUNT_CFG_MAGIC; + out[1] = attStep; + out[2] = foxGraphMode; + out[3] = foxAudioMode; + out[4] = beaconIdle; +} + +// Restore persisted settings; erased/invalid flash leaves the defaults in place. +static void FOXHUNT_LoadConfig(void) +{ + uint8_t cfg[5]; + 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]; + } + + 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[5]; + 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 @@ -774,9 +947,13 @@ void APP_RunFoxHunt(void) sprintf(beaconMsg, "%s BEACON", call); } - // Start in the hunt (RX) sub-mode. - attStep = 0; - FOXHUNT_EnterHunt(); + // Start in the hunt (RX) sub-mode, S-meter staircase gauge by default, then + // restore any persisted settings (attenuator, gauge, audio, beacon idle). + attStep = 0; + foxGraphMode = FOXHUNT_GRAPH_BAR; + foxAudioMode = FOXHUNT_AUDIO_OFF; + FOXHUNT_LoadConfig(); // may override att / gauge / audio / idle + FOXHUNT_EnterHunt(); // applies the restored attStep and audio mode // 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 @@ -807,6 +984,8 @@ void APP_RunFoxHunt(void) trendTick = 0; } + FOXHUNT_HistSample(); // feed the smoothed signal-history sparkline + FOXHUNT_Draw(); ST7565_BlitStatusLine(); @@ -835,6 +1014,10 @@ void APP_RunFoxHunt(void) 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(); + // Mute any pending tone, drop the PA (safety), then restore the normal VFO // selection and RX config. AUDIO_AudioPathOff(); diff --git a/App/bitmaps.c b/App/bitmaps.c index 56c8e929..a6c617de 100644 --- a/App/bitmaps.c +++ b/App/bitmaps.c @@ -374,6 +374,28 @@ const uint8_t BITMAP_FoxHuntFlat[11] = 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 +}; #endif #ifndef ENABLE_CUSTOM_MENU_LAYOUT diff --git a/App/bitmaps.h b/App/bitmaps.h index c72b41d5..7fa04f59 100644 --- a/App/bitmaps.h +++ b/App/bitmaps.h @@ -51,6 +51,8 @@ extern const uint8_t BITMAP_NOAA[12]; 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]; #endif #ifndef ENABLE_CUSTOM_MENU_LAYOUT diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index d880e97c..44790800 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, 0x0090E5), // 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 * 5 Bytes 0x90E0 -> 0x90E5 + // (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