Files
uv-k1-k5v3-firmware-custom/App/apps/foxhunt/foxhunt_app.c
T

357 lines
12 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.
*/
/*
* FoxHunt (RX) — overlay app. Ported from App/app/foxhunt.c, hunt sub-mode only
* (the beacon becomes a separate app). Signal-strength hunter: corrected dBm with
* a big number, an IARU S-meter staircase or a scrolling history, peak/min hold,
* a 1 s trend, a front-end attenuator ladder, and Geiger/station audio.
*
* Keys: 1 gauge (bar/history) · 2 audio (off/beep/station) · 3 + UP/DOWN attenuator
* MENU reset holds · long F keypad lock · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define BK_REG_13 0x13
/* --- tuning (from foxhunt.c) --- */
#define DBM_FLOOR (-141)
#define DBM_CEIL (-53)
#define TICK_MS 50
#define TREND_TICKS 20
#define LOCK_HOLD_MS 500
#define SILENCE_DBM (-120)
#define TONE_MIN 400
#define TONE_MAX 2400
#define RATE_SLOW 20
#define RATE_FAST 2
#define BLIP_MS 50
#define AUDIO_SETTLE 60
#define ATT_SETTLE 40
#define ATT_COUNT 6
#define ATT_BYP0 4
#define SEG_COUNT 13
#define BAR_X0 6
#define SEG_PITCH 9
#define SEG_W 8
#define SEG_BOTTOM 37
#define HIST_LEN 120
#define HIST_DECIM 3
#define GRAPH_X0 4
#define GRAPH_TOP 27
#define GRAPH_BOT 45
#define GRAPH_FLOOR 2
#define AUDIO_OFF 0
#define AUDIO_BEEP 1
#define AUDIO_STATION 2
#define GRAPH_BAR 0
#define GRAPH_HIST 1
#define CFG_MAGIC 0xF1 /* config validity marker */
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
static const uint8_t FONT_F[8] = {0x7f,0x00,0x76,0x76,0x76,0x76,0x7e,0x7f};
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static bool foxLocked, fArm, fLongDone;
static uint16_t fHoldMs;
static uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
static int16_t curDbm, peakDbm, minDbm, trendRef, trendDelta;
static uint8_t trendTick;
static uint8_t histBuf[HIST_LEN];
static uint8_t histHead, histTick;
static int16_t histEma;
static uint8_t prevKey;
static bool running;
static char str[16];
/* ---- tiny formatting ---- */
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *puti(char *o,int v){
if(v<0){*o++='-';v=-v;}
char t[6]; int8_t n=0;
do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6);
while(n--)*o++=t[n];
return o;
}
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
/* ---- radio helpers ---- */
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
static void applyAtt(void){
uint16_t reg = A->bk_read(BK_REG_13);
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
A->bk_write(BK_REG_13, reg);
}
static void setAudio(void){
if(foxAudioMode==AUDIO_OFF){ A->audio_path(false); return; }
A->audio_path(true);
A->delay_ms(AUDIO_SETTLE);
A->set_af(APP_AF_MUTE);
}
static int32_t lerp(int16_t dbm,int32_t lo,int32_t hi){
if(dbm<=DBM_FLOOR) return lo;
if(dbm>=DBM_CEIL) return hi;
return lo + ((int32_t)(dbm-DBM_FLOOR)*(hi-lo))/(DBM_CEIL-DBM_FLOOR);
}
static void blip(uint16_t freq){
A->prepare_tone();
A->play_tone_raw(freq, BLIP_MS);
A->tones_off_rx();
A->set_agc(false);
applyAtt();
}
static uint8_t fillCount(int16_t dbm){
int16_t n;
if(dbm<-141) return 0;
if(dbm<=-93) n=1+(dbm+141)/6;
else n=9+(dbm+93)/10;
if(n>SEG_COUNT) n=SEG_COUNT;
return (uint8_t)n;
}
static void buildS(char *out,int16_t dbm){
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
else if(dbm<-141) put(out,"S0")[0]='\0';
else { char *p=put(out,"S"); i2str(p,(dbm+147)/6); }
}
static void histSample(void){
histEma += ((int16_t)curDbm*8 - histEma)/4;
if(++histTick<HIST_DECIM) return;
histTick=0;
histBuf[histHead]=fillCount((int16_t)(histEma/8));
histHead=(uint8_t)((histHead+1)%HIST_LEN);
}
static void rebase(void){
curDbm=A->rssi_dbm();
peakDbm=minDbm=trendRef=curDbm;
trendDelta=0; trendTick=0;
uint8_t lvl=fillCount(curDbm);
for(uint8_t i=0;i<HIST_LEN;i++) histBuf[i]=lvl;
histHead=0; histTick=0; histEma=(int16_t)(curDbm*8);
}
static void attCycle(int8_t dir){
attStep=(uint8_t)((attStep + (dir>0?1:ATT_COUNT-1))%ATT_COUNT);
applyAtt();
A->delay_ms(ATT_SETTLE);
rebase();
}
/* ---- drawing ---- */
static void fillRect(int16_t x0,int16_t y0,int16_t x1,int16_t y1){
for(int16_t x=x0;x<=x1;x++) A->draw_line(A->fb,x,y0,x,y1,true);
}
static void drawBar(void){
uint8_t n=fillCount(curDbm);
for(uint8_t i=0;i<n;i++){
int16_t sx=BAR_X0+i*SEG_PITCH;
fillRect(sx,SEG_BOTTOM-i,sx+SEG_W-1,SEG_BOTTOM);
}
A->draw_line(A->fb,BAR_X0,SEG_BOTTOM+2,BAR_X0+(SEG_COUNT-1)*SEG_PITCH+SEG_W-1,SEG_BOTTOM+2,true);
}
static void drawHist(void){
const uint8_t floorY=GRAPH_BOT-GRAPH_FLOOR;
const uint8_t span=floorY-GRAPH_TOP;
A->draw_line(A->fb,GRAPH_X0,GRAPH_BOT,GRAPH_X0+HIST_LEN-1,GRAPH_BOT,true);
uint8_t prevY=0, idx=histHead;
for(uint8_t c=0;c<HIST_LEN;c++){
uint8_t lvl=histBuf[idx]; if(++idx>=HIST_LEN)idx=0;
if(lvl>SEG_COUNT)lvl=SEG_COUNT;
uint8_t x=GRAPH_X0+c;
uint8_t y=(uint8_t)(floorY-(lvl*span)/SEG_COUNT);
for(uint8_t yy=y+1;yy<=floorY;yy++) if(((x+yy)&1)==0) A->draw_line(A->fb,x,yy,x,yy,true);
if(c==0){ A->draw_line(A->fb,x,y,x,y,true); }
else { uint8_t lo=(y<prevY)?y:prevY, hi=(y<prevY)?prevY:y; A->draw_line(A->fb,x,lo,x,hi,true); }
prevY=y;
}
}
static void tag(const char *s,uint8_t x,uint8_t line){
A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4));
}
static void draw(void){
char big[8], sMeter[8];
A->display_clear();
A->status_clear();
A->print_inverse("FOX HUNT",2,0,true,true,34);
A->draw_battery();
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
else cpy(A->status_line+38,BMP_BARS,11);
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
if(foxLocked) cpy(A->status_line+69,FONT_LOCK,9);
else if(fArm) cpy(A->status_line+69,FONT_F,8);
i2str(big,curDbm);
A->display_freq(big,2,0,false);
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
/* trend arrow (line 0, right) + signed delta (line 1) */
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
if(trendDelta!=0){
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
A->print_normal("dBm",127-3*7,0,1);
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
}
i2str(put(str,"PK "),peakDbm);
tag(str,4,2);
i2str(put(str,"MN "),minDbm);
tag(str,(uint8_t)((LCD_WIDTH-slen(str)*4)/2),2);
buildS(sMeter,curDbm);
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
if(foxGraphMode==GRAPH_HIST) drawHist();
else drawBar();
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
tag(str,4,6);
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
uint32_t fr=f%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
}
/* ---- config (deferred) ---- */
static void loadConfig(void){
uint8_t c[4];
A->cfg_load(c,4);
if(c[0]==CFG_MAGIC){
if(c[1]<ATT_COUNT) attStep=c[1];
if(c[2]<=GRAPH_HIST) foxGraphMode=c[2];
if(c[3]<=AUDIO_STATION) foxAudioMode=c[3];
}
}
static void saveConfig(void){
uint8_t c[4]={CFG_MAGIC,attStep,foxGraphMode,foxAudioMode};
A->cfg_save(c,4);
}
/* ---- input ---- */
static void handleKeys(void){
uint8_t key=A->get_key();
/* long-press F -> keypad lock */
if(key==APP_KEY_F){
if(!fLongDone){ fHoldMs+=TICK_MS; if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); } }
} else { fHoldMs=0; fLongDone=false; }
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
prevKey=key;
A->backlight_on();
if(foxLocked){
if(key==APP_KEY_UP||key==APP_KEY_DOWN) attCycle(A->nav_dir(key));
return;
}
if(key==APP_KEY_F){ fArm=!fArm; return; }
int8_t dir=fArm?-1:1;
switch(key){
case APP_KEY_EXIT: running=false; break;
case APP_KEY_1: foxGraphMode^=1; break;
case APP_KEY_2:
foxAudioMode=(uint8_t)((foxAudioMode+(dir>0?1:2))%3); setAudio();
if(foxAudioMode==AUDIO_BEEP){ audioTick=RATE_SLOW; }
break;
case APP_KEY_3: attCycle(dir); break;
case APP_KEY_UP:
case APP_KEY_DOWN: attCycle(A->nav_dir(key)); break;
case APP_KEY_MENU: peakDbm=minDbm=trendRef=curDbm; break;
default: break;
}
fArm=false;
}
static void tickDelay(void){
for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); }
}
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api;
foxLocked=fArm=fLongDone=false; fHoldMs=0;
attStep=0; foxGraphMode=GRAPH_BAR; foxAudioMode=AUDIO_OFF;
prevKey=APP_KEY_INVALID;
loadConfig();
A->backlight_on();
A->set_agc(false);
applyAtt();
setAudio();
A->delay_ms(ATT_SETTLE);
rebase();
running=true;
while(running){
handleKeys();
if(!running) break;
curDbm=A->rssi_dbm();
if(curDbm>peakDbm) peakDbm=curDbm;
if(curDbm<minDbm) minDbm=curDbm;
if(++trendTick>=TREND_TICKS){ trendDelta=curDbm-trendRef; trendRef=curDbm; trendTick=0; }
histSample();
draw();
A->blit_status();
A->blit_full();
if(foxAudioMode==AUDIO_BEEP && curDbm>=SILENCE_DBM){
if(++audioTick>=(uint8_t)lerp(curDbm,RATE_SLOW,RATE_FAST)){ audioTick=0; blip((uint16_t)lerp(curDbm,TONE_MIN,TONE_MAX)); }
} else if(foxAudioMode==AUDIO_STATION){
A->set_af(curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
}
A->battery_sample();
tickDelay();
}
saveConfig();
A->audio_path(false);
}