mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
APRS RX/TX: scrollable large-font frame view, compact view toggle
This commit is contained in:
1 parent
4934f83fea
commit
af8142990d
7 files changed
+326
-117
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user