APRS RX/TX: scrollable large-font frame view, compact view toggle

This commit is contained in:
Armel FAUVEAU committed 2026-10-01 19:22:24 +02:00
1 parent 4934f83fea
commit af8142990d
7 files changed
+326 -117

No files matched your search

+4 -4
View File
@@ -39,10 +39,8 @@ The screen is redrawn after a new frame, a key, or every 5 s.
| Screen | Content |
|---|---|
| Status bar | `APRS RX` title, then a `WAIT` capsule until the first frame, and the speaker icon while the speaker is on |
| Line 0 | Source call and frame number, e.g. `F4HWN-7 #3` |
| Tiny row y=8 | `>DEST,DIGI*,...` (as many path entries as fit on one row) |
| Tiny rows y=14/20/26 | Info field, 3 × 32 characters (non-ASCII shown as `.`); for Mic-E (most Yaesu/Kenwood beacons, v0.3): position `48 50.89N 002 16.25E`, then speed km/h, course, symbol and message type (`Off Duty`, `En Route`...), then altitude and comment without the device markers; for an uncompressed position (`!` `=` `/` `@`, v0.5): position, then the timestamp if any, the symbol (table + code) and the comment over two rows. Compressed positions stay raw text |
| Status bar | `APRS RX` title, then a `WAIT` capsule until the first frame, the speaker icon while the speaker is on, then in the scroll view ▲ while rows are hidden above and ▼ while rows are hidden below (x = 72-76 as in APRS TX, the speaker icon at x = 59-68; both drawn in one asset read) |
| Lines 0-3 | The frame in the small font (18 characters a row, non-ASCII shown as `.`), 4 rows at a time, scrolled pixel by pixel with UP/DOWN (v0.7): source call and frame number in bold (`F4HWN-7 #3`), `>DEST,DIGI*,...` (as many path entries as fit two rows), then the info field, whole. For Mic-E (most Yaesu/Kenwood beacons, v0.3): latitude `48 50.89N`, longitude `002 16.25E`, speed km/h, course and symbol, the message type (`Off Duty`, `En Route`...), then altitude and comment without the device markers; for an uncompressed position (`!` `=` `/` `@`, v0.5): latitude, longitude, then the timestamp if any, the symbol (table + code) and the comment. Compressed positions stay raw text. A new frame scrolls back to the top. Key * switches to the compact view (below) |
| Lines 4-5 | Under a dotted separator (y=31), the frequency drawn as the main screen draws it (big digits up to the kHz, the last two in the small font), centred; same code as APRS TX |
| Bottom row y=49 | `ok 12 -89dBm`: frames received, RSSI at the end of the last frame. (v0.4-v0.5 also showed frames per slicer, `sl a/b/c`: on air the outer slicers found frames too; dropped in v0.6 to fit the speaker key) |
@@ -50,6 +48,8 @@ Keys (UV-K5 and UV-K1):
| Key | Action |
|---|---|
| UP/DOWN (held) | Scroll the frame 1 px per 50 ms slot (UV-K1: LEFT/RIGHT, as `nav_dir`), from the source row down to the last row |
| * | Scroll view / compact view, saved (the flash is written only after a change). Compact: the source in bold, then the path and 3 info rows in the tiny 3x5 font, 32 characters each (position `48 50.89N 002 16.25E` on one row, speed, course, symbol and message type on the next, then the comment), as up to v0.6; no scroll |
| 1 | Speaker on/off (off at launch); FoxHunt's speaker icon in the status bar while on |
| MENU | Clear the last frame and the counters |
| EXIT | Quit |
+146 -63
View File
@@ -33,8 +33,9 @@
* are served every 50 ms only while no slicer is inside a preamble or a
* plausible frame; the redraw follows a new frame, a key, or every 5 s.
*
* Keys (UV-K5 and UV-K1): 1 speaker on/off (off at launch: the decoder does
* not need it) · MENU clear · EXIT quit.
* Keys (UV-K5 and UV-K1): UP/DOWN (held) scroll the frame 1 px per slot ·
* * scroll view / compact view (saved) · 1 speaker on/off (off at launch:
* the decoder does not need it) · MENU clear · EXIT quit.
* The loader re-runs RADIO_SetupRegisters on exit; the app restores the ADC,
* PA4, the DAC and its clock itself.
*/
@@ -45,18 +46,26 @@
#include "../app_api.h"
#include "aprsrx_assets.h" /* generated by gen_assets.py */
/* ---- MCU registers (PY32F071, core and SysTick at 48 MHz, 10 ms period) ---- */
/* ---- MCU registers (PY32F071, core and SysTick at 48 MHz, 10 ms period) ----
* The ADC and DAC registers are reached from one base per block: the base goes
* through an empty asm, so GCC addresses them as [base, #offset] from one
* literal-pool word instead of one word per register address. */
static inline volatile uint32_t *hw(uint32_t a){
volatile uint32_t *p=(volatile uint32_t *)a;
__asm__("" : "+l"(p));
return p;
}
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u)
#define SYST_VAL (*(volatile uint32_t *)0xE000E018u)
#define ADC_SR (*(volatile uint32_t *)0x40012400u)
#define ADC_CR2 (*(volatile uint32_t *)0x40012408u)
#define ADC_SMPR3 (*(volatile uint32_t *)0x40012414u)
#define ADC_SQR3 (*(volatile uint32_t *)0x40012438u)
#define ADC_DR (*(volatile uint32_t *)0x40012450u)
#define ADC_SR (hw(0x40012400u)[0x00/4])
#define ADC_CR2 (hw(0x40012400u)[0x08/4])
#define ADC_SMPR3 (hw(0x40012400u)[0x14/4])
#define ADC_SQR3 (hw(0x40012400u)[0x38/4])
#define ADC_DR (hw(0x40012400u)[0x50/4])
#define GPIOA_MODER (*(volatile uint32_t *)0x50000000u)
#define DAC_CR (*(volatile uint32_t *)0x40007400u)
#define DAC_SWTRIGR (*(volatile uint32_t *)0x40007404u)
#define DAC_DHR12R1 (*(volatile uint32_t *)0x40007408u)
#define DAC_CR (hw(0x40007400u)[0x00/4])
#define DAC_SWTRIGR (hw(0x40007400u)[0x04/4])
#define DAC_DHR12R1 (hw(0x40007400u)[0x08/4])
#define RCC_APBENR1 (*(volatile uint32_t *)0x4002103Cu)
#define RCC_DACEN (1u << 29)
/* DAC channel 1 on, output buffer off, software trigger (TSEL1 = 111) */
@@ -95,7 +104,6 @@
#define FRAME_MAX 330u /* 10 addresses + control + PID + 256 info + FCS */
#define FRAME_MIN 18u /* 2 addresses + control + PID + FCS */
#define FCS_GOOD 0xF0B8u
#define COUNT_MAX 999u /* counters stop here: the stats row stays within 32 tiny chars */
/* the front end shared by the slicers */
typedef struct {
@@ -103,8 +111,10 @@ typedef struct {
int32_t x1, x2, y1, y2; /* band-pass state */
int32_t im, qm, is, qs; /* sliding correlator sums */
int32_t pm, ps; /* per-tone magnitude peaks (AGC) */
uint32_t r, ks; /* ring slot = 1200 Hz phase, 2200 Hz phase;
words: dem_t is inlined on the stack, where
only words have an sp-relative load/store */
int16_t ring[8][4]; /* the products of the last bit */
uint8_t r, ks; /* ring slot = 1200 Hz phase, 2200 Hz phase */
int8_t cm[8], cs[48]; /* cos tables (assets) */
} dem_t;
/* listen() loads both tables in one asset_read: they must stay back to back,
@@ -135,6 +145,8 @@ typedef struct {
static struct {
uint8_t seq, prevKey, redraw, spk; /* spk: speaker amplifier on (key 1) */
bool running;
uint8_t top, lim, vrow; /* scroll (px) and its limit, row being drawn */
uint8_t cw; /* view: 0 scroll, CW_COMPACT compact (saved) */
int16_t rssi; /* RSSI at the end of the last frame */
uint16_t flen, nOk; /* frame length, frames */
const app_api_t *A;
@@ -174,21 +186,28 @@ static char *puti(char *o,int32_t v){
static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); }
#define WAIT_CAPS_X 40u /* "WAIT" capsule in the status bar, after the title */
#define SPK_X 62u /* speaker icon (FoxHunt's), after the capsule */
#define SPK_X 59u /* speaker icon (FoxHunt's, x = 59-68) after the capsule
(x <= 56), then the scroll marks at x = 72-76 as in APRS
TX, before the battery text (x >= 83): BMP_TAIL in the
assets, both in one read */
/* Tiny rows under the source line: the path, then 3 info rows, 6 px apart (the
* 3x5 glyphs are 5 px: one blank px between rows, descenders aside), the last
* ending at y = 30, one px above the separator (y = 31). */
#define Y_PATH 8u
#define Y_INFO 14u
#define ROW_STEP 6u
/* The frame is shown above the separator (y = 31), in one of two views (key *):
* - 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 3x5 glyphs: one
* blank px between rows), 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 */
/* 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.
* Same code as APRS TX. */
* digits in the small font on line 5 right after them, 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. As APRS TX, without its 1 GHz case (the
* main screen's small font there): 1296.000 in big digits still fits (105 px). */
static void drawFreq(uint32_t f){
const app_api_t *A=g.A;
uint16_t m=0;
@@ -198,7 +217,6 @@ static void drawFreq(uint32_t f){
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';
@@ -259,16 +277,20 @@ static void accept(const sl_t *m){
&& t-g.tFrame<DUP_MS)){
memcpy(g.frm,m->buf,n);
g.flen=n;
if(g.nOk<COUNT_MAX) g.nOk++;
g.nOk++; /* wraps after 65535: still one tiny row */
g.seq++;
g.rssi=A->rssi_dbm(); /* the carrier is still up: closing flags */
g.top=0;
g.redraw|=REDRAW_FRAME;
}
g.tFrame=t;
}
/* ---- HDLC: one bit after NRZI. A UI frame with a good FCS goes to accept()
* (m->buf, m->n bytes) before the closing flag opens the next frame. ---- */
* (m->buf, m->n bytes) before the closing flag opens the next frame. Not
* inlined: a function of its own, its m stays in a register (inlined in the
* slicer loop, it cost 56 B more); called 3600 times a second only. ---- */
__attribute__((noinline))
static void hdlcBit(sl_t *m,uint8_t b){
if(m->bits<255u) m->bits++;
m->sr=(uint8_t)((m->sr>>1)|(b<<7));
@@ -338,13 +360,13 @@ static void front(dem_t *m,int32_t adc,int32_t *a,int32_t *b){
* At 9.6 kHz one bit and one 1200 Hz period are both 8 samples: the ring slot
* is also the 1200 Hz table phase. */
int16_t *o=m->ring[m->r];
uint8_t k=(uint8_t)(m->ks+12u); if(k>=48u) k-=48u;
uint32_t k=m->ks+12u; if(k>=48u) k-=48u;
int32_t p;
p=(y*m->cm[m->r])>>8; m->im+=p-o[0]; o[0]=(int16_t)p;
p=(y*m->cm[(m->r+6u)&7u])>>8; m->qm+=p-o[1]; o[1]=(int16_t)p;
p=(y*m->cs[m->ks])>>8; m->is+=p-o[2]; o[2]=(int16_t)p;
p=(y*m->cs[k])>>8; m->qs+=p-o[3]; o[3]=(int16_t)p;
m->r=(uint8_t)((m->r+1u)&7u);
m->r=(m->r+1u)&7u;
if(++m->ks>=48u) m->ks=0;
int32_t mm=tone(m->im,m->qm,&m->pm), ms=tone(m->is,m->qs,&m->ps);
@@ -378,12 +400,42 @@ static char *putCall(char *o,const uint8_t *a){
/* The tiny font covers 0x20-0x7F: anything else shows as '.'. */
static char safe(uint8_t ch){ return (ch<0x20u||ch>0x7Eu)?'.':(char)ch; }
/* Info bytes f[i..end) on one tiny row from o (up to 32 characters); returns
* the next byte. */
static uint16_t textRow(char *o,const uint8_t *f,uint16_t i,uint16_t end,uint8_t y){
for(; o<str+32 && i<end; i++) *o++=safe(f[i]);
tiny(y,o);
return i;
/* 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 char *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;
}
return str; /* the next row starts there */
}
/* Between two parts of a row ("48 50.89N" / "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; }
return emit(o);
}
/* Info bytes f[i..end) after the text already at str..o, on as many rows as
* they take. */
static void textRows(char *o,const uint8_t *f,uint16_t i,uint16_t end){
do{
for(; o<str+ROW_CHARS+g.cw && i<end; i++) *o++=safe(f[i]);
o=emit(o);
}while(i<end);
}
/* ---- Mic-E (APRS 1.0.1 chapter 10; test/mice.py is the reference) ----
@@ -406,7 +458,7 @@ static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){
uint8_t c[6];
for(uint8_t k=0;k<6u;k++) c[k]=(uint8_t)(f[k]>>1);
/* "48 50.89N 002 16.25E" */
/* "48 50.89N" / "002 16.25E" (brk) */
char *o=str;
for(uint8_t k=0;k<6u;k++){
*o++=(char)('0'+micDigit(c[k]));
@@ -414,17 +466,18 @@ static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){
if(k==3u) *o++='.';
}
*o++=micBit(c[3])?'N':'S';
o=brk(o);
uint32_t deg=(uint8_t)(f[i+1]-28u);
if(micBit(c[4])) deg+=100u;
if(deg>=180u && deg<=189u) deg-=80u;
else if(deg>=190u && deg<=199u) deg-=190u;
uint32_t mn=(uint8_t)(f[i+2]-28u);
if(mn>=60u) mn-=60u;
*o++=' '; o=put3(o,deg); *o++=' '; o=put2(o,mn); *o++='.'; o=put2(o,(uint8_t)(f[i+3]-28u));
o=put3(o,deg); *o++=' '; o=put2(o,mn); *o++='.'; o=put2(o,(uint8_t)(f[i+3]-28u));
*o++=micBit(c[5])?'W':'E';
tiny(Y_INFO,o);
emit(o);
/* "12km/h 245 [/ En Route": speed in knots x 1.8525 (1897/1024) */
/* "12km/h 245 [/" / "En Route" (brk): speed in knots x 1.8525 (1897/1024) */
uint32_t dc=(uint8_t)(f[i+5]-28u);
uint32_t sp=(uint32_t)(uint8_t)(f[i+4]-28u)*10u+sub(&dc,10u);
uint32_t crs=dc*100u+(uint8_t)(f[i+6]-28u);
@@ -432,13 +485,14 @@ static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){
if(crs>=400u) crs-=400u;
o=puti(str,(int32_t)((sp*1897u)>>10)); o=put(o,s+T_KMH);
o=puti(o,(int32_t)crs); *o++=' ';
*o++=safe(f[i+7]); *o++=safe(f[i+8]); *o++=' ';
*o++=safe(f[i+7]); *o++=safe(f[i+8]);
o=brk(o);
uint8_t idx=(uint8_t)(micBit(c[0])*4u+micBit(c[1])*2u+micBit(c[2]));
bool custom=false;
for(uint8_t k=0;k<3u;k++) if(c[k]>='A'&&c[k]<='K') custom=true;
if(custom && idx){ o=put(o,s+T_CUSTOM); *o++=(char)('0'+7u-idx); }
else o+=A->asset_read((uint16_t)(T_MSG+idx*T_MSG_STRIDE),o,T_MSG_STRIDE); /* NUL-padded */
tiny(Y_INFO+ROW_STEP,o);
emit(o);
/* comment: trailing CR/LF, the device markers (Yaesu/other: '`' or '\'' +
* text + 2 chars; Kenwood: '>' or ']' + text [+ '=' or '^']) and a leading
@@ -456,13 +510,14 @@ static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){
o=puti(o,alt); *o++='m'; *o++=' ';
j+=4u;
}
textRow(o,f,j,end,Y_INFO+2u*ROW_STEP);
textRows(o,f,j,end);
}
/* ---- uncompressed position ('!' '=' or, after a 7-character timestamp, '/'
* '@'; test/mice.py draw_pos is the reference): "49 21.07N 001 50.50E", then
* the timestamp, the symbol and the comment. False for anything else (compressed
* positions included), which is then shown as raw text. ---- */
* positions included), which is then shown as raw text. The position takes
* two rows in the scroll view, "49 21.07N" / "001 50.50E", one when compact. ---- */
static bool drawPos(const uint8_t *f,uint16_t i,uint16_t end){
uint8_t t=f[i];
uint16_t p=(uint16_t)(i+1u);
@@ -472,19 +527,19 @@ static bool drawPos(const uint8_t *f,uint16_t i,uint16_t end){
const uint8_t *q=f+p; /* DDMM.hhN T DDDMM.hhE C */
char *o=str;
for(uint8_t k=0;k<18u;k++){
if(k==8u) continue; /* symbol table: next row */
if(k==2u||k==9u||k==12u) *o++=' ';
if(k==8u){ o=brk(o); continue; } /* symbol table: later */
if(k==2u||k==12u) *o++=' ';
*o++=safe(q[k]);
}
tiny(Y_INFO,o);
o=str;
o=emit(o);
if(p>i+1u){ for(uint16_t k=(uint16_t)(i+1u);k<p;k++) *o++=safe(f[k]); *o++=' '; }
*o++=safe(q[8]); *o++=safe(q[18]); *o++=' ';
textRow(str,f,textRow(o,f,(uint16_t)(p+19u),end,Y_INFO+ROW_STEP),end,Y_INFO+2u*ROW_STEP);
textRows(o,f,(uint16_t)(p+19u),end);
return true;
}
/* ---- display ---- */
/* ---- display ---- (not inlined: 16 B less) */
__attribute__((noinline))
static void draw(void){
const app_api_t *A=g.A;
/* The UI texts live in the assets: one read per frame into this word-aligned
@@ -495,9 +550,9 @@ static void draw(void){
A->asset_read(0,s,UI_SIZE);
A->display_clear();
A->status_clear();
g.vrow=0;
A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u));
A->draw_battery();
if(g.spk) A->asset_read(BMP_SPEAKER,A->status_line+SPK_X,BMP_SPEAKER_LEN);
if(!g.flen) /* no frame yet: a capsule, as APRS TX's TRANSMIT */
A->print_inverse(s+T_WAIT,WAIT_CAPS_X,0,true,true,(uint8_t)(WAIT_CAPS_X+T_WAIT_CHARS*4u));
@@ -505,10 +560,10 @@ static void draw(void){
const uint8_t *f=g.frm;
uint16_t end=(uint16_t)(g.flen-2u); /* the FCS is not shown */
o=putCall(str,f+7); *o++=' '; *o++='#'; o=puti(o,g.seq); *o='\0';
A->print_normal(str,0,0,0); /* source */
o=putCall(str,f+7); *o++=' '; *o++='#'; o=puti(o,g.seq);
emit(o); /* source */
/* ">DEST,DIGI*,..." while it fits one tiny row, then the rest of the
/* ">DEST,DIGI*,..." while it fits 2 rows, then the rest of the
* address field is skipped up to its end bit */
o=str; *o++='>'; o=putCall(o,f);
uint16_t e=13; /* SSID byte of the current address */
@@ -516,19 +571,37 @@ static void draw(void){
e+=7;
if(o<=str+20){ *o++=','; o=putCall(o,f+e-6); if(f[e]&0x80u) *o++='*'; }
}
tiny(Y_PATH,o);
emit(o);
/* info field (after control and PID): Mic-E and uncompressed positions
* decoded, anything else as text on 3 rows of 32 characters */
* decoded, anything else as text */
uint16_t i=(uint16_t)(e+3u);
if(end>=i+9u && micType(f[i])) drawMicE(f,i,end,s);
else if(!drawPos(f,i,end))
for(uint8_t row=0;row<3u;row++) i=textRow(str,f,i,end,(uint8_t)(Y_INFO+row*ROW_STEP));
else if(!drawPos(f,i,end)) textRows(str,f,i,end);
}
/* 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;
/* status bar from SPK_X: the speaker icon while on, an up mark while rows
* hide above, a down mark while rows hide below */
uint8_t t=g.spk;
if(g.top) t+=2u;
if(g.top<lim) t+=4u;
A->asset_read((uint16_t)(BMP_TAIL+t*TAIL_W),A->status_line+SPK_X,TAIL_W);
/* dotted separator above the frequency: y = 31, bit 7 of line 3 (the big
* digits start at y = 33: one blank px between) */
for(uint8_t x=0;x<128u;x+=2u) A->fb[3][x]|=0x80u;
/* 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(g.vfoFreq);
/* bottom line: "ok 12 -89dBm": frames, RSSI of the last one */
@@ -541,12 +614,20 @@ static void draw(void){
static void handleKeys(void){
const app_api_t *A=g.A;
uint8_t key=A->get_key();
/* UP/DOWN, held: scroll 1 px per slot (20 px/s), from the source row to
* g.lim (draw(); 0 when compact: no scroll) */
int8_t d=A->nav_dir(key);
if((d<0 && g.top) || (d>0 && g.top<g.lim)){ g.top=(uint8_t)(g.top+d); g.redraw|=REDRAW_ON; }
if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; }
g.prevKey=key;
g.redraw|=REDRAW_ON;
if(key==APP_KEY_EXIT) g.running=false;
else if(key==APP_KEY_MENU){ g.flen=0; g.nOk=0; g.seq=0; g.rssi=0; }
else if(key==APP_KEY_MENU){ g.flen=0; g.nOk=0; g.seq=0; g.rssi=0; g.top=0; } /* g.lim: 0 at the redraw that follows */
else if(key==APP_KEY_1){ g.spk^=1u; A->audio_path(g.spk); } /* speaker, as FoxHunt's audio */
else if(key==APP_KEY_STAR){ /* the view, staged now: flash is written only after a change */
g.cw^=CW_COMPACT; g.top=0;
A->cfg_save(&g.cw,1);
}
}
/* ---- keys and screen, between frames ---- */
@@ -586,7 +667,7 @@ static void listen(void){
adcSelPA4();
clkStart();
uint32_t next=CYC_PER_SAMPLE;
uint32_t next=0; /* the first sample at once */
uint16_t cnt=0;
uint8_t busyFor=0;
g.redraw=REDRAW_ON;
@@ -603,7 +684,7 @@ static void listen(void){
if(bz && ++busyFor<BUSY_MAX) continue;
busyFor=0;
house();
next=clkCyc()+CYC_PER_SAMPLE; /* skip the samples the slot took */
next=clkCyc(); /* skip the samples the slot took */
}
adcRestore();
}
@@ -620,6 +701,8 @@ void app_main(const app_api_t *api){
biasOn();
api->backlight_on();
g.vfoFreq=api->rx_freq();
api->cfg_load(&g.cw,1); /* the view; erased flash (0xFF) or junk: scroll */
if(g.cw!=CW_COMPACT) g.cw=0;
/* Speaker amplifier (PA8) off: the decoder does not need it (tested on the
* radio, 2026-09-30): PA4, the voice-prompt DAC pin, joins the audio before
* the amplifier; only the BK4829 AF output must be on. Key 1 turns the
+12 -1
View File
@@ -47,7 +47,18 @@ a.const("UI_SIZE", ui_size) # the UI block read by draw()
a.const("T_TITLE_CHARS", len(TITLE))
a.const("T_WAIT_CHARS", len(WAIT_CAPS))
a.table("T_MSG", MIC_MSG) # Mic-E standard messages
a.u8("BMP_SPEAKER", [0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c]) # FoxHunt's
# The status bar from x = 59 (SPK_X), in one asset_read: the speaker icon
# (bit 0 of the index), 3 blank columns, then the scroll marks at x = 72, up
# (bit 1, rows hidden above) in bits 0-2, down (bit 2, rows hidden below) in
# bits 4-6. 8 variants of TAIL_W bytes.
SPK = [0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c] # FoxHunt's
UP, DN = [0x04, 0x06, 0x07, 0x06, 0x04], [0x10, 0x30, 0x70, 0x30, 0x10]
tail = []
for i in range(8):
tail += (SPK if i & 1 else [0] * len(SPK)) + [0] * 3
tail += [(u if i & 2 else 0) | (d if i & 4 else 0) for u, d in zip(UP, DN)]
a.u8("BMP_TAIL", tail)
a.const("TAIL_W", len(tail) // 8)
a.i8("COS1200", cos_table(1200)) # 8 entries
a.i8("COS2200", cos_table(2200)) # 48 entries
a.main()
+14 -9
View File
@@ -32,10 +32,13 @@ transmit.
| Key | Action |
|---|---|
| PTT or MENU | Send one frame |
| UP / DOWN | Tone level (deviation): REG_70 gain 10-127, step 4, default 66 (the firmware's tone gain) |
| 1 / 3 | Twist `tw` -4..+8: 2200 Hz gain = level × (8 + tw) / 8 (-6..+6 dB) |
| UP / DOWN (held) | Scroll the frame above the separator 1 px per 40 ms loop (UV-K1: LEFT/RIGHT, as `nav_dir`). In the status bar (x = 72-76), ▲ while rows are hidden above and ▼ while rows are hidden below; while F is armed its icon (x = 70-78, centred on them) takes their place, and they are not drawn while transmitting (the `TRANSMIT` capsule ends at x = 72). It is shown in the small font, 18 characters a row, 4 rows at a time, as in APRS RX: source in bold, `>DEST,WIDE…` (a row breaks after a comma), latitude, longitude, then symbol and comment |
| 1 / F then 1 | Tone level (deviation) up / down: REG_70 gain 10-127, step 4, default 66 (the firmware's tone gain) |
| 2 / F then 2 | Twist `tw` up / down, -4..+8: 2200 Hz gain = level × (8 + tw) / 8 (-6..+6 dB) |
| F | Arm the next key's down direction (1, 2), as FoxHunt's F; icon in the status bar while armed |
| * | Scroll view / compact view (saved): compact shows the source in bold, then the path, the position `48 50.90N 002 16.25E`, the symbol and the comment in the tiny 3x5 font, 32 characters a row, without scroll |
| 5 | Edit the position, SSID and path |
| EXIT | Quit (level, twist, position, SSID and path are saved) |
| EXIT | Quit (level, twist, position, SSID, path and view are saved) |
### Editor (v0.3)
@@ -69,10 +72,12 @@ Paths: `DIRECT` (heard only by the stations and iGates in range), `WIDE1-1` (one
repeat by a nearby digipeater), `WIDE1-1,2-1` (`WIDE1-1,WIDE2-1`: the usual
mobile / portable path, two repeats).
Everything is saved on exit (`cfg_save`, 11 bytes: magic 0xA8, level, twist, 13
digits and the hemispheres in nibbles, then `0x80 | path << 4 | SSID`). A v0.2
config (10 bytes: byte 10 erased) keeps its position and takes the default SSID
and path; a v0.1 config (magic 0xA7) is ignored: defaults.
Everything is saved on exit (`cfg_save`, 12 bytes: magic 0xA8, level, twist, 13
digits and the hemispheres in nibbles, `0x80 | path << 4 | SSID`, then the view:
14 compact, anything else scroll). A v0.2 config (10 bytes: byte 10 erased)
keeps its position and takes the default SSID and path; an 11-byte config
(byte 11 erased) opens in the scroll view; a v0.1 config (magic 0xA7) is
ignored: defaults.
## How it transmits
@@ -90,8 +95,8 @@ SPI writes, a few tens of µs out of 833. `tx_mute` + `tx_end` restore RX.
2/3 with noise in STD); a hardware TNC such as the FT3D may be stricter.
2. **Twist.** The tone probably enters after the pre-emphasis, so the signal is
flat and a receiver with de-emphasis sees 2200 Hz ~5 dB low. If the FT3D
misses frames that APRS RX decodes, raise `tw` (key 3): +4 is +3.5 dB.
3. **Deviation** at level 66: aim for ~3 kHz; UP/DOWN.
misses frames that APRS RX decodes, raise `tw` (key 2): +4 is +3.5 dB.
3. **Deviation** at level 66: aim for ~3 kHz; 1 up, F then 1 down.
## Test plan
+128 -29
View File
@@ -32,16 +32,19 @@
* (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 tone level (deviation,
* REG_70 gain 10-127) · 1/3 twist: 2200 Hz gain = level x (8 + tw) / 8,
* 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 · EXIT quit.
* 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 and path are saved on exit (cfg_save, 11 bytes).
* Level, twist, position, SSID, path and view are saved on exit (cfg_save, 12
* bytes).
*/
#include <stdint.h>
@@ -81,6 +84,10 @@
#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
@@ -89,8 +96,9 @@
#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 11u /* magic, level, twist, 13 digits + hemispheres in nibbles,
then 0x80 | path << 4 | SSID (v0.2 configs: defaults) */
#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) */
@@ -109,6 +117,8 @@ static struct {
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) */
@@ -144,6 +154,46 @@ static char *puti(char *o,int32_t v){
/* 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
@@ -305,7 +355,7 @@ static void draw(void){
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+70,BMP_F_LEN);
A->asset_read(BMP_F,A->status_line+F_X,BMP_F_LEN);
A->draw_battery();
if(g.edit){
@@ -320,25 +370,62 @@ static void draw(void){
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); *o='\0';
A->print_normal(str,0,0,0); /* source */
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++=','; o=putCall(o,f+e-6); }
tiny(9,o); /* ">APZK5,WIDE1-1,WIDE2-1" (<= 2 hops) */
uint8_t i=(uint8_t)(e+3u), end=(uint8_t)(g.flen-2u); /* info, after control + PID */
for(uint8_t row=0;row<2u;row++){ /* info (<= 64), 2 rows of 32 */
o=str;
for(; o<str+32 && i<end; i++) *o++=(char)f[i];
tiny((uint8_t)(17u+row*7u),o);
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);
/* dotted separator above the frequency: y = 30, bit 6 of line 3 (the big
* digits start at y = 33) */
for(uint8_t x=0;x<128u;x+=2u) A->fb[3][x]|=0x40u;
/* 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 */
@@ -387,15 +474,22 @@ static void send(void){
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;
if(key==APP_KEY_F){ g.fArm=!g.fArm; return; }
bool back=g.fArm; /* F then a key: it goes backwards */
g.fArm=false;
uint8_t cur=g.cur;
bool lon=cur>CUR_NS && cur<=CUR_EW; /* on the longitude line */
if(key<=APP_KEY_9){
@@ -430,20 +524,23 @@ static void handleKeys(void){
}
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(g.tw>TW_MIN) g.tw--; }
else if(key==APP_KEY_3){ if(g.tw<TW_MAX) g.tw++; }
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;
}
else {
int8_t d=A->nav_dir(key);
if(d>0 && g.lvl<=LVL_MAX-LVL_STEP) g.lvl=(uint8_t)(g.lvl+LVL_STEP);
if(d<0 && g.lvl>=LVL_MIN+LVL_STEP) g.lvl=(uint8_t)(g.lvl-LVL_STEP);
}
}
__attribute__((section(".text.entry"),used))
@@ -467,6 +564,7 @@ void app_main(const app_api_t *api){
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;
@@ -492,5 +590,6 @@ void app_main(const app_api_t *api){
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);
}
+5
View File
@@ -92,4 +92,9 @@ a.u8("POS_DEF", POS + [HEMI])
a.raw("F_SYM", SYMBOL.encode("ascii"))
a.raw("F_COMMENT", COMMENT.encode("ascii"))
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e]) # F armed, as FoxHunt / FM / Beacon
# scroll marks (status bar, x = 75, as APRS RX): up only, down only, both; up
# in bits 0-2 (rows hidden above), down in bits 4-6 (rows hidden below)
UP, DN = [0x04, 0x06, 0x07, 0x06, 0x04], [0x10, 0x30, 0x70, 0x30, 0x10]
a.u8("BMP_SCROLL", UP + DN + [u | d for u, d in zip(UP, DN)])
a.const("BMP_SCROLL_W", 5)
a.main()
+17 -11
View File
@@ -48,22 +48,28 @@ def pos_ok(d):
return True
def cfg_pack(lvl, tw, pos, hemi, ssid, path):
c = [0xA8, lvl, tw & 0xFF] + [0] * 8
CW_COMPACT = 14 # aprstx_app.c: the compact view
def cfg_pack(lvl, tw, pos, hemi, ssid, path, cw=0):
c = [0xA8, lvl, tw & 0xFF] + [0] * 9
for i, x in enumerate(pos): c[3 + i // 2] |= x << ((i & 1) * 4)
c[9] |= hemi << 4
c[10] = 0x80 | path << 4 | ssid
c[11] = cw
return bytes(c)
def cfg_unpack(c, D):
"""aprstx_app.c app_main(): position, hemispheres, SSID and path (the
defaults when byte 10 is erased or from a v0.2 config)"""
"""aprstx_app.c app_main(): position, hemispheres, SSID, path (the
defaults when byte 10 is erased or from a v0.2 config) and view (scroll
unless byte 11 is CW_COMPACT: erased before v0.4)"""
pos = [(c[3 + i // 2] >> ((i & 1) * 4)) & 15 for i in range(13)]
cw = CW_COMPACT if c[11] == CW_COMPACT else 0
h = c[10]
if (h & 0xC0) == 0x80 and (h >> 4) & 3 <= 2:
return pos, c[9] >> 4, h & 15, (h >> 4) & 3
return pos, c[9] >> 4, D["CFG_SSID"], D["CFG_PATH"]
return pos, c[9] >> 4, h & 15, (h >> 4) & 3, cw
return pos, c[9] >> 4, D["CFG_SSID"], D["CFG_PATH"], cw
def edit_row(label, d, h, cur):
@@ -223,13 +229,13 @@ def main():
want = build("F4HWN-%d" % sid if sid else "F4HWN", dst="APZK5", path=WIDE[:path], info=info)
ok &= f3 == want
print("SSID %2d, path %d frame:" % (sid, path), f3 == want, decode(f3))
c = cfg_pack(70, -2, pos, 3, 9, 2)
good = len(c) == 11 and cfg_unpack(c, D) == (pos, 3, 9, 2)
c = cfg_pack(70, -2, pos, 3, 9, 2, CW_COMPACT)
good = len(c) == 12 and cfg_unpack(c, D) == (pos, 3, 9, 2, CW_COMPACT)
old = cfg_pack(70, -2, pos, 3, 0, 0)[:10] # a v0.2 config: byte 10 erased or 0
good &= all(cfg_unpack(old + bytes([b]), D) == (pos, 3, D["CFG_SSID"], D["CFG_PATH"])
for b in (0xFF, 0x00))
good &= all(cfg_unpack(old + bytes([b, 0xFF]), D) == (pos, 3, D["CFG_SSID"], D["CFG_PATH"], 0)
for b in (0xFF, 0x00)) # byte 11 erased: scroll view
ok &= good
print("config round trip, v0.2 config -> default SSID and path:", good, c.hex())
print("config round trip, v0.2 config -> default SSID and path, scroll view:", good, c.hex())
rows = (edit_row("LAT ", pos[:6], "S", 2), edit_row("LON ", pos[6:], "W", -1),
edit_row("LAT ", pos[:6], "S", CUR_NS), # N/S in bold
edit_row("LON ", pos[6:], "W", CUR_EW - 7), # E/W in bold