diff --git a/App/apps/aprsrx/README.md b/App/apps/aprsrx/README.md index 480f3dd3..37a1a3ec 100644 --- a/App/apps/aprsrx/README.md +++ b/App/apps/aprsrx/README.md @@ -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 | diff --git a/App/apps/aprsrx/aprsrx_app.c b/App/apps/aprsrx/aprsrx_app.c index 75e4a27f..419f9e22 100644 --- a/App/apps/aprsrx/aprsrx_app.c +++ b/App/apps/aprsrx/aprsrx_app.c @@ -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.tFramebuf,n); g.flen=n; - if(g.nOkrssi_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(; ow)?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>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);kasset_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.topasset_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.topaudio_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 && ++busyForbacklight_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 diff --git a/App/apps/aprsrx/gen_assets.py b/App/apps/aprsrx/gen_assets.py index 6b5fc663..84c06abc 100755 --- a/App/apps/aprsrx/gen_assets.py +++ b/App/apps/aprsrx/gen_assets.py @@ -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() diff --git a/App/apps/aprstx/README.md b/App/apps/aprstx/README.md index ea7b22b7..e57d5e44 100644 --- a/App/apps/aprstx/README.md +++ b/App/apps/aprstx/README.md @@ -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 diff --git a/App/apps/aprstx/aprstx_app.c b/App/apps/aprstx/aprstx_app.c index 1c73d7b4..b0b22afd 100644 --- a/App/apps/aprstx/aprstx_app.c +++ b/App/apps/aprstx/aprstx_app.c @@ -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 @@ -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(pw)?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(; ostr+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);i4u) 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.topasset_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.topbacklight_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=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.twnav_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;icfg_save(c,CFG_LEN); } diff --git a/App/apps/aprstx/gen_assets.py b/App/apps/aprstx/gen_assets.py index 2aaa5e25..1c8710a7 100755 --- a/App/apps/aprstx/gen_assets.py +++ b/App/apps/aprstx/gen_assets.py @@ -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() diff --git a/App/apps/aprstx/test/tx_model.py b/App/apps/aprstx/test/tx_model.py index 747d2f95..58c8d126 100644 --- a/App/apps/aprstx/test/tx_model.py +++ b/App/apps/aprstx/test/tx_model.py @@ -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