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

371 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"
#include "foxhunt_assets.h" /* generated by gen_assets.py */
#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 */
/* Texts, the attenuator ladder and the icons are read-only assets
* (gen_assets.py): icons are copied straight into the LCD buffers. */
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){
uint32_t u;
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
char t[6]; int8_t n=0;
do{t[n++]=(char)('0'+u%10u);u/=10u;}while(u&&n<6);
while(n--)*o++=t[n];
return o;
}
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
/* Text from the assets in a shared buffer, valid until the next T() call. */
static char tbuf[TEXT_MAX];
static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; }
/* ---- 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), gain;
A->asset_read(ATT_REG13 + attStep * 2u, &gain, sizeof(gain));
reg = (uint16_t)((reg & ~0x03FFu) | gain);
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;
const int32_t delta = hi - lo;
const uint32_t distance = ((uint32_t)(dbm-DBM_FLOOR) *
(uint32_t)(delta < 0 ? -delta : delta)) /
(uint32_t)(DBM_CEIL-DBM_FLOOR);
return lo + (delta < 0 ? -(int32_t)distance : (int32_t)distance);
}
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=(int16_t)(1u+(uint16_t)(dbm+141)/6u);
else n=(int16_t)(9u+(uint16_t)(dbm+93)/10u);
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,T(T_S9P)); if(o<10)*p++='0'; i2str(p,o); }
else if(dbm<-141) put(out,T(T_S0))[0]='\0';
else { char *p=put(out,T(T_S)); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
}
static int16_t div_trunc_pow2(int32_t v, uint8_t shift)
{
if (v < 0)
return (int16_t)-((uint32_t)(-v) >> shift);
return (int16_t)((uint32_t)v >> shift);
}
static uint8_t cycleIndex(uint8_t value, uint8_t count, int8_t dir)
{
if (dir > 0)
return ++value < count ? value : 0u;
return value > 0u ? (uint8_t)(value - 1u) : (uint8_t)(count - 1u);
}
static void histSample(void){
histEma += div_trunc_pow2((int32_t)curDbm * 8 - histEma, 2u);
if(++histTick<HIST_DECIM) return;
histTick=0;
histBuf[histHead]=fillCount(div_trunc_pow2(histEma, 3u));
if(++histHead>=HIST_LEN) histHead=0;
}
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=cycleIndex(attStep,ATT_COUNT,dir);
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-((uint32_t)lvl*span)/(uint32_t)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(T(T_TITLE),2,0,true,true,34);
A->draw_battery();
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
if(foxGraphMode==GRAPH_HIST) A->asset_read(BMP_GRAPH,A->status_line+38,BMP_GRAPH_LEN);
else A->asset_read(BMP_BARS,A->status_line+38,BMP_BARS_LEN);
if(foxAudioMode!=AUDIO_OFF)
A->asset_read(foxAudioMode==AUDIO_BEEP?BMP_SIGNAL:BMP_SPEAKER,A->status_line+55,BMP_SIGNAL_LEN);
if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
i2str(big,curDbm);
A->display_freq(big,2,0,false);
A->print_normal(T(T_DBM),(uint8_t)(slen(big)*13+4),0,1);
/* trend arrow (line 0, right) + signed delta (line 1) */
A->asset_read(trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, A->fb[0]+115, BMP_UP_LEN);
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(T(T_DBM),127-3*7,0,1);
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
}
i2str(put(str,T(T_PK)),peakDbm);
tag(str,4,2);
i2str(put(str,T(T_MN)),minDbm);
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),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){
uint8_t db; A->asset_read(ATT_DB+attStep,&db,1);
char *o=put(str,T(T_ATT)); o=puti(o,db); put(o,T(T_DB))[0]='\0';
}
else put(str,T(attStep==ATT_BYP0?T_BYP:T_BYP_PLUS))[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=cycleIndex(foxAudioMode,3u,dir); 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);
}