mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 11:08:20 +00:00
401 lines
14 KiB
C
401 lines
14 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.
|
|
*
|
|
* No division is linked: numbers are printed by subtracting powers of ten and
|
|
* the few divisions by constants are exact multiply-and-shift equivalents,
|
|
* checked exhaustively over their input ranges.
|
|
*
|
|
* 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 */
|
|
|
|
/* The multiply-and-shift divisions below rely on these constants. */
|
|
_Static_assert(DBM_CEIL - DBM_FLOOR == 88, "lerp() divides by 88");
|
|
_Static_assert(SEG_COUNT == 13 && (GRAPH_BOT - GRAPH_FLOOR - GRAPH_TOP) * SEG_COUNT <= 208,
|
|
"drawHist() divides by 13 up to 208");
|
|
|
|
/* Texts, the attenuator ladder, the powers of ten and the icons are read-only
|
|
* assets (gen_assets.py): icons are copied straight into the LCD buffers. */
|
|
|
|
/* All the state in one struct: Thumb-1 code then reaches every field from a
|
|
* single base address (one literal per function instead of one per global),
|
|
* byte fields first so their offsets fit ldrb's 0..31 immediate. The overlay
|
|
* is zeroed by the loader at each launch. */
|
|
struct globals {
|
|
bool foxLocked, fArm, fLongDone, running;
|
|
uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
|
|
uint8_t trendTick, histHead, histTick, prevKey;
|
|
uint16_t fHoldMs;
|
|
/* words: loaded without a sign extension */
|
|
int32_t curDbm, peakDbm, minDbm, trendRef, trendDelta, histEma;
|
|
const app_api_t *api;
|
|
uint8_t *histBuf; /* HIST_LEN levels, on app_main's stack */
|
|
};
|
|
static struct globals g;
|
|
#define A (g.api)
|
|
|
|
_Static_assert(offsetof(struct globals, prevKey) < 32u, "byte fields out of ldrb range");
|
|
|
|
/* ---- 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; }
|
|
/* Decimal digits of u, no leading zeros, by subtracting the powers of ten. */
|
|
static char *putu(char *o,uint32_t u){
|
|
uint32_t place[PLACE_LEN/4u];
|
|
A->asset_read(PLACE,place,sizeof(place));
|
|
bool lead=true;
|
|
for(uint8_t i=0;i<PLACE_LEN/4u;i++){
|
|
uint8_t d=0;
|
|
while(u>=place[i]){ u-=place[i]; d++; }
|
|
if(d||i==PLACE_LEN/4u-1u) lead=false;
|
|
if(!lead) *o++=(char)('0'+d);
|
|
}
|
|
return o;
|
|
}
|
|
static char *puti(char *o,int v){
|
|
if(v<0){ *o++='-'; v=-v; }
|
|
return putu(o,(uint32_t)v);
|
|
}
|
|
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
|
|
|
|
/* Text from the assets in the caller's buffer (TEXT_MAX bytes). */
|
|
static char *T(char *buf,uint16_t off){ A->asset_read(off,buf,TEXT_MAX); return buf; }
|
|
|
|
/* ---- radio helpers ---- */
|
|
static int32_t iabs32(int32_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 + g.attStep * 2u, &gain, sizeof(gain));
|
|
reg = (uint16_t)((reg & ~0x03FFu) | gain);
|
|
A->bk_write(BK_REG_13, reg);
|
|
}
|
|
|
|
static void setAudio(void){
|
|
if(g.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(int32_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 x = (uint32_t)(dbm-DBM_FLOOR) *
|
|
(uint32_t)(delta < 0 ? -delta : delta);
|
|
/* x / 88 as ((x / 8) * 23832) >> 18: exact for x <= 174000, i.e. every
|
|
|delta| <= 2000 (the tone and rate ranges) */
|
|
const uint32_t distance = ((x >> 3) * 23832u) >> 18;
|
|
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(int32_t dbm){
|
|
int32_t n;
|
|
if(dbm<-141) return 0;
|
|
if(dbm<=-93) n=(int32_t)(1u+(((uint32_t)(dbm+141)*43u)>>8)); /* (dbm+141)/6: exact to 127 */
|
|
else n=(int32_t)(9u+(((uint32_t)(dbm+93)*205u)>>11)); /* (dbm+93)/10: exact to 1028 */
|
|
if(n>SEG_COUNT) n=SEG_COUNT;
|
|
return (uint8_t)n;
|
|
}
|
|
static void buildS(char *out,int32_t dbm){
|
|
char t[TEXT_MAX];
|
|
if(dbm>=-93){ int32_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,T(t,T_S9P)); if(o<10)*p++='0'; i2str(p,(int)o); }
|
|
else if(dbm<-141) put(out,T(t,T_S0))[0]='\0';
|
|
else { char *p=put(out,T(t,T_S)); i2str(p,(int)(((uint32_t)(dbm+147)*43u)>>8)); } /* (dbm+147)/6 */
|
|
}
|
|
|
|
static int32_t div_trunc_pow2(int32_t v, uint8_t shift)
|
|
{
|
|
if (v < 0)
|
|
return -(int32_t)((uint32_t)(-v) >> shift);
|
|
return (int32_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){
|
|
g.histEma += div_trunc_pow2(g.curDbm * 8 - g.histEma, 2u);
|
|
if(++g.histTick<HIST_DECIM) return;
|
|
g.histTick=0;
|
|
g.histBuf[g.histHead]=fillCount(div_trunc_pow2(g.histEma, 3u));
|
|
if(++g.histHead>=HIST_LEN) g.histHead=0;
|
|
}
|
|
static void rebase(void){
|
|
g.curDbm=A->rssi_dbm();
|
|
g.peakDbm=g.minDbm=g.trendRef=g.curDbm;
|
|
g.trendDelta=0; g.trendTick=0;
|
|
uint8_t lvl=fillCount(g.curDbm);
|
|
for(uint8_t i=0;i<HIST_LEN;i++) g.histBuf[i]=lvl;
|
|
g.histHead=0; g.histTick=0; g.histEma=g.curDbm*8;
|
|
}
|
|
static void attCycle(int8_t dir){
|
|
g.attStep=cycleIndex(g.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(g.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=g.histHead;
|
|
for(uint8_t c=0;c<HIST_LEN;c++){
|
|
uint8_t lvl=g.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*79u)>>10)); /* lvl*span/13: exact to 208 */
|
|
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], str[16], t[TEXT_MAX];
|
|
|
|
A->display_clear();
|
|
A->status_clear();
|
|
A->print_inverse(T(t,T_TITLE),2,0,true,true,34);
|
|
A->draw_battery();
|
|
|
|
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
|
|
if(g.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(g.foxAudioMode!=AUDIO_OFF)
|
|
A->asset_read(g.foxAudioMode==AUDIO_BEEP?BMP_SIGNAL:BMP_SPEAKER,A->status_line+55,BMP_SIGNAL_LEN);
|
|
if(g.foxLocked||g.fArm) A->asset_read(g.foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
|
|
|
|
i2str(big,(int)g.curDbm);
|
|
A->display_freq(big,2,0,false);
|
|
A->print_normal(T(t,T_DBM),(uint8_t)(slen(big)*13+4),0,1);
|
|
|
|
/* trend arrow (line 0, right) + signed delta (line 1) */
|
|
A->asset_read(g.trendDelta>0?BMP_UP:g.trendDelta<0?BMP_DOWN:BMP_FLAT, A->fb[0]+115, BMP_UP_LEN);
|
|
if(g.trendDelta!=0){
|
|
char *p=str; if(g.trendDelta>0)*p++='+'; else{*p++='-';}
|
|
int32_t a=iabs32(g.trendDelta); if(a<10){*p++='0';} i2str(p,(int)a);
|
|
A->print_normal(T(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,T_PK)),(int)g.peakDbm);
|
|
tag(str,4,2);
|
|
i2str(put(str,T(t,T_MN)),(int)g.minDbm);
|
|
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)>>1),2);
|
|
buildS(sMeter,g.curDbm);
|
|
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
|
|
|
|
if(g.foxGraphMode==GRAPH_HIST) drawHist();
|
|
else drawBar();
|
|
|
|
if(g.attStep<ATT_BYP0){
|
|
uint8_t db; A->asset_read(ATT_DB+g.attStep,&db,1);
|
|
char *o=put(str,T(t,T_ATT)); o=puti(o,db); put(o,T(t,T_DB))[0]='\0';
|
|
}
|
|
else put(str,T(t,g.attStep==ATT_BYP0?T_BYP:T_BYP_PLUS))[0]='\0';
|
|
tag(str,4,6);
|
|
|
|
/* the RX frequency (10 Hz units) as MHz with 5 decimals: every digit, then
|
|
the point before the last five (f >= 18 MHz: at least 7 digits) */
|
|
{ char *e=putu(str,A->rx_freq());
|
|
e[1]='\0';
|
|
for(uint8_t k=0;k<5u;k++,e--) e[0]=e[-1];
|
|
e[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) g.attStep=c[1];
|
|
if(c[2]<=GRAPH_HIST) g.foxGraphMode=c[2];
|
|
if(c[3]<=AUDIO_STATION) g.foxAudioMode=c[3];
|
|
}
|
|
}
|
|
static void saveConfig(void){
|
|
uint8_t c[4]={CFG_MAGIC,g.attStep,g.foxGraphMode,g.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(!g.fLongDone){ g.fHoldMs+=TICK_MS; if(g.fHoldMs>=LOCK_HOLD_MS){ g.fLongDone=true; g.foxLocked=!g.foxLocked; g.fArm=false; A->backlight_on(); } }
|
|
} else { g.fHoldMs=0; g.fLongDone=false; }
|
|
|
|
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
|
|
g.prevKey=key;
|
|
A->backlight_on();
|
|
|
|
if(g.foxLocked){
|
|
if(key==APP_KEY_UP||key==APP_KEY_DOWN) attCycle(A->nav_dir(key));
|
|
return;
|
|
}
|
|
if(key==APP_KEY_F){ g.fArm=!g.fArm; return; }
|
|
int8_t dir=g.fArm?-1:1;
|
|
|
|
switch(key){
|
|
case APP_KEY_EXIT: g.running=false; break;
|
|
case APP_KEY_1: g.foxGraphMode^=1; break;
|
|
case APP_KEY_2:
|
|
g.foxAudioMode=cycleIndex(g.foxAudioMode,3u,dir); setAudio();
|
|
if(g.foxAudioMode==AUDIO_BEEP){ g.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: g.peakDbm=g.minDbm=g.trendRef=g.curDbm; break;
|
|
default: break;
|
|
}
|
|
g.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){
|
|
uint8_t hist[HIST_LEN]; /* the history, on the stack: not in the overlay */
|
|
A=api;
|
|
g.histBuf=hist;
|
|
/* locks, holds, attenuator, bar gauge and audio off start zeroed (.bss) */
|
|
g.prevKey=APP_KEY_INVALID;
|
|
|
|
loadConfig();
|
|
A->backlight_on();
|
|
A->set_agc(false);
|
|
applyAtt();
|
|
setAudio();
|
|
A->delay_ms(ATT_SETTLE);
|
|
rebase();
|
|
|
|
g.running=true;
|
|
while(g.running){
|
|
handleKeys();
|
|
if(!g.running) break;
|
|
|
|
g.curDbm=A->rssi_dbm();
|
|
if(g.curDbm>g.peakDbm) g.peakDbm=g.curDbm;
|
|
if(g.curDbm<g.minDbm) g.minDbm=g.curDbm;
|
|
if(++g.trendTick>=TREND_TICKS){ g.trendDelta=g.curDbm-g.trendRef; g.trendRef=g.curDbm; g.trendTick=0; }
|
|
histSample();
|
|
|
|
draw();
|
|
A->blit_status();
|
|
A->blit_full();
|
|
|
|
if(g.foxAudioMode==AUDIO_BEEP && g.curDbm>=SILENCE_DBM){
|
|
if(++g.audioTick>=(uint8_t)lerp(g.curDbm,RATE_SLOW,RATE_FAST)){ g.audioTick=0; blip((uint16_t)lerp(g.curDbm,TONE_MIN,TONE_MAX)); }
|
|
} else if(g.foxAudioMode==AUDIO_STATION){
|
|
A->set_af(g.curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
|
|
}
|
|
|
|
A->battery_sample();
|
|
tickDelay();
|
|
}
|
|
|
|
saveConfig();
|
|
A->audio_path(false);
|
|
}
|