Files
uv-k1-k5v3-firmware-custom/App/apps/fm/fm_app.c
T

360 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.
*/
/*
* Broadcast FM — overlay app. Sovereign: drives the BK1080 directly and owns the
* radio while running (NO BK4819 dual-watch, unlike the resident FM). Reimplements
* the full resident FM (fm.c / UI_DisplayFM) - VFO + 48 memories - sharing the
* resident channel table (gFM_Channels) and config (gEeprom.FM_*), committed to
* EEPROM on exit. The BK1080 audio is a hardware path, so playback needs no CPU loop.
*
* Keys (iso legacy): 0-9 = frequency (VFO) / channel (MR) entry · UP/DOWN = tune /
* channel step · STAR = manual scan · F+STAR = auto-scan (stores 48) · MENU = save
* (VFO) / delete (MR) · F+1 = band · F+3 = VFO<->MR · F+0 / EXIT = quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define STEP 1
#define SCAN_SETTLE 100
#define CHMAX APP_FM_CH_MAX
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
static const uint8_t FONT_F[8] = {0x7f,0x00,0x76,0x76,0x76,0x76,0x7e,0x7f};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static uint16_t *ch; /* shared gFM_Channels[48] */
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
static uint16_t lo, hi;
static int8_t scanState; /* 0 = off, +1 up, -1 down (non-blocking) */
static bool autoScan;
static uint8_t chPos;
static uint16_t scanTimer; /* ms until the next scan step */
static bool foundFreq;
static bool askSave, askDelete;
static uint8_t savePos;
static bool fArm;
static uint8_t inBox[4], inIdx;
static char str[16];
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 char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
static void dirty(void){ /* nothing yet; committed on exit */ }
static bool validCh(uint8_t c){ return c<CHMAX && ch[c]>=lo && ch[c]<hi; }
static uint8_t findNext(uint8_t c, int8_t dir)
{
for(uint8_t i=0;i<CHMAX;i++){
if(c==0xFF) c=CHMAX-1;
else if(c>=CHMAX) c=0;
if(validCh(c)) return c;
c=(uint8_t)(c+dir);
}
return 0xFF;
}
static int configChannel(void)
{
st.freq_playing=st.sel_freq;
if(st.is_mr){
uint8_t c=findNext(st.sel_ch,+1);
if(c==0xFF){ st.is_mr=false; return -1; }
st.sel_ch=c; st.freq_playing=ch[c];
}
return 0;
}
/* ---- drawing (mirror UI_DisplayFM) ---- */
static void freqBig(uint16_t f)
{
uint16_t mhz=f/10u; char *o=str;
if(mhz<100u)*o++=' ';
o=putu(o,mhz); *o++='.'; *o++=(char)('0'+f%10u); *o='\0';
A->display_freq(str,36,1,true);
}
/* append a zero-padded 2-digit number (1..99), return the end pointer */
static char *num2(char *o, uint8_t n){ if(n<10)*o++='0'; return putu(o,n); }
static void draw(void)
{
char b[12], *o;
A->display_clear();
A->status_clear();
A->draw_battery();
if(fArm) cpy(A->status_line+69,FONT_F,8); /* F armed indicator */
A->print_string("FM",2,0,0,8);
A->print_normal(BAND_NAME[st.band],1,0,6);
/* ---- status (line 3) ---- */
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
else if(scanState) {
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
else o=put(b,"M-SCAN");
*o='\0';
}
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
else {
o=put(b,"VFO"); *o='\0';
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
}
A->print_string(b,0,127,3,10);
/* ---- line 1: input in progress / channel prompt / frequency ---- */
if(inIdx>0){
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
o=put(b,"CH-");
*o++=(char)('0'+inBox[0]);
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
*o='\0';
A->print_string(b,0,127,1,10);
} else { /* VFO frequency: "ddd.d" */
b[0]=(char)('0'+inBox[0]);
b[1]=(inIdx>1)?(char)('0'+inBox[1]):'-';
b[2]=(inIdx>2)?(char)('0'+inBox[2]):'-';
b[3]='.';
b[4]=(inIdx>3)?(char)('0'+inBox[3]):'-';
b[5]='\0';
A->display_freq(b,36,1,false);
}
} else if(askSave){ /* pick a save slot */
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else if(askDelete){
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else {
freqBig(st.freq_playing);
}
}
static void show(void){ draw(); A->blit_status(); A->blit_full(); }
/* ---- non-blocking scan state machine (mirrors ACTION_Scan_FM / FM_Play / FM_Tune) ---- */
static void scanTuneNext(void) /* FM_Tune(freq, scanState, false): step + mute + settle */
{
A->fm_mute(true);
foundFreq=false;
st.freq_playing=wrap((uint16_t)(st.freq_playing+scanState));
tune();
scanTimer=SCAN_SETTLE;
}
static void stopScan(void) /* FM_PlayAndUpdate */
{
if(autoScan){ st.is_mr=true; st.sel_ch=0; }
scanState=0; autoScan=false;
configChannel();
tune();
A->fm_mute(false);
}
static void scanStep(void) /* FM_Play: one step when the settle timer expires */
{
if(A->fm_valid(st.freq_playing,lo)==0){ /* station locked */
if(!autoScan){
scanState=0; foundFreq=true;
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_mute(false); /* audio on */
return;
}
if(chPos<CHMAX) ch[chPos++]=st.freq_playing;
if(chPos>=CHMAX){ stopScan(); return; }
}
if(autoScan && st.freq_playing>=A->fm_hi(1)){ stopScan(); return; }
scanTuneNext();
}
static void startManual(int8_t dir)
{
if(scanState){ stopScan(); return; } /* STAR toggles */
autoScan=false; chPos=0; scanState=dir;
scanTuneNext();
}
static void startAuto(void)
{
if(scanState){ stopScan(); return; }
autoScan=true; chPos=0; scanState=+1;
for(uint8_t i=0;i<CHMAX;i++) ch[i]=0xFFFF;
st.freq_playing=lo;
A->fm_mute(true);
tune(); /* FM_Tune(lo,1,true): no step */
scanTimer=SCAN_SETTLE;
}
/* ---- digit entry ---- */
static void digit(uint8_t d)
{
if(askDelete||scanState) return;
if(askSave || st.is_mr){ /* 2-digit channel */
if(inIdx<2) inBox[inIdx++]=d;
if(inIdx>=2){
uint8_t c=(uint8_t)(inBox[0]*10+inBox[1]-1); inIdx=0;
if(askSave){ if(c<CHMAX) savePos=c; }
else if(validCh(c)){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
}
return;
}
/* VFO frequency: 4 digits, with the leading-digit>1 zero-pad trick */
inBox[inIdx++]=d;
if(inIdx==1 && inBox[0]>1){ inBox[1]=inBox[0]; inBox[0]=0; inIdx=2; }
else if(inIdx>3){
uint16_t f=(uint16_t)(inBox[0]*1000+inBox[1]*100+inBox[2]*10+inBox[3]); inIdx=0;
if(f>=lo && f<=hi){ st.sel_freq=f; st.freq_playing=f; tune(); }
}
}
/* ---- MENU: save (VFO) / delete (MR) ---- */
static void menu(void)
{
inIdx=0;
if(!st.is_mr){ /* VFO: save */
if(askSave){ ch[savePos]=st.freq_playing; dirty(); }
askSave=!askSave;
} else { /* MR: delete */
if(askDelete){ ch[st.sel_ch]=0xFFFF; configChannel(); tune(); dirty(); }
askDelete=!askDelete;
}
}
/* ---- UP/DOWN: tune (VFO) or channel step (MR) ---- */
static void upDown(int8_t step)
{
if(scanState){ scanState=step; scanTuneNext(); return; } /* continue scan, new direction */
if(askSave){ savePos=(uint8_t)((savePos+step+CHMAX)%CHMAX); return; }
if(st.is_mr){
uint8_t c=findNext((uint8_t)(st.sel_ch+step),step);
if(c!=0xFF && c!=st.sel_ch){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
} else {
st.sel_freq=wrap((uint16_t)(st.sel_freq+step));
st.freq_playing=st.sel_freq; tune();
}
}
static void toggleMr(void)
{
st.is_mr=!st.is_mr;
if(configChannel()!=0){ /* no valid channel: stayed VFO */ }
tune();
askSave=askDelete=false; inIdx=0;
}
static bool running;
static void cycleBand(void)
{
st.band=(uint8_t)((st.band+1)&3u); bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi){ st.freq_playing=lo; st.sel_freq=lo; }
tune();
}
/* F+key and long-press functions (band / VFO<->MR / auto-scan / quit). */
static void func(uint8_t key)
{
switch(key){
case APP_KEY_0: running=false; break;
case APP_KEY_1: cycleBand(); break;
case APP_KEY_3: toggleMr(); break;
case APP_KEY_STAR: startAuto(); break;
default: break;
}
}
static void onShort(uint8_t key)
{
if(key==APP_KEY_F){ fArm=!fArm; return; }
if(fArm){ fArm=false; func(key); return; } /* F + key */
if(key<=APP_KEY_9){ digit(key); return; }
switch(key){
case APP_KEY_UP:
case APP_KEY_DOWN: upDown(A->nav_dir(key)); break;
case APP_KEY_STAR: startManual(+1); break;
case APP_KEY_MENU: menu(); break;
case APP_KEY_EXIT:
if(inIdx) inIdx--;
else if(askSave||askDelete) askSave=askDelete=false;
else running=false;
break;
default: break;
}
}
static void onLong(uint8_t key){ fArm=false; func(key); } /* long band / MR / scan / quit */
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api)
{
A=api;
ch=A->fm_channels;
A->fm_state(&st,false); /* inherit the resident FM state */
if(st.band>3) st.band=0;
bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi) st.freq_playing=lo;
if(st.sel_freq<lo||st.sel_freq>hi) st.sel_freq=st.freq_playing;
if(st.is_mr) configChannel();
askSave=askDelete=fArm=false; inIdx=0; savePos=0;
A->fm_enter(st.freq_playing,st.band);
/* short = on release (held < ~0.5 s) · long = at the 0.5 s threshold ·
UP/DOWN = immediate step on press, then auto-repeat while held. */
running=true;
uint8_t held=APP_KEY_INVALID;
uint16_t heldMs=0;
bool firedLong=false;
bool dirty=true; /* redraw only on change */
while(running){
uint8_t key=A->get_key();
bool rep=(key==APP_KEY_UP||key==APP_KEY_DOWN);
if(key==APP_KEY_INVALID){
if(held!=APP_KEY_INVALID && !firedLong &&
held!=APP_KEY_UP && held!=APP_KEY_DOWN){
onShort(held); dirty=true;
}
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
} else if(key!=held){
held=key; heldMs=0; firedLong=false;
if(rep){ onShort(key); dirty=true; } /* immediate first step */
} else {
heldMs=(uint16_t)(heldMs+50u);
if(rep){
if(heldMs>=300){ onShort(held); dirty=true; } /* auto-repeat */
} else if(!firedLong && heldMs>=400){
firedLong=true; onLong(held); dirty=true;
}
}
/* non-blocking scan: one step each time the settle timer expires */
if(scanState){
if(scanTimer<=50) scanStep();
else scanTimer=(uint16_t)(scanTimer-50);
dirty=true;
}
if(dirty){ show(); dirty=false; }
A->delay_ms(50);
}
/* push our state back to the resident FM and persist (config + 48 channels) */
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_state(&st,true);
A->fm_commit();
A->fm_exit();
}