mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
853 lines
32 KiB
C
853 lines
32 KiB
C
/* 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
|
|
|
|
#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
|
|
|
|
// 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 sends the CW message ("<call> BEACON") in Morse, then stays silent for the
|
|
// (adjustable) idle gap. The carrier stays up during the message; 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 80 // one Morse time unit (~15 WPM)
|
|
#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 60 // longest idle gap (s)
|
|
#define FOXHUNT_BEACON_IDLE_STEP 5 // idle adjust step (s)
|
|
#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash
|
|
|
|
// 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.
|
|
static const uint8_t FOXHUNT_ATT_PGA[4] = {7, 5, 3, 1}; // 0, -6, -15, -27 dB
|
|
static const uint8_t FOXHUNT_ATT_DB[4] = {0, 6, 15, 27};
|
|
|
|
static KeyboardState kbd = {KEY_INVALID, KEY_INVALID, 0};
|
|
|
|
static bool foxRunning;
|
|
static uint8_t foxAudioMode;
|
|
static uint8_t attStep;
|
|
static int16_t curDbm;
|
|
static int16_t peakDbm;
|
|
static int16_t trendRef;
|
|
static int16_t trendDelta;
|
|
static uint8_t trendTick;
|
|
static uint8_t audioTick;
|
|
|
|
static char str[16];
|
|
|
|
// 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
|
|
};
|
|
|
|
// 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[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);
|
|
|
|
// 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)FOXHUNT_ATT_PGA[attStep]; // PGA = 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);
|
|
}
|
|
|
|
// Map a dBm value to a Geiger tone frequency (higher signal, higher pitch).
|
|
static uint16_t FOXHUNT_DbmToTone(int16_t dbm)
|
|
{
|
|
if (dbm <= FOXHUNT_DBM_FLOOR) return FOXHUNT_TONE_MIN;
|
|
if (dbm >= FOXHUNT_DBM_CEIL) return FOXHUNT_TONE_MAX;
|
|
return FOXHUNT_TONE_MIN +
|
|
(uint16_t)(((int32_t)(dbm - FOXHUNT_DBM_FLOOR) * (FOXHUNT_TONE_MAX - FOXHUNT_TONE_MIN)) /
|
|
(FOXHUNT_DBM_CEIL - FOXHUNT_DBM_FLOOR));
|
|
}
|
|
|
|
// Ticks between two blips: fewer ticks = faster clicking as the signal rises.
|
|
static uint8_t FOXHUNT_BlipPeriod(int16_t dbm)
|
|
{
|
|
if (dbm <= FOXHUNT_DBM_FLOOR) return FOXHUNT_RATE_SLOW_TICKS;
|
|
if (dbm >= FOXHUNT_DBM_CEIL) return FOXHUNT_RATE_FAST_TICKS;
|
|
return FOXHUNT_RATE_SLOW_TICKS -
|
|
(uint8_t)(((int32_t)(dbm - FOXHUNT_DBM_FLOOR) * (FOXHUNT_RATE_SLOW_TICKS - FOXHUNT_RATE_FAST_TICKS)) /
|
|
(FOXHUNT_DBM_CEIL - FOXHUNT_DBM_FLOOR));
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
// 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);
|
|
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.
|
|
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();
|
|
|
|
// 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]);
|
|
GUI_DisplaySmallestInverse(str, 4, 6, false, true, 4 + strlen(str) * 4);
|
|
sprintf(str, "%u.%05u", gRxVfo->pRX->Frequency / 100000, gRxVfo->pRX->Frequency % 100000);
|
|
UI_PrintStringSmallNormal(str, 126 - strlen(str) * 7, 0, 6);
|
|
}
|
|
|
|
// Map a navigation key to a logical step (+1 = up/increase, -1 = down/decrease),
|
|
// honouring the SET_NAV up/down orientation (UV-K1 vs UV-K5), like the rest of
|
|
// the firmware (see MAIN_Key_UP_DOWN / spectrum GetDirection).
|
|
static int8_t FOXHUNT_NavDir(KEY_Code_t key)
|
|
{
|
|
int8_t dir = (key == KEY_UP) ? 1 : -1;
|
|
return gEeprom.SET_NAV ? dir : -dir;
|
|
}
|
|
|
|
// Change the attenuator by one step in the given direction.
|
|
static void FOXHUNT_AttStep(int8_t dir)
|
|
{
|
|
if (dir > 0) {
|
|
if (attStep < 3) { attStep++; FOXHUNT_ApplyAtt(); }
|
|
} else {
|
|
if (attStep > 0) { attStep--; FOXHUNT_ApplyAtt(); }
|
|
}
|
|
}
|
|
|
|
// Change the beacon idle gap by one step in the given direction (shared by the
|
|
// idle-phase keys and the mid-transmission adjust).
|
|
static void FOXHUNT_IdleStep(int8_t dir)
|
|
{
|
|
if (dir > 0) {
|
|
if (beaconIdle < FOXHUNT_BEACON_IDLE_MAX) beaconIdle += FOXHUNT_BEACON_IDLE_STEP;
|
|
} else {
|
|
if (beaconIdle > FOXHUNT_BEACON_IDLE_MIN) beaconIdle -= FOXHUNT_BEACON_IDLE_STEP;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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
|
|
|
|
switch (kbd.current) {
|
|
case KEY_EXIT:
|
|
foxRunning = false;
|
|
break;
|
|
case KEY_UP:
|
|
case KEY_DOWN:
|
|
// Arrows follow SET_NAV; the 4/0 alternates below stay fixed.
|
|
FOXHUNT_AttStep(FOXHUNT_NavDir(kbd.current));
|
|
break;
|
|
case KEY_4:
|
|
FOXHUNT_AttStep(1);
|
|
break;
|
|
case KEY_0:
|
|
FOXHUNT_AttStep(-1);
|
|
break;
|
|
case KEY_MENU:
|
|
// Reset the peak hold and the trend reference.
|
|
peakDbm = curDbm;
|
|
trendRef = curDbm;
|
|
break;
|
|
case KEY_1:
|
|
// Cycle the audio: silent -> Geiger beep -> station audio -> silent.
|
|
foxAudioMode = (uint8_t)((foxAudioMode + 1) % 3);
|
|
FOXHUNT_SetAudio();
|
|
if (foxAudioMode == FOXHUNT_AUDIO_BEEP)
|
|
audioTick = FOXHUNT_RATE_SLOW_TICKS; // blip promptly
|
|
break;
|
|
case KEY_SIDE1:
|
|
case KEY_SIDE2:
|
|
// Same shortcut that opened Fox Hunt now toggles to the beacon.
|
|
FOXHUNT_EnterBeacon();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// (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.
|
|
static void FOXHUNT_EnterHunt(void)
|
|
{
|
|
foxBeacon = false;
|
|
foxAudioMode = FOXHUNT_AUDIO_OFF;
|
|
AUDIO_AudioPathOff();
|
|
|
|
BK4819_SetAGC(false);
|
|
FOXHUNT_ApplyAtt();
|
|
|
|
curDbm = FOXHUNT_ReadDbm();
|
|
peakDbm = curDbm;
|
|
trendRef = curDbm;
|
|
trendDelta = 0;
|
|
trendTick = 0;
|
|
audioTick = 0;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
UI_DisplayClear();
|
|
UI_StatusClear();
|
|
|
|
GUI_DisplaySmallestInverse("BEACON", 2, 0, true, true, 28);
|
|
FOXHUNT_DrawStatusBattery();
|
|
|
|
// 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);
|
|
|
|
// Bottom-right: the barred TX frequency, so it is clear what was refused.
|
|
sprintf(str, "%u.%05u", gTxVfo->pTX->Frequency / 100000, gTxVfo->pTX->Frequency % 100000);
|
|
UI_PrintStringSmallNormal(str, 126 - strlen(str) * 7, 0, 6);
|
|
|
|
ST7565_BlitStatusLine();
|
|
ST7565_BlitFullScreen();
|
|
|
|
// 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;
|
|
foxAudioMode = FOXHUNT_AUDIO_OFF;
|
|
AUDIO_AudioPathOff();
|
|
gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up
|
|
beaconPhaseTx = true;
|
|
}
|
|
|
|
// Sleep in short slices while watching the keypad, so a burst stays interactive:
|
|
// EXIT cuts it mid-air (returns true), and UP/DOWN retune the idle gap live.
|
|
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;
|
|
BACKLIGHT_Update(); // keep any brightness fade moving in TX
|
|
|
|
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_UP || kbd.current == KEY_DOWN) {
|
|
// The gap is only read once the burst ends, so it is safe to change
|
|
// now; refresh the "IDLE nn" label so the change is visible at once.
|
|
FOXHUNT_IdleStep(FOXHUNT_NavDir(kbd.current));
|
|
FOXHUNT_BeaconDraw(true, 0);
|
|
ST7565_BlitFullScreen();
|
|
}
|
|
}
|
|
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 code = FOXHUNT_MorseByte(c);
|
|
|
|
if (code == 0) // space / unknown -> word gap
|
|
return FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 4);
|
|
|
|
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
|
|
}
|
|
// Inter-character gap is 3 units; one already elapsed after the last element.
|
|
return FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 2);
|
|
}
|
|
|
|
// One beacon burst: transmit the CW identification message once, at the TX VFO's
|
|
// configured power. Carrier stays up; the tone is keyed per Morse element (MCW).
|
|
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();
|
|
|
|
bool stop = false;
|
|
for (uint8_t i = 0; !stop && beaconMsg[i]; i++)
|
|
stop = FOXHUNT_MorseChar(beaconMsg[i]);
|
|
|
|
// 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);
|
|
}
|
|
|
|
static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft)
|
|
{
|
|
UI_DisplayClear();
|
|
UI_StatusClear();
|
|
|
|
// Status line: BEACON tag (left) + battery (right), same style as FOX HUNT.
|
|
GUI_DisplaySmallestInverse("BEACON", 2, 0, true, true, 28);
|
|
FOXHUNT_DrawStatusBattery();
|
|
|
|
// Centre, on two lines: the state, and below it the idle countdown (idle only).
|
|
if (txNow) {
|
|
UI_PrintString("TX", 0, 127, 1, 10);
|
|
} else {
|
|
UI_PrintString("IDLE", 0, 127, 1, 10);
|
|
sprintf(str, "%02us", idleLeft);
|
|
UI_PrintString(str, 0, 127, 3, 10);
|
|
}
|
|
|
|
// Bottom-left: the idle-duration setting as an inverse label (ATT style).
|
|
sprintf(str, "IDLE %02u", beaconIdle);
|
|
GUI_DisplaySmallestInverse(str, 4, 6, false, true, 4 + strlen(str) * 4);
|
|
|
|
// Bottom-right: the working (TX) frequency.
|
|
sprintf(str, "%u.%05u", gTxVfo->pTX->Frequency / 100000, gTxVfo->pTX->Frequency % 100000);
|
|
UI_PrintStringSmallNormal(str, 126 - strlen(str) * 7, 0, 6);
|
|
}
|
|
|
|
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
|
|
|
|
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_UP:
|
|
case KEY_DOWN:
|
|
// Idle duration, arrows following the SET_NAV orientation.
|
|
FOXHUNT_IdleStep(FOXHUNT_NavDir(kbd.current));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
FOXHUNT_BeaconDraw(true, 0);
|
|
ST7565_BlitStatusLine();
|
|
ST7565_BlitFullScreen();
|
|
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
|
K5VIEWER_Update(false);
|
|
#endif
|
|
FOXHUNT_BeaconTransmit(); // may clear foxBeacon (abort)
|
|
|
|
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);
|
|
ST7565_BlitStatusLine();
|
|
ST7565_BlitFullScreen();
|
|
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
|
K5VIEWER_Update(false);
|
|
#endif
|
|
|
|
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
|
|
}
|
|
|
|
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.
|
|
beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
|
|
beaconPhaseTx = false;
|
|
|
|
// Build the CW message "<call> BEACON": the callsign is the boot message
|
|
// (line 1), sanitised to what Morse can send; empty/erased falls back to FOX.
|
|
{
|
|
char call[16];
|
|
uint8_t n = 0;
|
|
PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, sizeof(call) - 1);
|
|
call[sizeof(call) - 1] = '\0';
|
|
for (uint8_t i = 0; i < sizeof(call) - 1; 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 == '/')
|
|
call[n++] = c;
|
|
}
|
|
call[n] = '\0';
|
|
if (n == 0)
|
|
strcpy(call, "FOX");
|
|
sprintf(beaconMsg, "%s BEACON", call);
|
|
}
|
|
|
|
// Start in the hunt (RX) sub-mode.
|
|
attStep = 0;
|
|
FOXHUNT_EnterHunt();
|
|
|
|
// 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();
|
|
|
|
foxRunning = true;
|
|
while (foxRunning) {
|
|
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 (++trendTick >= FOXHUNT_TREND_TICKS) {
|
|
trendDelta = curDbm - trendRef;
|
|
trendRef = curDbm;
|
|
trendTick = 0;
|
|
}
|
|
|
|
FOXHUNT_Draw();
|
|
|
|
ST7565_BlitStatusLine();
|
|
ST7565_BlitFullScreen();
|
|
|
|
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
|
// Mirror the screen to K5Viewer (sends only changed chunks).
|
|
K5VIEWER_Update(false);
|
|
#endif
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|