/* Copyright 2026 Armel F4HWN * https://github.com/armel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * APRS TX - sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK * 1200 bauds) on the TX VFO at each press of PTT or MENU. * * The source is the boot-message callsign (API boot_callsign) with an SSID; * destination, the WIDE path entries, symbol and comment are AX.25-encoded in * gen_assets.py (assets). The position, the SSID, the path (DIRECT, WIDE1-1 * or WIDE1-1,WIDE2-1) and the symbol are edited on the radio (key 5) and saved; gen_assets.py * holds the defaults. The frame is built at launch and after each edit, FCS * included. * * AFSK comes from the BK4829 tone generator, as the Beacon app keys MCW: the tone * frequency (REG_71) is rewritten at each NRZI transition, 1200 Hz mark / 2200 Hz * space, bits timed to the cycle (40000 cycles at 48 MHz) from SysTick. The * mic ADC is off while a tone is sent; any CTCSS/DCS of the VFO is cleared * (REG_51) so nothing rides under the AFSK. test/tx_model.py mirrors the frame * and bit stream and decodes them with the APRS RX model. * * Keys (UV-K5 and UV-K1): PTT or MENU send · UP/DOWN (held) scroll the frame * 1 px per loop · 1 / F then 1 tone level up / down (deviation, REG_70 gain * 10-127) · 2 / F then 2 twist up / down: 2200 Hz gain = level x (8 + tw) / 8, * tw -4..+8 (-6..+6 dB), for a receiver's de-emphasis · 5 edit the position, * SSID, path and symbol · * 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, path or symbol on theirs · F then * the previous * value · UP/DOWN move the cursor (6 digits, N/S, 7 digits, E/W, SSID, path, symbol) · * MENU check and keep · EXIT cancel. * Level, twist, position, SSID, path, symbol and view are saved on exit (cfg_save, 13 * bytes). */ #include #include #include #include "../app_api.h" #include "aprstx_assets.h" /* generated by gen_assets.py */ /* ---- SysTick (core at 48 MHz, 10 ms period) ---- */ #define SYST_LOAD (*(volatile uint32_t *)0xE000E014u) #define SYST_VAL (*(volatile uint32_t *)0xE000E018u) #define CYC_PER_BIT (48000000u / 1200u) /* 40000: exact */ /* ---- BK4829 ---- */ #define REG_51 0x51 /* sub-audio: 0 = no CTCSS/DCS (BK4819_ExitSubAu) */ #define REG_70 0x70 /* tone 1: <15> enable, <14:8> gain */ #define REG_71 0x71 /* tone 1 frequency, Hz x 10.32444 */ #define F_MARK 12389u /* 1200 Hz: (f * 1353245 + 2^16) >> 17, scale_freq */ #define F_SPACE 22714u /* 2200 Hz */ #define LVL_DEF 66 /* BK4819_TransmitTone's tone gain */ #define LVL_MIN 10 #define LVL_MAX 127 #define LVL_STEP 4 #define TW_MIN (-4) /* 2200 Hz gain x 0.5 (-6 dB) */ #define TW_MAX 8 /* 2200 Hz gain x 2 (+6 dB) */ /* ---- AX.25 ---- */ #define TXDELAY_FLAGS 40u /* 267 ms of flags before the frame */ #define TAIL_FLAGS 3u #define CALL_MAX 6u #define INFO_LEN (1u + 8u + 1u + 9u + 1u + F_COMMENT_LEN) /* !lat/lon[comment */ #define PATH_MAX 2u /* WIDE entries: 0 = DIRECT */ #define FRAME_MAX (7u + 7u + PATH_MAX * 7u + 2u + INFO_LEN + 2u) /* position: 13 digits DDMMhh DDDMMhh, hemispheres bit 0 south, bit 1 west */ #define POS_DIGITS 13u #define LAT_DIGITS 6u #define TX_CAPS_X 40u /* "TRANSMIT" capsule in the status bar, after the title */ #define F_X 70u /* F icon (9 px), as FoxHunt / FM / Beacon */ #define SCROLL_X 72u /* scroll marks (5 px), centred on the F icon that takes their place while armed; not while transmitting: the capsule ends at x = 72 */ /* editor cursor stops: 0-5 the latitude digits, N/S, 7-13 the longitude * digits, E/W, SSID, path, symbol */ #define CUR_NS LAT_DIGITS #define CUR_EW (POS_DIGITS + 1u) #define CUR_SSID (CUR_EW + 1u) #define CUR_PATH (CUR_EW + 2u) #define CUR_SYM (CUR_EW + 3u) #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 13u /* magic, level, twist, 13 digits + hemispheres in nibbles, then 0x80 | path << 4 | SSID (v0.2 configs: defaults), the view, then the symbol index (default if erased) */ #define LOOP_MS 40u #define REFRESH_LOOPS 125u /* a redraw every 5 s without a key (battery) */ enum { ST_IDLE = 0, ST_TX, ST_DENIED, ST_NOCALL, ST_BADPOS }; /* The app state in one struct: a function loads one base address instead of one * literal-pool word per global. Bytes, then the halfword, then words, so every * field stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62, * ldr <= 124); the arrays, addressed from their base, go last. The loader zeroes * the overlay (.bss) at every launch. */ static struct { uint8_t flen, prevKey, space, ones, lvl, status; int8_t tw; bool running, edit, redraw; /* redraw: draw() at the next loop */ bool fArm; /* editor: F pressed, the next key goes back */ uint8_t hemi, ehemi, cur; /* hemispheres, edited ones, editor cursor */ uint8_t ssid, essid, path, epath; /* source SSID, path, and the edited ones */ uint8_t sym, esym; /* transmitted symbol and edited index */ uint8_t tick; /* loops since the last draw() */ uint8_t top, lim, vrow; /* scroll (px) and its limit, row being drawn */ uint8_t cw; /* view: 0 scroll, CW_COMPACT compact */ uint16_t sent; /* frames sent */ const app_api_t *A; uint8_t *frm; /* the frame (app_main's stack) */ uint32_t tPrev, tCyc, next; /* SysTick cycle counter, next bit edge */ uint8_t pos[POS_DIGITS]; /* the position sent */ uint8_t ed[POS_DIGITS]; /* the position being edited */ } g; static char str[34]; /* ---- tiny freestanding helpers: GCC may turn small copies into calls ---- */ void *memset(void *d, int c, size_t n) { uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d; } void *memcpy(void *d, const void *s, size_t n) { uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d; } /* ---- formatting ---- */ static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; } /* Formatting by repeated subtraction: no division. Values printed stay below 100000. */ static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; } static const uint16_t P10[]={10000u,1000u,100u,10u,1u}; static char *puti(char *o,int32_t v){ uint32_t u; if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v; bool lead=false; for(uint8_t i=0;i<5;i++){ uint8_t c=sub(&u,P10[i]); if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; } } return o; } /* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so * every row built below stays within that. */ static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); } /* The frame is shown above the separator (y = 31), in one of two views (key *), * as in APRS RX: * - scroll (g.cw = 0): rows of 18 characters in the small font (7 px each), * 8 px apart (glyphs 7 px, bit 7 blank), scrolled by g.top pixels: 4 rows * show at g.top = 0; * - compact (g.cw = CW_COMPACT): the source in the small font, then rows of 32 * tiny characters (4 px each), 6 px apart from y = 8, the 4th ending at * y = 30; no scroll, the rest of the frame is not shown. */ #define ROW_CHARS 18u #define CW_COMPACT 14u /* compact rows: ROW_CHARS + 14 = 32 characters */ /* One row str..end of the frame, wrapped every ROW_CHARS + g.cw (neither font * clips). Scroll view: row r goes to line r - top/8 when that is 0..4: line 4 * (under the separator, redrawn after) only feeds the pixel shift of draw(). * Row 0, the source, is drawn in bold (same 7 px glyphs) on line 0 in both * views; compact rows 1-4 in the tiny font, the next ones dropped. */ static void emit(char *end){ const app_api_t *A=g.A; char *p=str; while(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 * string in the big font). The group is aligned left to reserve the rightmost * 20 columns for the APRS symbol. Big digits are 13 px (drawn from +2), the * point 3 px. No division. */ static void drawFreq(uint32_t f){ const app_api_t *A=g.A; uint16_t m=0; while(f>=100000u){ f-=100000u; m++; } char *o=puti(str,m); *o++='.'; for(uint8_t i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i])); *o='\0'; uint8_t n=(uint8_t)(o-str), big=(uint8_t)(n-3u); /* digits before "00" */ if(m>=1000u){ A->print_string(str,0,0,4,8); return; } A->print_normal(str+n-2u,(uint8_t)(big*13u+3u),0,5); /* "00" */ str[n-2u]='\0'; A->display_freq(str,0,4,false); /* "144.800" */ } #define SYM_X 108u _Static_assert(SYM_X+SYM_W<=128u,"APRS symbol must fit on screen"); static void drawSymbol(uint8_t x,uint8_t sym){ const app_api_t *A=g.A; uint16_t off=(uint16_t)(SYM_BITMAPS+(uint16_t)sym*SYM_BYTES); for(uint8_t page=0;page<3u;page++) A->asset_read((uint16_t)(off+page*SYM_W),A->fb[4u+page]+x,SYM_W); } /* AX.25 address field -> "F4HWN-7" */ static char *putCall(char *o,const uint8_t *a){ for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; } uint8_t ssid=(uint8_t)((a[6]>>1)&15u); if(ssid){ *o++='-'; o=puti(o,ssid); } return o; } /* ---- position ---- */ /* "4850.90N" (6 digits) or "00216.25E" (7 digits), as APRS sends it */ static uint8_t *putCoord(uint8_t *o,const uint8_t *d,uint8_t n,char h){ for(uint8_t i=0;i9u) return false; if(la>90u || lam>59u || lo>180u || lom>59u) return false; if(la==90u && (lam|d[4]|d[5])) return false; if(lo==180u && (lom|d[11]|d[12])) return false; return true; } /* ---- the frame: dest, source (boot callsign + SSID), path, UI, info, FCS ---- */ static void build(void){ const app_api_t *A=g.A; uint8_t *f=g.frm; char call[12]; A->boot_callsign(call,sizeof call); uint8_t n=0; while(call[n]){ if(call[n]=='/') return; n++; } /* AX.25: letters and digits */ if(!n || n>CALL_MAX) return; /* flen stays 0: no TX */ A->asset_read(F_DEST,f,F_DEST_LEN); for(uint8_t i=0;i<6u;i++) f[7+i]=(uint8_t)((iasset_read(F_WIDE,f+14,(uint16_t)(k-14u)); f[k-1]|=1u; } f[k++]=0x03; f[k++]=0xF0; /* UI frame, no layer 3 */ uint8_t sym[2]; A->asset_read((uint16_t)(SYM_CODES+g.sym*2u),sym,2); f[k++]='!'; /* position, no messaging */ k=(uint8_t)(putCoord(f+k,g.pos,LAT_DIGITS,(g.hemi&1u)?'S':'N')-f); f[k++]=sym[0]; /* symbol table */ k=(uint8_t)(putCoord(f+k,g.pos+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.hemi&2u)?'W':'E')-f); f[k++]=sym[1]; /* symbol code */ A->asset_read(F_COMMENT,f+k,F_COMMENT_LEN); k+=F_COMMENT_LEN; uint16_t c=0xFFFFu; /* CRC-16/X.25 */ for(uint8_t i=0;i>1)^0x8408u):(uint16_t)(c>>1); } c^=0xFFFFu; f[k++]=(uint8_t)c; f[k++]=(uint8_t)(c>>8); g.flen=k; } /* ---- SysTick cycle counter (call at least every 10 ms; compare with a signed * difference) ---- */ static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; } static uint32_t clkCyc(void){ uint32_t v=SYST_VAL; g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v); g.tPrev=v; return g.tCyc; } /* ---- AFSK ---- */ static void setTone(void){ const app_api_t *A=g.A; uint32_t v=g.lvl; if(g.space){ v=(v*(uint32_t)(8+g.tw))>>3; if(v>LVL_MAX) v=LVL_MAX; } A->bk_write(REG_71,(uint16_t)(g.space?F_SPACE:F_MARK)); A->bk_write(REG_70,(uint16_t)(0x8000u|(v<<8))); } /* one bit on the air: NRZI (a 0 changes the tone), from the next bit edge */ static void sendBit(uint8_t b){ while((int32_t)(clkCyc()-g.next)<0){} if(!b){ g.space^=1u; setTone(); } g.next+=CYC_PER_BIT; } /* one byte LSB first; inside the frame, a 0 is stuffed after five 1s */ static void sendByte(uint8_t v,bool stuff){ for(uint8_t i=0;i<8u;i++){ uint8_t b=(uint8_t)(v&1u); v>>=1; sendBit(b); if(stuff){ if(!b) g.ones=0; else if(++g.ones==5u){ sendBit(0); g.ones=0; } } } } /* "LAT 48 50.90 N" on a normal-font line (7 px per character). The line's * cursor stops are its n digits then the hemisphere (cur = n); the one under cur * is redrawn in bold (glyphs overwrite their columns). A cursor off the line * (negative, or past the hemisphere) shows none. */ static void editRow(const char *label,uint8_t line,const uint8_t *d,uint8_t n,char h,int8_t cur){ char *o=put(str,label); uint8_t col=0; for(uint8_t i=0;iprint_normal(str,0,0,line); if((uint8_t)cur<=n){ str[0]=str[col]; str[1]='\0'; g.A->print_bold(str,(uint8_t)(col*7u),0,line); } } /* A labelled value on a normal-font line, at x; the value is bold when selected. */ static void fieldRow(const char *label,const char *val,uint8_t x,uint8_t line,bool sel){ char *o=put(put(str,label),val); *o='\0'; g.A->print_normal(str,x,0,line); if(sel) g.A->print_bold(val,(uint8_t)(x+FIELD_COL*7u),0,line); } /* ---- display ---- */ static void draw(void){ /* The UI texts live in the assets: one read per frame into this word-aligned * stack block (gen_assets.py keeps every offset aligned). */ char s[UI_SIZE] __attribute__((aligned(4))); const app_api_t *A=g.A; char *o; A->asset_read(0,s,UI_SIZE); A->display_clear(); A->status_clear(); A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u)); if(g.status==ST_TX) /* a second capsule while on the air */ A->print_inverse(s+T_TX,TX_CAPS_X,0,true,true,(uint8_t)(TX_CAPS_X+T_TX_CHARS*4u)); if(g.fArm) /* editor: F armed, as FoxHunt / FM / Beacon */ A->asset_read(BMP_F,A->status_line+F_X,BMP_F_LEN); A->draw_battery(); if(g.edit){ int8_t c=(int8_t)g.cur; /* each line takes its own stops from it */ editRow(s+T_LAT,0,g.ed,LAT_DIGITS,(g.ehemi&1u)?'S':'N',c); editRow(s+T_LON,1,g.ed+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.ehemi&2u)?'W':'E', (int8_t)(c-(int8_t)(CUR_NS+1u))); char v[3]; *puti(v,g.essid)='\0'; fieldRow(s+T_SSID,v,0,2,g.cur==CUR_SSID); fieldRow(s+T_PATH,s+T_PATHS+g.epath*T_PATHS_STRIDE,0,3,g.cur==CUR_PATH); A->asset_read((uint16_t)(SYM_CODES+g.esym*2u),v,2); v[2]='\0'; if(g.cur==CUR_SYM){ fieldRow(s+T_SYM,v,0,4,true); drawSymbol(SYM_X,g.esym); tiny(40,put(str,s+T_HELP1)); tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2)); } else { fieldRow(s+T_SYM,v,0,4,false); tiny(40,put(str,s+T_HELP1)); tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2)); } } else { g.vrow=0; if(g.flen){ const uint8_t *f=g.frm; o=putCall(str,f+7); emit(o); /* source */ /* ">APZK5,WIDE1-1," / "WIDE2-1": a row breaks after a comma when * the next call (up to 9 characters) might not fit */ o=str; *o++='>'; o=putCall(o,f); uint8_t e=13; /* the last address byte */ while(!(f[e]&1u)){ e+=7; *o++=','; if(o>str+ROW_CHARS+g.cw-9u){ emit(o); o=str; } o=putCall(o,f+e-6); } emit(o); /* info "!DDMM.hhN/DDDMM.hhE[comment" (built here, always this * layout): "48 50.90N" / "002 16.25E" (brk) / "/[ " + the comment */ const uint8_t *q=f+e+4u; /* after control, PID and '!' */ uint8_t end=(uint8_t)(g.flen-2u); o=str; for(uint8_t k=0;k<18u;k++){ if(k==8u){ o=brk(o); continue; } /* symbol table: later */ if(k==2u||k==12u) *o++=' '; *o++=(char)q[k]; } emit(o); o=str; *o++=(char)q[8]; *o++=(char)q[18]; *o++=' '; for(uint8_t i=(uint8_t)(q+19u-f);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); /* 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()); drawSymbol(SYM_X,g.sym); /* bottom line: "lvl 66 tw +0 sent 3", or why nothing was sent */ if(g.status==ST_DENIED||g.status==ST_NOCALL) o=put(str,g.status==ST_DENIED?s+T_DENIED:s+T_NOCALL); else { o=put(str,s+T_LVL); o=puti(o,g.lvl); o=put(o,s+T_TW); if(g.tw>0) *o++='+'; o=puti(o,g.tw); o=put(o,s+T_SENT); o=puti(o,g.sent); } tiny(49,o); } A->blit_status(); A->blit_full(); } /* ---- one frame on the air ---- */ static void send(void){ const app_api_t *A=g.A; if(!g.flen){ g.status=ST_NOCALL; return; } if(A->tx_state()){ g.status=ST_DENIED; return; } g.status=ST_TX; draw(); A->tx_set_params(); /* carrier + PA on the TX VFO */ A->bk_write(REG_51,0); /* no CTCSS/DCS under the AFSK */ A->tx_tone(1200); /* tone path on, mic off, 50 ms settle */ g.space=0; setTone(); /* mark at our level */ clkStart(); g.next=CYC_PER_BIT; for(uint8_t i=0;itx_mute(true); A->tx_end(); /* PA off + RX restored */ if(g.sent<9999u) g.sent++; g.status=ST_IDLE; } /* ---- input ---- */ static void handleKeys(void){ const app_api_t *A=g.A; uint8_t key=A->get_key(); /* UP/DOWN, held, out of the editor: scroll 1 px per loop (25 px/s), from * the source row to the last row fully shown (its blank bit 7 on the * separator) */ if(!g.edit){ int8_t d=A->nav_dir(key); /* g.lim: draw(), 0 when compact */ if((d<0 && g.top) || (d>0 && g.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, path and symbol editor */ g.status=ST_IDLE; uint8_t cur=g.cur; bool lon=cur>CUR_NS && cur<=CUR_EW; /* on the longitude line */ if(key<=APP_KEY_9){ if(curnav_dir(key); if(d<0 && g.cur) g.cur--; if(d>0 && g.cur=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.twasset_read(POS_DEF,g.pos,POS_DIGITS); api->asset_read(POS_DEF+POS_DIGITS,&g.hemi,1); api->cfg_load(c,CFG_LEN); if(c[0]==CFG_MAGIC){ if(c[1]>=LVL_MIN && c[1]<=LVL_MAX) g.lvl=c[1]; if((int8_t)c[2]>=TW_MIN && (int8_t)c[2]<=TW_MAX) g.tw=(int8_t)c[2]; for(uint8_t i=0;i>((i&1u)*4u))&15u); uint8_t h=(uint8_t)(c[9]>>4); if(posOk(g.ed) && h<=3u){ memcpy(g.pos,g.ed,POS_DIGITS); g.hemi=h; } h=c[10]; /* 0x80 | path << 4 | SSID; erased (0xFF) or 0: defaults */ if((h&0xC0u)==0x80u && ((h>>4)&3u)<=PATH_MAX){ g.ssid=(uint8_t)(h&15u); g.path=(uint8_t)((h>>4)&3u); } if(c[11]==CW_COMPACT) g.cw=CW_COMPACT; if(c[12]backlight_on(); g.running=true; g.redraw=true; while(g.running){ handleKeys(); if(!g.running) break; /* The screen only changes on a key (or a send it starts): redraw then, * and every 5 s for the battery, not at each loop. The state stays in g: * locals here would live on the stack across the inlined calls. */ if(++g.tick>=REFRESH_LOOPS) g.redraw=true; if(g.redraw){ g.redraw=false; g.tick=0; draw(); } api->battery_sample(); api->delay_ms(LOOP_MS); api->backlight_update(); /* normal BLTime timeout; keys re-arm it */ } c[0]=CFG_MAGIC; c[1]=g.lvl; c[2]=(uint8_t)g.tw; for(uint8_t i=3;icfg_save(c,CFG_LEN); }