mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
596 lines
26 KiB
C
596 lines
26 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.
|
|
*/
|
|
|
|
/*
|
|
* APRS TX - sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK
|
|
* 1200 bauds) on the TX VFO at each press of PTT or MENU.
|
|
*
|
|
* The source is the boot-message callsign (API boot_callsign) with an SSID;
|
|
* destination, the WIDE path entries, symbol and comment are AX.25-encoded in
|
|
* gen_assets.py (assets). The position, the SSID and the path (DIRECT, WIDE1-1
|
|
* or WIDE1-1,WIDE2-1) are edited on the radio (key 5) and saved; gen_assets.py
|
|
* holds the defaults. The frame is built at launch and after each edit, FCS
|
|
* included.
|
|
*
|
|
* AFSK comes from the BK4829 tone generator, as the Beacon app keys MCW: the tone
|
|
* frequency (REG_71) is rewritten at each NRZI transition, 1200 Hz mark / 2200 Hz
|
|
* space, bits timed to the cycle (40000 cycles at 48 MHz) from SysTick. The
|
|
* mic ADC is off while a tone is sent; any CTCSS/DCS of the VFO is cleared
|
|
* (REG_51) so nothing rides under the AFSK. test/tx_model.py mirrors the frame
|
|
* and bit stream and decodes them with the APRS RX model.
|
|
*
|
|
* Keys (UV-K5 and UV-K1): PTT or MENU send · UP/DOWN (held) scroll the frame
|
|
* 1 px per loop · 1 / F then 1 tone level up / down (deviation, REG_70 gain
|
|
* 10-127) · 2 / F then 2 twist up / down: 2200 Hz gain = level x (8 + tw) / 8,
|
|
* tw -4..+8 (-6..+6 dB), for a receiver's de-emphasis · 5 edit the position,
|
|
* SSID and path · * scroll view / compact view · EXIT quit. F as FoxHunt's: the next key goes down, an icon
|
|
* in the status bar while armed.
|
|
* Editor (the field under the cursor in bold): 0-9 type a digit (the cursor
|
|
* skips the separators and N/S, and moves on) · * toggle N/S or E/W on the
|
|
* position lines, next SSID or path on theirs · F then * the previous SSID or
|
|
* path · UP/DOWN move the cursor (6 digits, N/S, 7 digits, E/W, SSID, path) ·
|
|
* MENU check and keep · EXIT cancel.
|
|
* Level, twist, position, SSID, path and view are saved on exit (cfg_save, 12
|
|
* bytes).
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include "../app_api.h"
|
|
#include "aprstx_assets.h" /* generated by gen_assets.py */
|
|
|
|
/* ---- SysTick (core at 48 MHz, 10 ms period) ---- */
|
|
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u)
|
|
#define SYST_VAL (*(volatile uint32_t *)0xE000E018u)
|
|
#define CYC_PER_BIT (48000000u / 1200u) /* 40000: exact */
|
|
|
|
/* ---- BK4829 ---- */
|
|
#define REG_51 0x51 /* sub-audio: 0 = no CTCSS/DCS (BK4819_ExitSubAu) */
|
|
#define REG_70 0x70 /* tone 1: <15> enable, <14:8> gain */
|
|
#define REG_71 0x71 /* tone 1 frequency, Hz x 10.32444 */
|
|
#define F_MARK 12389u /* 1200 Hz: (f * 1353245 + 2^16) >> 17, scale_freq */
|
|
#define F_SPACE 22714u /* 2200 Hz */
|
|
|
|
#define LVL_DEF 66 /* BK4819_TransmitTone's tone gain */
|
|
#define LVL_MIN 10
|
|
#define LVL_MAX 127
|
|
#define LVL_STEP 4
|
|
#define TW_MIN (-4) /* 2200 Hz gain x 0.5 (-6 dB) */
|
|
#define TW_MAX 8 /* 2200 Hz gain x 2 (+6 dB) */
|
|
|
|
/* ---- AX.25 ---- */
|
|
#define TXDELAY_FLAGS 40u /* 267 ms of flags before the frame */
|
|
#define TAIL_FLAGS 3u
|
|
#define CALL_MAX 6u
|
|
#define INFO_LEN (1u + 8u + 1u + 9u + 1u + F_COMMENT_LEN) /* !lat/lon[comment */
|
|
#define PATH_MAX 2u /* WIDE entries: 0 = DIRECT */
|
|
#define FRAME_MAX (7u + 7u + PATH_MAX * 7u + 2u + INFO_LEN + 2u)
|
|
|
|
/* position: 13 digits DDMMhh DDDMMhh, hemispheres bit 0 south, bit 1 west */
|
|
#define POS_DIGITS 13u
|
|
#define LAT_DIGITS 6u
|
|
#define TX_CAPS_X 40u /* "TRANSMIT" capsule in the status bar, after the title */
|
|
#define F_X 70u /* F icon (9 px), as FoxHunt / FM / Beacon */
|
|
#define SCROLL_X 72u /* scroll marks (5 px), centred on the F icon that takes
|
|
their place while armed; not while transmitting:
|
|
the capsule ends at x = 72 */
|
|
/* editor cursor stops: 0-5 the latitude digits, N/S, 7-13 the longitude
|
|
* digits, E/W, SSID, path */
|
|
#define CUR_NS LAT_DIGITS
|
|
#define CUR_EW (POS_DIGITS + 1u)
|
|
#define CUR_SSID (CUR_EW + 1u)
|
|
#define CUR_PATH (CUR_EW + 2u)
|
|
#define FIELD_COL 5u /* "SSID " / "PATH ": the value's column */
|
|
#define CFG_MAGIC 0xA8u /* 0xA7 (v0.1: level and twist only) is ignored */
|
|
#define CFG_LEN 12u /* magic, level, twist, 13 digits + hemispheres in nibbles,
|
|
then 0x80 | path << 4 | SSID (v0.2 configs: defaults),
|
|
then the view (CW_COMPACT, else scroll: 0xFF before v0.4) */
|
|
#define LOOP_MS 40u
|
|
#define REFRESH_LOOPS 125u /* a redraw every 5 s without a key (battery) */
|
|
|
|
enum { ST_IDLE = 0, ST_TX, ST_DENIED, ST_NOCALL, ST_BADPOS };
|
|
|
|
/* The app state in one struct: a function loads one base address instead of one
|
|
* literal-pool word per global. Bytes, then the halfword, then words, so every
|
|
* field stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62,
|
|
* ldr <= 124); the arrays, addressed from their base, go last. The loader zeroes
|
|
* the overlay (.bss) at every launch. */
|
|
static struct {
|
|
uint8_t flen, prevKey, space, ones, lvl, status;
|
|
int8_t tw;
|
|
bool running, edit, redraw; /* redraw: draw() at the next loop */
|
|
bool fArm; /* editor: F pressed, the next key goes back */
|
|
uint8_t hemi, ehemi, cur; /* hemispheres, edited ones, editor cursor */
|
|
uint8_t ssid, essid, path, epath; /* source SSID, path, and the edited ones */
|
|
uint8_t tick; /* loops since the last draw() */
|
|
uint8_t top, lim, vrow; /* scroll (px) and its limit, row being drawn */
|
|
uint8_t cw; /* view: 0 scroll, CW_COMPACT compact */
|
|
uint16_t sent; /* frames sent */
|
|
const app_api_t *A;
|
|
uint8_t *frm; /* the frame (app_main's stack) */
|
|
uint32_t tPrev, tCyc, next; /* SysTick cycle counter, next bit edge */
|
|
uint8_t pos[POS_DIGITS]; /* the position sent */
|
|
uint8_t ed[POS_DIGITS]; /* the position being edited */
|
|
} g;
|
|
static char str[34];
|
|
|
|
/* ---- tiny freestanding helpers: GCC may turn small copies into calls ---- */
|
|
void *memset(void *d, int c, size_t n) {
|
|
uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d;
|
|
}
|
|
void *memcpy(void *d, const void *s, size_t n) {
|
|
uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d;
|
|
}
|
|
|
|
/* ---- formatting ---- */
|
|
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
|
/* Formatting by repeated subtraction: no division. Values printed stay below 100000. */
|
|
static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; }
|
|
static const uint16_t P10[]={10000u,1000u,100u,10u,1u};
|
|
static char *puti(char *o,int32_t v){
|
|
uint32_t u;
|
|
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
|
|
bool lead=false;
|
|
for(uint8_t i=0;i<5;i++){
|
|
uint8_t c=sub(&u,P10[i]);
|
|
if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; }
|
|
}
|
|
return o;
|
|
}
|
|
/* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so
|
|
* every row built below stays within that. */
|
|
static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); }
|
|
|
|
/* The frame is shown above the separator (y = 31), in one of two views (key *),
|
|
* as in APRS RX:
|
|
* - scroll (g.cw = 0): rows of 18 characters in the small font (7 px each),
|
|
* 8 px apart (glyphs 7 px, bit 7 blank), scrolled by g.top pixels: 4 rows
|
|
* show at g.top = 0;
|
|
* - compact (g.cw = CW_COMPACT): the source in the small font, then rows of 32
|
|
* tiny characters (4 px each), 6 px apart from y = 8, the 4th ending at
|
|
* y = 30; no scroll, the rest of the frame is not shown. */
|
|
#define ROW_CHARS 18u
|
|
#define CW_COMPACT 14u /* compact rows: ROW_CHARS + 14 = 32 characters */
|
|
|
|
/* One row str..end of the frame, wrapped every ROW_CHARS + g.cw (neither font
|
|
* clips). Scroll view: row r goes to line r - top/8 when that is 0..4: line 4
|
|
* (under the separator, redrawn after) only feeds the pixel shift of draw().
|
|
* Row 0, the source, is drawn in bold (same 7 px glyphs) on line 0 in both
|
|
* views; compact rows 1-4 in the tiny font, the next ones dropped. */
|
|
static void emit(char *end){
|
|
const app_api_t *A=g.A;
|
|
char *p=str;
|
|
while(p<end){
|
|
uint8_t w=(uint8_t)(ROW_CHARS+g.cw); /* compare lengths: p + w may lie past str */
|
|
char *q=(end-p>w)?p+w:end;
|
|
char c=*q; *q='\0';
|
|
uint8_t r=g.vrow, ln=(uint8_t)(r-(g.top>>3)); /* top = 0 when compact */
|
|
if(g.cw && r){ if(r<5u) A->print_tiny(p,0,(uint8_t)(r*6u+2u),false,true); }
|
|
else if(ln<5u) (r?A->print_normal:A->print_bold)(p,0,0,ln);
|
|
*q=c;
|
|
g.vrow=(uint8_t)(r+1u);
|
|
p=q;
|
|
}
|
|
}
|
|
|
|
/* Between two parts of a row ("48 50.90N" / "002 16.25E"): a new row in the
|
|
* scroll view, a space in the compact one. */
|
|
static char *brk(char *o){
|
|
if(g.cw){ *o++=' '; return o; }
|
|
emit(o);
|
|
return str;
|
|
}
|
|
/* The frequency (10 Hz units) drawn as the main screen draws it (ui/main.c,
|
|
* ENABLE_BIG_FREQ): the big digits up to the kHz on lines 4-5, the last two
|
|
* digits in the small font on line 5 right after them (from 1 GHz, the whole
|
|
* string in the big font), the group centred. Big digits are 13 px (drawn from
|
|
* +2), the point 3 px: the group shows (n big digits x 13 + 14) px. No division. */
|
|
static void drawFreq(uint32_t f){
|
|
const app_api_t *A=g.A;
|
|
uint16_t m=0;
|
|
while(f>=100000u){ f-=100000u; m++; }
|
|
char *o=puti(str,m);
|
|
*o++='.';
|
|
for(uint8_t i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i]));
|
|
*o='\0';
|
|
uint8_t n=(uint8_t)(o-str), big=(uint8_t)(n-3u); /* digits before "00" */
|
|
if(m>=1000u){ A->print_string(str,(uint8_t)((128u-n*8u)>>1),0,4,8); return; }
|
|
uint8_t x=(uint8_t)(((128u-(big*13u+14u))>>1)-2u);
|
|
A->print_normal(str+n-2u,(uint8_t)(x+big*13u+3u),0,5); /* "00" */
|
|
str[n-2u]='\0';
|
|
A->display_freq(str,x,4,false); /* "144.800" */
|
|
}
|
|
/* AX.25 address field -> "F4HWN-7" */
|
|
static char *putCall(char *o,const uint8_t *a){
|
|
for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; }
|
|
uint8_t ssid=(uint8_t)((a[6]>>1)&15u);
|
|
if(ssid){ *o++='-'; o=puti(o,ssid); }
|
|
return o;
|
|
}
|
|
|
|
/* ---- position ---- */
|
|
/* "4850.90N" (6 digits) or "00216.25E" (7 digits), as APRS sends it */
|
|
static uint8_t *putCoord(uint8_t *o,const uint8_t *d,uint8_t n,char h){
|
|
for(uint8_t i=0;i<n;i++){ if(i==n-2u) *o++='.'; *o++=(uint8_t)('0'+d[i]); }
|
|
*o++=(uint8_t)h;
|
|
return o;
|
|
}
|
|
/* degrees <= 90 / 180 (and nothing beyond), minutes < 60 */
|
|
static bool posOk(const uint8_t *d){
|
|
uint8_t la=(uint8_t)(d[0]*10u+d[1]), lam=(uint8_t)(d[2]*10u+d[3]);
|
|
uint8_t lom=(uint8_t)(d[9]*10u+d[10]);
|
|
uint16_t lo=(uint16_t)(d[6]*100u+d[7]*10u+d[8]);
|
|
for(uint8_t i=0;i<POS_DIGITS;i++) if(d[i]>9u) return false;
|
|
if(la>90u || lam>59u || lo>180u || lom>59u) return false;
|
|
if(la==90u && (lam|d[4]|d[5])) return false;
|
|
if(lo==180u && (lom|d[11]|d[12])) return false;
|
|
return true;
|
|
}
|
|
|
|
/* ---- the frame: dest, source (boot callsign + SSID), path, UI, info, FCS ---- */
|
|
static void build(void){
|
|
const app_api_t *A=g.A;
|
|
uint8_t *f=g.frm;
|
|
char call[12];
|
|
A->boot_callsign(call,sizeof call);
|
|
uint8_t n=0;
|
|
while(call[n]){ if(call[n]=='/') return; n++; } /* AX.25: letters and digits */
|
|
if(!n || n>CALL_MAX) return; /* flen stays 0: no TX */
|
|
|
|
A->asset_read(F_DEST,f,F_DEST_LEN);
|
|
for(uint8_t i=0;i<6u;i++) f[7+i]=(uint8_t)((i<n?call[i]:' ')<<1);
|
|
f[13]=(uint8_t)(0x60u|(g.ssid<<1)|(g.path?0u:1u)); /* end bit if DIRECT */
|
|
uint8_t k=(uint8_t)(14u+g.path*7u); /* the first path WIDEs */
|
|
if(g.path){ A->asset_read(F_WIDE,f+14,(uint16_t)(k-14u)); f[k-1]|=1u; }
|
|
f[k++]=0x03; f[k++]=0xF0; /* UI frame, no layer 3 */
|
|
uint8_t sym[2];
|
|
A->asset_read(F_SYM,sym,2);
|
|
f[k++]='!'; /* position, no messaging */
|
|
k=(uint8_t)(putCoord(f+k,g.pos,LAT_DIGITS,(g.hemi&1u)?'S':'N')-f);
|
|
f[k++]=sym[0]; /* symbol table */
|
|
k=(uint8_t)(putCoord(f+k,g.pos+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.hemi&2u)?'W':'E')-f);
|
|
f[k++]=sym[1]; /* symbol code */
|
|
A->asset_read(F_COMMENT,f+k,F_COMMENT_LEN); k+=F_COMMENT_LEN;
|
|
uint16_t c=0xFFFFu; /* CRC-16/X.25 */
|
|
for(uint8_t i=0;i<k;i++){
|
|
c^=f[i];
|
|
for(uint8_t b=0;b<8u;b++) c=(c&1u)?(uint16_t)((c>>1)^0x8408u):(uint16_t)(c>>1);
|
|
}
|
|
c^=0xFFFFu;
|
|
f[k++]=(uint8_t)c; f[k++]=(uint8_t)(c>>8);
|
|
g.flen=k;
|
|
}
|
|
|
|
/* ---- SysTick cycle counter (call at least every 10 ms; compare with a signed
|
|
* difference) ---- */
|
|
static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; }
|
|
static uint32_t clkCyc(void){
|
|
uint32_t v=SYST_VAL;
|
|
g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v);
|
|
g.tPrev=v;
|
|
return g.tCyc;
|
|
}
|
|
|
|
/* ---- AFSK ---- */
|
|
static void setTone(void){
|
|
const app_api_t *A=g.A;
|
|
uint32_t v=g.lvl;
|
|
if(g.space){ v=(v*(uint32_t)(8+g.tw))>>3; if(v>LVL_MAX) v=LVL_MAX; }
|
|
A->bk_write(REG_71,(uint16_t)(g.space?F_SPACE:F_MARK));
|
|
A->bk_write(REG_70,(uint16_t)(0x8000u|(v<<8)));
|
|
}
|
|
/* one bit on the air: NRZI (a 0 changes the tone), from the next bit edge */
|
|
static void sendBit(uint8_t b){
|
|
while((int32_t)(clkCyc()-g.next)<0){}
|
|
if(!b){ g.space^=1u; setTone(); }
|
|
g.next+=CYC_PER_BIT;
|
|
}
|
|
/* one byte LSB first; inside the frame, a 0 is stuffed after five 1s */
|
|
static void sendByte(uint8_t v,bool stuff){
|
|
for(uint8_t i=0;i<8u;i++){
|
|
uint8_t b=(uint8_t)(v&1u);
|
|
v>>=1;
|
|
sendBit(b);
|
|
if(stuff){
|
|
if(!b) g.ones=0;
|
|
else if(++g.ones==5u){ sendBit(0); g.ones=0; }
|
|
}
|
|
}
|
|
}
|
|
|
|
/* "LAT 48 50.90 N" on a normal-font line (7 px per character). The line's
|
|
* cursor stops are its n digits then the hemisphere (cur = n); the one under cur
|
|
* is redrawn in bold (glyphs overwrite their columns). A cursor off the line
|
|
* (negative, or past the hemisphere) shows none. */
|
|
static void editRow(const char *label,uint8_t line,const uint8_t *d,uint8_t n,char h,int8_t cur){
|
|
char *o=put(str,label);
|
|
uint8_t col=0;
|
|
for(uint8_t i=0;i<n;i++){
|
|
if(i==n-4u) *o++=' ';
|
|
if(i==n-2u) *o++='.';
|
|
if((int8_t)i==cur) col=(uint8_t)(o-str);
|
|
*o++=(char)('0'+d[i]);
|
|
}
|
|
*o++=' ';
|
|
if((int8_t)n==cur) col=(uint8_t)(o-str);
|
|
*o++=h; *o='\0';
|
|
g.A->print_normal(str,0,0,line);
|
|
if((uint8_t)cur<=n){ str[0]=str[col]; str[1]='\0'; g.A->print_bold(str,(uint8_t)(col*7u),0,line); }
|
|
}
|
|
|
|
/* "SSID 7" / "PATH WIDE1-1" on a normal-font line, the value in bold when the
|
|
* cursor is on it */
|
|
static void fieldRow(const char *label,const char *val,uint8_t line,bool sel){
|
|
char *o=put(put(str,label),val);
|
|
*o='\0';
|
|
g.A->print_normal(str,0,0,line);
|
|
if(sel) g.A->print_bold(val,FIELD_COL*7u,0,line);
|
|
}
|
|
|
|
/* ---- display ---- */
|
|
static void draw(void){
|
|
/* The UI texts live in the assets: one read per frame into this word-aligned
|
|
* stack block (gen_assets.py keeps every offset aligned). */
|
|
char s[UI_SIZE] __attribute__((aligned(4)));
|
|
const app_api_t *A=g.A;
|
|
char *o;
|
|
A->asset_read(0,s,UI_SIZE);
|
|
A->display_clear();
|
|
A->status_clear();
|
|
A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u));
|
|
if(g.status==ST_TX) /* a second capsule while on the air */
|
|
A->print_inverse(s+T_TX,TX_CAPS_X,0,true,true,(uint8_t)(TX_CAPS_X+T_TX_CHARS*4u));
|
|
if(g.fArm) /* editor: F armed, as FoxHunt / FM / Beacon */
|
|
A->asset_read(BMP_F,A->status_line+F_X,BMP_F_LEN);
|
|
A->draw_battery();
|
|
|
|
if(g.edit){
|
|
int8_t c=(int8_t)g.cur; /* each line takes its own stops from it */
|
|
editRow(s+T_LAT,0,g.ed,LAT_DIGITS,(g.ehemi&1u)?'S':'N',c);
|
|
editRow(s+T_LON,1,g.ed+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.ehemi&2u)?'W':'E',
|
|
(int8_t)(c-(int8_t)(CUR_NS+1u)));
|
|
char v[3];
|
|
*puti(v,g.essid)='\0';
|
|
fieldRow(s+T_SSID,v,2,g.cur==CUR_SSID);
|
|
fieldRow(s+T_PATH,s+T_PATHS+g.epath*T_PATHS_STRIDE,3,g.cur==CUR_PATH);
|
|
tiny(40,put(str,s+T_HELP1));
|
|
tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2));
|
|
} else {
|
|
g.vrow=0;
|
|
if(g.flen){
|
|
const uint8_t *f=g.frm;
|
|
o=putCall(str,f+7);
|
|
emit(o); /* source */
|
|
/* ">APZK5,WIDE1-1," / "WIDE2-1": a row breaks after a comma when
|
|
* the next call (up to 9 characters) might not fit */
|
|
o=str; *o++='>'; o=putCall(o,f);
|
|
uint8_t e=13; /* the last address byte */
|
|
while(!(f[e]&1u)){
|
|
e+=7; *o++=',';
|
|
if(o>str+ROW_CHARS+g.cw-9u){ emit(o); o=str; }
|
|
o=putCall(o,f+e-6);
|
|
}
|
|
emit(o);
|
|
/* info "!DDMM.hhN/DDDMM.hhE[comment" (built here, always this
|
|
* layout): "48 50.90N" / "002 16.25E" (brk) / "/[ " + the comment */
|
|
const uint8_t *q=f+e+4u; /* after control, PID and '!' */
|
|
uint8_t end=(uint8_t)(g.flen-2u);
|
|
o=str;
|
|
for(uint8_t k=0;k<18u;k++){
|
|
if(k==8u){ o=brk(o); continue; } /* symbol table: later */
|
|
if(k==2u||k==12u) *o++=' ';
|
|
*o++=(char)q[k];
|
|
}
|
|
emit(o);
|
|
o=str; *o++=(char)q[8]; *o++=(char)q[18]; *o++=' ';
|
|
for(uint8_t i=(uint8_t)(q+19u-f);i<end;i++){
|
|
*o++=(char)f[i];
|
|
if(o==str+ROW_CHARS+g.cw){ emit(o); o=str; }
|
|
}
|
|
emit(o);
|
|
}
|
|
/* the scroll limit: the last row fully shown, its blank bit 7 on the
|
|
* separator; 0 when compact or when the frame fits */
|
|
uint8_t lim=0;
|
|
if(!g.cw && g.vrow>4u) lim=(uint8_t)(g.vrow*8u-32u);
|
|
g.lim=lim;
|
|
/* an up mark while rows hide above, a down mark while rows hide below */
|
|
uint8_t mk=0;
|
|
if(g.top) mk=1u;
|
|
if(g.top<lim) mk+=2u;
|
|
if(mk && !g.fArm && g.status!=ST_TX)
|
|
A->asset_read((uint16_t)(BMP_SCROLL+(mk-1u)*BMP_SCROLL_W),A->status_line+SCROLL_X,BMP_SCROLL_W);
|
|
|
|
/* Pixel scroll: lines 0-3 go up top%8 px, the bits below coming from
|
|
* the next line (an 8-bit shift of the next line gives 0 at top%8 = 0).
|
|
* Line 4 was only the source of line 3: cleared for the frequency. */
|
|
uint8_t sh=g.top&7u, *b=A->fb[0];
|
|
for(uint16_t k=0;k<512u;k++) b[k]=(uint8_t)((b[k]>>sh)|(b[k+128u]<<(8u-sh)));
|
|
memset(b+512,0,128);
|
|
|
|
/* dotted separator above the frequency: y = 31, bit 7 of line 3, over
|
|
* whatever the shift brought there (the big digits start at y = 33: one
|
|
* blank px between) */
|
|
for(uint8_t x=0;x<128u;x++) b[384u+x]=(uint8_t)((b[384u+x]&0x7Fu)|((~x&1u)<<7));
|
|
drawFreq(A->tx_freq());
|
|
|
|
/* bottom line: "lvl 66 tw +0 sent 3", or why nothing was sent */
|
|
if(g.status==ST_DENIED||g.status==ST_NOCALL)
|
|
o=put(str,g.status==ST_DENIED?s+T_DENIED:s+T_NOCALL);
|
|
else {
|
|
o=put(str,s+T_LVL); o=puti(o,g.lvl);
|
|
o=put(o,s+T_TW); if(g.tw>0) *o++='+'; o=puti(o,g.tw);
|
|
o=put(o,s+T_SENT); o=puti(o,g.sent);
|
|
}
|
|
tiny(49,o);
|
|
}
|
|
|
|
A->blit_status();
|
|
A->blit_full();
|
|
}
|
|
|
|
/* ---- one frame on the air ---- */
|
|
static void send(void){
|
|
const app_api_t *A=g.A;
|
|
if(!g.flen){ g.status=ST_NOCALL; return; }
|
|
if(A->tx_state()){ g.status=ST_DENIED; return; }
|
|
g.status=ST_TX;
|
|
draw();
|
|
|
|
A->tx_set_params(); /* carrier + PA on the TX VFO */
|
|
A->bk_write(REG_51,0); /* no CTCSS/DCS under the AFSK */
|
|
A->tx_tone(1200); /* tone path on, mic off, 50 ms settle */
|
|
g.space=0;
|
|
setTone(); /* mark at our level */
|
|
clkStart();
|
|
g.next=CYC_PER_BIT;
|
|
for(uint8_t i=0;i<TXDELAY_FLAGS;i++) sendByte(0x7E,false);
|
|
g.ones=0;
|
|
for(uint8_t i=0;i<g.flen;i++) sendByte(g.frm[i],true);
|
|
for(uint8_t i=0;i<TAIL_FLAGS;i++) sendByte(0x7E,false);
|
|
while((int32_t)(clkCyc()-g.next)<0){} /* the last bit plays out */
|
|
A->tx_mute(true);
|
|
A->tx_end(); /* PA off + RX restored */
|
|
|
|
if(g.sent<9999u) g.sent++;
|
|
g.status=ST_IDLE;
|
|
}
|
|
|
|
/* ---- input ---- */
|
|
static void handleKeys(void){
|
|
const app_api_t *A=g.A;
|
|
uint8_t key=A->get_key();
|
|
/* UP/DOWN, held, out of the editor: scroll 1 px per loop (25 px/s), from
|
|
* the source row to the last row fully shown (its blank bit 7 on the
|
|
* separator) */
|
|
if(!g.edit){
|
|
int8_t d=A->nav_dir(key); /* g.lim: draw(), 0 when compact */
|
|
if((d<0 && g.top) || (d>0 && g.top<g.lim)){ g.top=(uint8_t)(g.top+d); g.redraw=true; }
|
|
}
|
|
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
|
|
g.prevKey=key;
|
|
g.redraw=true; /* a key changes the screen, a send too */
|
|
A->backlight_on();
|
|
if(key==APP_KEY_F){ g.fArm=!g.fArm; return; }
|
|
bool back=g.fArm; /* F then a key: it goes backwards (down) */
|
|
g.fArm=false;
|
|
if(g.edit){ /* position, SSID and path editor */
|
|
g.status=ST_IDLE;
|
|
uint8_t cur=g.cur;
|
|
bool lon=cur>CUR_NS && cur<=CUR_EW; /* on the longitude line */
|
|
if(key<=APP_KEY_9){
|
|
if(cur<CUR_NS || (lon && cur<CUR_EW)){ /* a digit: its stop is one past it on LON */
|
|
g.ed[cur-lon]=key;
|
|
if(++cur==CUR_NS) cur++; /* typing goes on past N/S ... */
|
|
if(cur==CUR_EW) cur--; /* ... and stays on the last digit */
|
|
g.cur=cur;
|
|
}
|
|
}
|
|
else if(key==APP_KEY_STAR){
|
|
if(cur<=CUR_EW) g.ehemi^=(uint8_t)(lon?2u:1u); /* on a digit or the letter */
|
|
else if(cur==CUR_SSID) g.essid=(uint8_t)((g.essid+(back?15u:1u))&15u);
|
|
else if(back) g.epath=(uint8_t)(g.epath?g.epath-1u:PATH_MAX);
|
|
else g.epath=(uint8_t)(g.epath<PATH_MAX?g.epath+1u:0u);
|
|
}
|
|
else if(key==APP_KEY_MENU){
|
|
if(!posOk(g.ed)){ g.status=ST_BADPOS; return; }
|
|
memcpy(g.pos,g.ed,POS_DIGITS);
|
|
g.hemi=g.ehemi; g.ssid=g.essid; g.path=g.epath;
|
|
g.edit=false;
|
|
build();
|
|
if(!g.flen) g.status=ST_NOCALL;
|
|
}
|
|
else if(key==APP_KEY_EXIT) g.edit=false;
|
|
else {
|
|
int8_t d=A->nav_dir(key);
|
|
if(d<0 && g.cur) g.cur--;
|
|
if(d>0 && g.cur<CUR_PATH) g.cur++;
|
|
}
|
|
return;
|
|
}
|
|
if(key==APP_KEY_EXIT) g.running=false;
|
|
else if(key==APP_KEY_MENU||key==APP_KEY_PTT) send();
|
|
else if(key==APP_KEY_1){
|
|
if(back){ if(g.lvl>=LVL_MIN+LVL_STEP) g.lvl=(uint8_t)(g.lvl-LVL_STEP); }
|
|
else if(g.lvl<=LVL_MAX-LVL_STEP) g.lvl=(uint8_t)(g.lvl+LVL_STEP);
|
|
}
|
|
else if(key==APP_KEY_2){
|
|
if(back){ if(g.tw>TW_MIN) g.tw--; }
|
|
else if(g.tw<TW_MAX) g.tw++;
|
|
}
|
|
else if(key==APP_KEY_STAR){ g.cw^=CW_COMPACT; g.top=0; } /* the view */
|
|
else if(key==APP_KEY_5){
|
|
memcpy(g.ed,g.pos,POS_DIGITS);
|
|
g.ehemi=g.hemi; g.essid=g.ssid; g.epath=g.path;
|
|
g.cur=0;
|
|
g.top=0; /* the frame may change: back to its top */
|
|
g.edit=true;
|
|
if(g.status!=ST_NOCALL) g.status=ST_IDLE;
|
|
}
|
|
}
|
|
|
|
__attribute__((section(".text.entry"),used))
|
|
void app_main(const app_api_t *api){
|
|
uint8_t frm[FRAME_MAX]; /* on the stack: the 4 KiB overlay also holds .bss */
|
|
uint8_t c[CFG_LEN];
|
|
g.A=api;
|
|
g.frm=frm;
|
|
g.prevKey=APP_KEY_INVALID; /* the rest of g starts at 0 (overlay zeroed by the loader) */
|
|
g.lvl=LVL_DEF;
|
|
g.ssid=CFG_SSID;
|
|
g.path=CFG_PATH;
|
|
api->asset_read(POS_DEF,g.pos,POS_DIGITS);
|
|
api->asset_read(POS_DEF+POS_DIGITS,&g.hemi,1);
|
|
api->cfg_load(c,CFG_LEN);
|
|
if(c[0]==CFG_MAGIC){
|
|
if(c[1]>=LVL_MIN && c[1]<=LVL_MAX) g.lvl=c[1];
|
|
if((int8_t)c[2]>=TW_MIN && (int8_t)c[2]<=TW_MAX) g.tw=(int8_t)c[2];
|
|
for(uint8_t i=0;i<POS_DIGITS;i++) g.ed[i]=(uint8_t)((c[3+i/2u]>>((i&1u)*4u))&15u);
|
|
uint8_t h=(uint8_t)(c[9]>>4);
|
|
if(posOk(g.ed) && h<=3u){ memcpy(g.pos,g.ed,POS_DIGITS); g.hemi=h; }
|
|
h=c[10]; /* 0x80 | path << 4 | SSID; erased (0xFF) or 0: defaults */
|
|
if((h&0xC0u)==0x80u && ((h>>4)&3u)<=PATH_MAX){ g.ssid=(uint8_t)(h&15u); g.path=(uint8_t)((h>>4)&3u); }
|
|
if(c[11]==CW_COMPACT) g.cw=CW_COMPACT;
|
|
}
|
|
build();
|
|
if(!g.flen) g.status=ST_NOCALL;
|
|
api->backlight_on();
|
|
|
|
g.running=true;
|
|
g.redraw=true;
|
|
while(g.running){
|
|
handleKeys();
|
|
if(!g.running) break;
|
|
/* The screen only changes on a key (or a send it starts): redraw then,
|
|
* and every 5 s for the battery, not at each loop. The state stays in g:
|
|
* locals here would live on the stack across the inlined calls. */
|
|
if(++g.tick>=REFRESH_LOOPS) g.redraw=true;
|
|
if(g.redraw){ g.redraw=false; g.tick=0; draw(); }
|
|
api->battery_sample();
|
|
api->delay_ms(LOOP_MS);
|
|
api->backlight_update(); /* normal BLTime timeout; keys re-arm it */
|
|
}
|
|
|
|
c[0]=CFG_MAGIC; c[1]=g.lvl; c[2]=(uint8_t)g.tw;
|
|
for(uint8_t i=3;i<CFG_LEN;i++) c[i]=0;
|
|
for(uint8_t i=0;i<POS_DIGITS;i++) c[3+i/2u]|=(uint8_t)(g.pos[i]<<((i&1u)*4u));
|
|
c[9]|=(uint8_t)(g.hemi<<4); /* the 14th nibble */
|
|
c[10]=(uint8_t)(0x80u|(g.path<<4)|g.ssid);
|
|
c[11]=g.cw;
|
|
api->cfg_save(c,CFG_LEN);
|
|
}
|