mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
335 lines
13 KiB
C
335 lines
13 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.
|
|
*/
|
|
|
|
/*
|
|
* Beacon (fox) — overlay app. Ported from App/app/foxhunt.c, beacon sub-mode only.
|
|
* Turns the radio into a hidden ARDF transmitter: keys up on the TX VFO and repeats
|
|
* a CW fox identifier (MOE..MO5 / MO / "<call> MOE") for the TX window, then stays
|
|
* silent for the idle gap. Two keying modes (key 4): TONE (default) keeps the carrier
|
|
* up and keys only the tone (MCW / F2A); CARR keys the PA together with the tone, so
|
|
* between elements the carrier itself is gone (carrier interruption, ARDF field pattern).
|
|
*
|
|
* Keys: 1 TX window · 2 idle gap · 3 fox id · 4 keying mode TONE/CARR (F reverses)
|
|
* MENU restart idle · long F keypad lock · EXIT quit.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include "../app_api.h"
|
|
#include "beacon_assets.h" /* generated by gen_assets.py */
|
|
|
|
#define LCD_WIDTH 128
|
|
#define TICK_MS 50
|
|
#define LOCK_HOLD_MS 500
|
|
#define MORSE_UNIT 100
|
|
#define TONE_HZ 1000
|
|
#define CALL_MAX 12
|
|
|
|
#define IDLE_DEF 30
|
|
#define IDLE_MIN 5
|
|
#define IDLE_MAX 240
|
|
#define IDLE_STEP 5
|
|
#define TX_DEF 30
|
|
#define TX_MIN 5
|
|
#define TX_MAX 60
|
|
#define TX_STEP 5
|
|
|
|
#define FOX_MO 5
|
|
#define FOX_CALL 6
|
|
#define FOX_COUNT 7
|
|
#define MSG_CALL 1
|
|
#define MSG_ID 3
|
|
#define MSG_ONE 2
|
|
|
|
#define CFG_MAGIC 0xF4
|
|
|
|
/* Texts, fox identifiers, the Morse table and icons are read-only assets
|
|
* (gen_assets.py); only the displayed item is fetched from external flash. */
|
|
|
|
static const app_api_t *A;
|
|
|
|
static bool foxLocked, fArm, fLongDone, running, phaseTx, carrier;
|
|
static uint16_t fHoldMs;
|
|
static uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
|
|
static uint16_t txMsLeft;
|
|
static char foxCall[CALL_MAX+1];
|
|
static char msg[17];
|
|
static uint8_t prevKey;
|
|
static char str[16];
|
|
|
|
/* 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; }
|
|
|
|
/* ---- 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 *putu(char *o,uint32_t 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 put2(char **o,uint8_t v){ if(v<10)*(*o)++='0'; *o=putu(*o,v); }
|
|
static void cpy(uint8_t *d,const uint8_t *s,uint8_t n){ while(n--)*d++=*s++; }
|
|
static const char *findsp(const char *s){ while(*s){ if(*s==' ')return s; s++; } return NULL; }
|
|
static uint8_t mmin(uint8_t a,uint8_t b){ return a<b?a:b; }
|
|
|
|
/* ---- settings cycles ---- */
|
|
static uint8_t rangeStep(uint8_t v,uint8_t lo,uint8_t hi,uint8_t step,int8_t dir){
|
|
if(dir>0) return (v>=hi)?lo:(uint8_t)(v+step);
|
|
return (v<=lo)?hi:(uint8_t)(v-step);
|
|
}
|
|
static bool settingKey(uint8_t key,int8_t dir){
|
|
switch(key){
|
|
case APP_KEY_1: beaconTx =rangeStep(beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
|
|
case APP_KEY_2: beaconIdle=rangeStep(beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
|
|
case APP_KEY_3:
|
|
if(dir>0){ if(++foxFox>=FOX_COUNT) foxFox=0; }
|
|
else foxFox=foxFox?(uint8_t)(foxFox-1u):(uint8_t)(FOX_COUNT-1u);
|
|
return true;
|
|
case APP_KEY_4: carrier=!carrier; return true; /* TONE <-> CARR (dir moot) */
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
/* ---- message ---- */
|
|
static void buildMsg(void){
|
|
char *o=msg;
|
|
uint8_t id=foxFox;
|
|
if(foxFox==FOX_CALL){ /* "<call> MOE", or "MOE" alone */
|
|
id=0;
|
|
if(foxCall[0]){ o=put(o,foxCall); *o++=' '; }
|
|
}
|
|
o=put(o,T(FOX_ID+id*FOX_ID_STRIDE));
|
|
*o='\0';
|
|
}
|
|
static void foxLabel(char *out){
|
|
char *o=put(out,T(T_FOX));
|
|
o=put(o,T(foxFox==FOX_CALL?T_CALL:FOX_ID+foxFox*FOX_ID_STRIDE));
|
|
*o='\0';
|
|
}
|
|
static uint8_t morseByte(char c){
|
|
uint8_t i, code;
|
|
if(c>='a'&&c<='z') c-=32;
|
|
if(c>='A'&&c<='Z') i=(uint8_t)(c-'A');
|
|
else if(c>='0'&&c<='9') i=(uint8_t)(26+c-'0');
|
|
else if(c=='/') i=36;
|
|
else return 0;
|
|
A->asset_read(MORSE+i,&code,1);
|
|
return code;
|
|
}
|
|
|
|
/* ---- drawing ---- */
|
|
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 chrome(void){
|
|
A->display_clear();
|
|
A->status_clear();
|
|
A->print_inverse(T(T_BEACON),2,0,true,true,26);
|
|
A->draw_battery();
|
|
if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
|
|
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%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);
|
|
}
|
|
static void drawTxSeconds(void){
|
|
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
|
|
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
|
|
A->print_normal(T(T_TX),1,0,0);
|
|
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
|
|
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
|
|
secShown=sec;
|
|
}
|
|
static void drawMessage(uint8_t vis){
|
|
const char *sp=findsp(msg);
|
|
if(!sp){
|
|
uint8_t idLen=slen(msg), v=mmin(vis,idLen);
|
|
char id[17]; cpy((uint8_t*)id,(const uint8_t*)msg,v); id[v]='\0';
|
|
A->print_string(id,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ONE,8);
|
|
return;
|
|
}
|
|
uint8_t callLen=(uint8_t)(sp-msg), vc=mmin(vis,callLen);
|
|
char call[CALL_MAX+1]; cpy((uint8_t*)call,(const uint8_t*)msg,vc); call[vc]='\0';
|
|
A->print_string(call,(uint8_t)((LCD_WIDTH-callLen*8u)/2u),0,MSG_CALL,8);
|
|
if(vis>callLen+1){
|
|
const char *id=sp+1; uint8_t idLen=slen(id), vi=mmin((uint8_t)(vis-callLen-1),idLen);
|
|
char idb[8]; cpy((uint8_t*)idb,(const uint8_t*)id,vi); idb[vi]='\0';
|
|
A->print_string(idb,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ID,8);
|
|
}
|
|
}
|
|
static void updateProgress(uint8_t vis){
|
|
const char *sp=findsp(msg); uint8_t top;
|
|
charsSent=vis;
|
|
if(!sp) top=MSG_ONE;
|
|
else { uint8_t callLen=(uint8_t)(sp-msg); if(vis==callLen+1) return; top=(vis<=callLen)?MSG_CALL:MSG_ID; }
|
|
for(uint8_t x=0;x<LCD_WIDTH;x++){ A->fb[top][x]=0; A->fb[top+1][x]=0; }
|
|
drawMessage(charsSent);
|
|
A->blit_line(top); A->blit_line(top+1);
|
|
}
|
|
static void beaconDraw(bool txNow,uint8_t il){
|
|
chrome();
|
|
if(txNow){
|
|
A->asset_read(BMP_TX,A->status_line+48,BMP_TX_LEN);
|
|
drawTxSeconds();
|
|
drawMessage(charsSent);
|
|
} else {
|
|
A->print_string(T(T_IDLE),0,127,1,10);
|
|
char *o=str; put2(&o,il); *o++='s'; *o='\0';
|
|
A->print_string(str,0,127,3,10);
|
|
}
|
|
char *o=put(str,T(T_TX)); *o++=' '; o=putu(o,beaconTx); *o++='s'; *o='\0'; tag(str,4,5);
|
|
o=put(str,T(T_IDLE)); *o++=' '; o=putu(o,beaconIdle); *o++='s'; *o='\0'; tag(str,4,6);
|
|
foxLabel(str); tag(str,66,5);
|
|
{ const char *m=T(carrier?T_CARR:T_TONE); tag(m,(uint8_t)(125-slen(m)*4),5); }
|
|
}
|
|
static void blit(void){ A->blit_status(); A->blit_full(); }
|
|
|
|
/* ---- keypad-lock long-press ---- */
|
|
/* true exactly on the lock/unlock toggle, so a caller that does not redraw every tick
|
|
* (the TX loop) can refresh the padlock at once. */
|
|
static bool lockTrack(uint8_t key,uint16_t ms){
|
|
if(key!=APP_KEY_F){ fHoldMs=0; fLongDone=false; return false; }
|
|
if(fLongDone) return false;
|
|
fHoldMs+=ms;
|
|
if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); return true; }
|
|
return false;
|
|
}
|
|
|
|
/* ---- interactive TX delay: drains the window, keys, live seconds. true = abort ---- */
|
|
static bool txDelay(uint16_t ms){
|
|
while(ms){
|
|
uint16_t slice=(ms>10)?10:ms;
|
|
A->delay_ms(slice); ms-=slice;
|
|
txMsLeft=(txMsLeft>slice)?(uint16_t)(txMsLeft-slice):0;
|
|
A->backlight_update();
|
|
if((uint8_t)((txMsLeft+999u)/1000u)!=secShown){ drawTxSeconds(); A->blit_line(0); }
|
|
|
|
uint8_t key=A->get_key();
|
|
if(lockTrack(key,slice)){ beaconDraw(true,0); blit(); } /* show the padlock at once, mid-TX */
|
|
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; continue; }
|
|
prevKey=key;
|
|
A->backlight_on();
|
|
if(foxLocked) continue;
|
|
if(key==APP_KEY_EXIT){ running=false; return true; }
|
|
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
|
|
if(key==APP_KEY_F){ fArm=!fArm; beaconDraw(true,0); blit(); continue; }
|
|
if(settingKey(key,fArm?-1:1)){ fArm=false; beaconDraw(true,0); blit(); }
|
|
}
|
|
return false;
|
|
}
|
|
/* Keying: both modes key the tone; CARR gates the PA in lockstep so the carrier is truly
|
|
* gone between elements (tone muted too, else it bleeds through residual PA leakage). */
|
|
static void keyOn(void){ A->tx_mute(false); A->tx_carrier(true); } /* carrier up both modes; re-arms after a mid-window switch */
|
|
static void keyOff(void){ A->tx_mute(true); if(carrier) A->tx_carrier(false); }
|
|
static bool morseChar(char c,uint8_t vis){
|
|
uint8_t code=morseByte(c);
|
|
if(code==0){ if(txDelay(MORSE_UNIT*4)) return true; updateProgress(vis); return false; }
|
|
uint8_t bit=0x80; while(!(code&bit))bit>>=1;
|
|
for(bit>>=1;bit;bit>>=1){
|
|
uint16_t on=(code&bit)?(MORSE_UNIT*3):MORSE_UNIT;
|
|
keyOn();
|
|
if(txDelay(on)){ keyOff(); return true; }
|
|
keyOff();
|
|
if(txDelay(MORSE_UNIT)) return true;
|
|
}
|
|
updateProgress(vis);
|
|
return txDelay(MORSE_UNIT*2);
|
|
}
|
|
static void transmit(void){
|
|
A->tx_set_params();
|
|
A->tx_tone(TONE_HZ);
|
|
A->tx_mute(true); /* open muted (both modes) */
|
|
if(carrier) A->tx_carrier(false); /* CARR: carrier off until first element */
|
|
txMsLeft=(uint16_t)beaconTx*1000u;
|
|
bool stop=false;
|
|
while(!stop && txMsLeft>0){
|
|
buildMsg();
|
|
charsSent=0;
|
|
beaconDraw(true,0); blit();
|
|
for(uint8_t i=0;!stop && msg[i];i++) stop=morseChar(msg[i],(uint8_t)(i+1));
|
|
if(!stop && txMsLeft>0) stop=txDelay(MORSE_UNIT*7);
|
|
}
|
|
A->tx_mute(true);
|
|
A->tx_end(); /* PA off + restore RX */
|
|
}
|
|
|
|
/* ---- idle-phase keys ---- */
|
|
static void idleKeys(void){
|
|
uint8_t key=A->get_key();
|
|
lockTrack(key,TICK_MS);
|
|
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
|
|
prevKey=key;
|
|
A->backlight_on();
|
|
if(foxLocked) return;
|
|
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
|
switch(key){
|
|
case APP_KEY_EXIT: running=false; break;
|
|
case APP_KEY_MENU: idleLeft=beaconIdle; idleTick=0; break;
|
|
default: settingKey(key,fArm?-1:1); break;
|
|
}
|
|
fArm=false;
|
|
}
|
|
|
|
static void tickDelay(void){ for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); } }
|
|
|
|
/* ---- config (deferred) ---- */
|
|
static void loadConfig(void){
|
|
uint8_t c[5]; A->cfg_load(c,5);
|
|
if(c[0]==CFG_MAGIC){
|
|
if(c[1]>=IDLE_MIN&&c[1]<=IDLE_MAX&&(c[1]%IDLE_STEP)==0) beaconIdle=c[1];
|
|
if(c[2]<FOX_COUNT) foxFox=c[2];
|
|
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&(c[3]%TX_STEP)==0) beaconTx=c[3];
|
|
if(c[4]<=1) carrier=c[4]; /* erased (0xFF) legacy config -> keep default */
|
|
}
|
|
}
|
|
static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx,(uint8_t)carrier}; A->cfg_save(c,5); }
|
|
|
|
static void txDenied(void){
|
|
chrome();
|
|
A->print_string(T(T_TX_OFF),0,127,1,8);
|
|
blit();
|
|
for(uint8_t i=0;i<20;i++) tickDelay();
|
|
}
|
|
|
|
__attribute__((section(".text.entry"),used))
|
|
void app_main(const app_api_t *api){
|
|
A=api;
|
|
foxLocked=fArm=fLongDone=carrier=false; fHoldMs=0;
|
|
beaconIdle=IDLE_DEF; beaconTx=TX_DEF; foxFox=FOX_CALL;
|
|
prevKey=APP_KEY_INVALID;
|
|
A->boot_callsign(foxCall,sizeof(foxCall));
|
|
loadConfig();
|
|
A->backlight_on();
|
|
|
|
phaseTx=true; idleLeft=beaconIdle; idleTick=0;
|
|
running=true;
|
|
while(running){
|
|
if(phaseTx){
|
|
if(A->tx_state()!=0){ txDenied(); phaseTx=false; idleLeft=beaconIdle; idleTick=0; continue; }
|
|
transmit();
|
|
phaseTx=false; idleLeft=beaconIdle; idleTick=0;
|
|
continue;
|
|
}
|
|
idleKeys();
|
|
if(!running) break;
|
|
beaconDraw(false,idleLeft); blit();
|
|
if(++idleTick>=(1000/TICK_MS)){ idleTick=0; if(idleLeft>0)idleLeft--; if(idleLeft==0)phaseTx=true; }
|
|
A->battery_sample();
|
|
tickDelay();
|
|
}
|
|
|
|
saveConfig();
|
|
A->tx_end(); /* safety: PA off + RX restored */
|
|
A->audio_path(false);
|
|
}
|