diff --git a/App/apps/aprs_symbols_20.py b/App/apps/aprs_symbols_20.py new file mode 100644 index 00000000..274c3cb8 --- /dev/null +++ b/App/apps/aprs_symbols_20.py @@ -0,0 +1,78 @@ +"""Shared 20x20 monochrome APRS symbol assets. + +Each icon is stored in native ST7565 page order: 20 bytes for each of three +8-pixel pages. Only the low four bits of the third page are used. This lets +the overlay apps copy an icon straight from external flash to the framebuffer. + +The masks were validated pixel by pixel with +aprsrx/test/aprs_symbols_20_editor.html before being packed here. +""" + +WIDTH = 20 +PAGES = 3 +BYTES_PER_ICON = WIDTH * PAGES + +CODES = ( + "/#", "/X", "/&", "/Y", "/'", "/[", "/-", "/\\", + "/.", "/^", "/0", "/_", "/:", "/a", "/;", "/b", + "/<", "/f", "/>", "/g", "/C", "/j", "/E", "/k", + "/I", "/m", "/K", "/O", "/P", "/s", "/R", "/v", + "/y", "/T", "/U", "/V", "\\#", "\\&", "\\-", "/W", + "\\.", "\\0", "E0", "I0", "W0", "YY", "\\^", "\\Y", +) + +_HEX = """ +00000000000000c0f0fcf0c000000000000000000001030787cfffffff7fffffffcf8707030100000000000607030101000000010103070600000000 +800060606020202020a0f0a020202020606060001b1f0e04040c1c3e7fff7f737f7af27c380000000000000000000000010303030303030201000000 +000000001c04047c04c4fcc4047c04041c00000000000000000080f0fccfcfcffcf08000000000000000000000040707050404040507070400000000 +0000000000000080e000fefcf0c000000000000000008080a0b8bebfbf80ffdfdfdfdfdcd0c060200000050505050505050505050505050505000000 +8080000000040c38f0e0c0800000000000000000203f1f0e0e0e0e8eeeff7f3f1f0e0e0e0e0e0e040000000000040603010000000000000000000000 +000000000000000098bcbcbc980000000000000000000000001c1e03e3ff3fffe3031e1c000000000000000000000006070700070706000000000000 +0000000000000080c0c28a2afe2a0a0200000000000000f8fcfe1f1fffff1f1f1ffcf800000000000000000707070707070704040407070000000000 +00000030f090101010101010101090f0300000000000000000030e38e08080e0380e0300000000000000000000000000000303000000000000000000 +00000078f8f0e0c080000080c0e0f0f878000000000000c0e0f1fb7f3f1f1f3f7ffbf1e0c00000000000000303010000000000000000010303000000 +00000000008080c0f0dcdcf0c0808000000000000c0e0e0707070783ffffffff83070707070e0e0c0000000000070303010707010303070000000000 +000000c0e030101808080808181030e0c000000000001f7fe080000000000000000080e07f1f00000000000000010103020202020301010000000000 +00007c381010d0d010386c6c000000000000000000000000181cffff1c180000007cfc7c000000000000000000000707020202020202030000000000 +00000000008000c0f0fcfee08080e0000000000000000038feffff1f0f01031f9ffff9fc3e0000000000000000010302000000020303010000000000 +0080c0c0c0c0c04040c0c0c0f0f87840c0800000007f3f9fdf9939606079797f7f3f9cdc9c3d7e7c0000000103010000000000000000010301000000 +0000000000000000c0f0c0000000000000000000000000c0f0f8fe3f0f070f3ffef8e0c0000000000001010101010101010101010101010101010100 +0000000000000040c0400000101090e0800000000070882474248c722127190905738924772488700000000101010000000000000000000101010000 +00000000000000000000008898f0c000000000003249b5b54933061c3c3c3f1f073349b5b54932000000000000000000000000000000000000000000 +00008080c040406020203010101888888880c0807e3e9fdf9f3f9fdf9f3f617f7c7c3f98d898397f0000010301000103010000000000000103010000 +000000008080c0c0c0c0c0c0c080000000000000787c1c4fefed4c1c3c3f3c3c1d4feeec4c1c7c780000000000000000000000000000000000000000 +0000000000000040c0c0800000000000808000000c1830606070703838191f0f0e060703030101000000000000000000000000000000000000000000 +00000000000000000000000080c07038381000000e1f1f1e9cdc6c34180c161b1d1c1c1c1e1f1f0e0002070703000000000000000000000000000000 +000000000000000000e02020e0202020608000003e3e0e66f6660e1e1f1f1c1c1f0e66f6660f1f3e0000000000000000000000000000000000000000 +000080c06030101888c8c88818103060c080000000070f18306040c78f9f9f8fc7406030180f07000000000000000000000000000000000000000000 +e0e0e0e0e0e0e0e0e0e0e0e0e0e00080808000003f1fcfefcf1f3f1fcfefcf1f3f3f13cfe8c81f3c0000000100000000000100000000000001000000 +000000000000007c44c4c4447c000000000000000000e0203c24e4040407070404e4243c20e000000000010101010100000000000001010101010000 +0000f80601f88c0070f0f070008cf80106f8000000000003040001e0b8cfcfb8e0010004030000000000000000020303020202020303020000000000 +0000000000000080c0e0fee6c680000000000000000000f8fc1e1fffff1c1c1cffff1f1efcf800000000000303030303030000000303030303030000 +0000000000f0fc0cf6fafefaf60cfcf000000000000000000003071c61dfbfdf611c0703000000000000000000000000000606060000000000000000 +000000000000240840606c604008240000000000703898d8dc9e3b79797f7f79793b9edcd89838780000000101000000000000000000000101000000 +00000000000000000000c0d0180c0c0e0e00000000103070787c76767f777f777f777c78703000000000010101010101010101010101010101010000 +0080c0e0e0e0e0e0e0e0e0e0e0606060e0c00000003f1fc8e8c8183f3f2020203f3f19c9ebce1c380000000001000000000000000000000001000000 +00c0c0e0e0e0e0e0e0e0e0e0e06060c080000000003f1fcfefcf1f3f3f3f3f3f3f1ccceccd1f3e3c0000000001000000000000000000000100000000 +00e0000000c0000000c0000000c0000000c00000000f01010107010101ff010101070101010701000000000000000000000300000000000000000000 +00e03090d050505254585854525050d09030e00000ff803f6040424e4240424c424040603f80ff000000010103030301010101010103030301010000 +00e03030f03030f03030f03030f0303070f0e000003f0e66f7660e3f3e3e3f3e0e67f6640c3f3f3c0000000000000000000000000000000000000000 +00000080c0c08000000080c8f8e0c080000000003049b5b54933071f1f1f1f1f073349b5b54932000000000000000000000000000000000000000000 +0000c07018080c0486e2e286040c081870c00000000f30e08003677f3f1f1f3f7f670380e0300f000000000001010302060404060203010100000000 +000080e030180884c6e2f2e2c684081830e0800000001f70c0860f1f3f7fff7f3f1f0f86c0701f000000000000010102060404040602010100000000 +0000000000000082c2ca8a2aff2a0a0a02020000000000f8fcfe1f1fffff1f1f1ffcf800000000000000000707070707070704040407070000000000 +000080c0e0e0f0f8fcfcfefcf8f8f0f0e0c0000000000143630707436307074363070743630100000000000000020300000203000002030000000000 +000000000070787c1c1c1cfcf8f00000000000000000000000000000dcdedf0701000000000000000000000000000000010101000000000000000000 +000080e0301888cce4e4e4e4cc881830e080000000000f3860c78f9f3f3f3f3f9f8fc760380f00000000000000000001010101010100000000000000 +000000000080c0c0ac1e1eacc0c0800000000000000000c0e0f9c5030100000103c5f9e0c00000000000000001010000000000000000010100000000 +0000000000000080dc3e3edc8000000000000000000000c0e0f8eec380808080c3eef8e0c00000000000000103030301000000000103030301000000 +000080603028fc2c26fe262cfc2830608000000000000f3262a7faa222ff22a2faa762320f0000000000000000000101030303010100000000000000 +000000000000c06070607f60c080000000000000000000000000ff5455fd5554ff0f0000000000000000000000000103030303030100000000000000 +008000000000001090f0f0901000000000008000030703030303034347ffff4743030303030307030000000000000000000000000000000000000000 +00000000000080c0e0c0fefe8000000000000000000000000000ffa8faaafaa8ffff0000000000000000000000000102030203020301000000000000 +""" + +BITMAPS = bytes.fromhex(_HEX) + +if len(CODES) != 48 or len(BITMAPS) != len(CODES) * BYTES_PER_ICON: + raise ValueError("invalid APRS 20x20 symbol table") diff --git a/App/apps/aprsrx/README.md b/App/apps/aprsrx/README.md index 37a1a3ec..0f41b391 100644 --- a/App/apps/aprsrx/README.md +++ b/App/apps/aprsrx/README.md @@ -11,7 +11,7 @@ Status: | Audio path an app can sample | **Done** by EPIRB 406: RX audio on PA4, ADC channel 4 at 9.6 kHz | | Integer demodulator, modelled on synthetic audio (`test/model_rx.py`) | **Done** (see below) | | Flipper Zero test transmitter (`test/flipper_aprs.py`) | **Done**, files in `test/flipper/` | -| Radio app (`aprsrx_app.c`) | **v0.1 receives the Flipper frames on the radio**; v0.3 decodes Mic-E (on air: F5RAV via F1PRY-14); v0.4 decodes continuously with 3 slicers (on air: F1PRY-14 via F5KTR-3); v0.5 shows standard positions, the frequency bottom right as Beacon, drops `pp` and the UP/DOWN/5 tuning offset to fit 4 KiB (3976 B); main-screen frequency, WAIT capsule (4068 B); v0.6: speaker off, key 1 toggles it, per-slicer counters dropped (not built yet) | +| Radio app (`aprsrx_app.c`) | **v0.1 receives the Flipper frames on the radio**; v0.3 decodes Mic-E (on air: F5RAV via F1PRY-14); v0.4 decodes continuously with 3 slicers (on air: F1PRY-14 via F5KTR-3); v0.5 shows standard positions; v0.6 adds the speaker key; v0.7 adds the corrected 20x20 APRS symbol bitmaps (not built here) | | Bench test with the Flipper on 433.650 MHz | **Done** (2026-09-30, v0.1: `_long` 10/10 in STD; `_badfcs` not shown) | | Real station: FT3D beacon on 144.800 MHz | **Done** (2026-09-30, v0.1: Mic-E frame `>TXUPX9` received, FCS good, = 48°50.89' N 2°16.25' E) | | Mic-E decoding (`test/mice.py`: spec encoder vs the app's decoder) | **Done** in the model: 6 cases + the FT3D frame | @@ -41,7 +41,7 @@ The screen is redrawn after a new frame, a key, or every 5 s. |---|---| | 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 | +| Lines 4-6 | 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), aligned left. A matching 20x20 bitmap from the 48-symbol Yaesu set is shown at x=108 on the right for decoded Mic-E and uncompressed positions; the area stays blank for an unknown or unavailable symbol | | 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) | Keys (UV-K5 and UV-K1): @@ -60,8 +60,8 @@ EPIRB 406); every on-air decode was made in STD and the model shows no gain for RAW, so v0.4 dropped it to fit the 4 KiB overlay. The last good frame is kept on `app_main`'s stack and the demodulator state on -`capture`'s: the 4 KiB overlay also holds `.bss`. The screen texts and the two -cos tables are assets. +`capture`'s: the 4 KiB overlay also holds `.bss`. The screen texts, symbol +bitmaps and the two cos tables are assets. ## Demodulator (`test/model_rx.py`, class `Demod`; the C is a transcription) @@ -154,4 +154,3 @@ the standard position display against real frames; 0.065 LSB/Hz), **measured 734-778** with the Flipper and **916** with a real station (F1PRY-14): ~20 % of full scale, plenty of headroom, and the per-tone AGC makes the absolute level irrelevant. - diff --git a/App/apps/aprsrx/aprsrx_app.c b/App/apps/aprsrx/aprsrx_app.c index e95a644b..9010c307 100644 --- a/App/apps/aprsrx/aprsrx_app.c +++ b/App/apps/aprsrx/aprsrx_app.c @@ -146,6 +146,7 @@ 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 symTable, symCode; /* symbol extracted from the displayed frame */ 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 */ @@ -206,10 +207,10 @@ static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false, /* 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, 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). */ + * digits in the small font on line 5 right after them. 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. As APRS TX, without its + * 1 GHz case (the main screen's small font there): 1296.000 still fits. */ static void drawFreq(uint32_t f){ const app_api_t *A=g.A; uint16_t m=0; @@ -219,10 +220,21 @@ 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" */ - 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" */ + A->print_normal(str+n-2u,(uint8_t)(big*13u+3u),0,5); /* "00" */ str[n-2u]='\0'; - A->display_freq(str,x,4,false); /* "144.800" */ + A->display_freq(str,0,4,false); /* "144.800" */ +} + +#define SYM_X 108u +static void drawSymbol(void){ + const app_api_t *A=g.A; + uint8_t hit[3]; + uint8_t slot=(uint8_t)((2u*g.symTable+13u*g.symCode)&127u); + A->asset_read((uint16_t)(SYM_MAP+slot*3u),hit,sizeof hit); + if(!hit[0] || hit[1]!=g.symTable || hit[2]!=g.symCode) return; + uint16_t off=(uint16_t)(SYM_BITMAPS+(uint16_t)(hit[0]-1u)*SYM_BYTES); + for(uint8_t page=0;page<3u;page++) + A->asset_read((uint16_t)(off+page*SYM_W),A->fb[4u+page]+SYM_X,SYM_W); } /* ---- SysTick cycle counter (call at least every 10 ms; wraps every 89 s, @@ -492,6 +504,7 @@ static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){ 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]); + g.symTable=f[i+8]; g.symCode=f[i+7]; o=brk(o); uint8_t idx=(uint8_t)(micBit(c[0])*4u+micBit(c[1])*2u+micBit(c[2])); bool custom=false; @@ -544,6 +557,7 @@ static bool drawPos(const uint8_t *f,uint16_t i,uint16_t end){ *o++=' '; } *o++=safe(q[8]); *o++=safe(q[18]); *o++=' '; + g.symTable=q[8]; g.symCode=q[18]; textRows(o,f,(uint16_t)(p+19u),end); return true; } @@ -561,6 +575,7 @@ static void draw(void){ A->display_clear(); A->status_clear(); g.vrow=0; + g.symTable=g.symCode=0; A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u)); A->draw_battery(); @@ -613,6 +628,7 @@ static void draw(void){ * 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); + drawSymbol(); /* bottom line: "ok 12 -89dBm": frames, RSSI of the last one */ o=put(str,s+T_OK); o=puti(o,g.nOk); *o++=' '; *o++=' '; diff --git a/App/apps/aprsrx/build.sh b/App/apps/aprsrx/build.sh index 46fc46f7..36ecb1a4 100755 --- a/App/apps/aprsrx/build.sh +++ b/App/apps/aprsrx/build.sh @@ -8,7 +8,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="APRS RX" # <-- the only per-app line -APP_VER="0.6" +APP_VER="0.7" APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) diff --git a/App/apps/aprsrx/gen_assets.py b/App/apps/aprsrx/gen_assets.py index 84c06abc..e0e9eaa2 100755 --- a/App/apps/aprsrx/gen_assets.py +++ b/App/apps/aprsrx/gen_assets.py @@ -15,6 +15,7 @@ sys.dont_write_bytecode = True HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..")) from app_assets import Assets +from aprs_symbols_20 import BITMAPS, BYTES_PER_ICON, CODES, WIDTH FS = 9600 TITLE = "APRS RX" # the version is in the .app header @@ -59,6 +60,17 @@ for i in range(8): 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) +symbol_map = bytearray(128 * 3) +for index, code in enumerate(CODES): + slot = (2 * ord(code[0]) + 13 * ord(code[1])) & 127 + if symbol_map[slot * 3]: + raise ValueError("APRS symbol hash collision") + symbol_map[slot * 3:slot * 3 + 3] = bytes((index + 1, ord(code[0]), ord(code[1]))) +a.raw("SYM_MAP", symbol_map) +a.raw("SYM_BITMAPS", BITMAPS) +a.const("SYM_COUNT", len(CODES)) +a.const("SYM_W", WIDTH) +a.const("SYM_BYTES", BYTES_PER_ICON) a.i8("COS1200", cos_table(1200)) # 8 entries a.i8("COS2200", cos_table(2200)) # 48 entries a.main() diff --git a/App/apps/aprsrx/test/aprs_symbols_20_edited.json b/App/apps/aprsrx/test/aprs_symbols_20_edited.json new file mode 100644 index 00000000..fcf0e990 --- /dev/null +++ b/App/apps/aprsrx/test/aprs_symbols_20_edited.json @@ -0,0 +1,248 @@ +{ + "format": "aprs-symbols-20", + "version": 1, + "width": 20, + "height": 20, + "symbols": [ + { + "code": "/#", + "name": "DIGI", + "pixels": "0000000000000000000000000000000000000000000000000100000000000000000001000000000000000000111000000000000000001110000000000000000111110000000000000001111100000000011111111111111111000011111111111111100000011111111111110000000001111111110000000000001111111000000000000011111110000000000001111111110000000000111110111110000000001111000111100000000111000000011100000001100000000011000000000000000000000000" + }, + { + "code": "/X", + "name": "HELICOPTER", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000001111111111111111100011100000100000111010000000011100000000110000001111100000001110000111111110000001111111111010010000111001111110110110001100001111111111100000000001111111111000000000001111111100000000000001000010000000000000111111101000000000000111111100000000000000000000000000000000000000000000" + }, + { + "code": "/&", + "name": "HF GATEWAY", + "pixels": "0000000000000000000000000000000000000000000011111111111110000000100100100100100000001001001001001000000000010010010000000000000101110100000000000000011100000000000000000111000000000000000001110000000000000000111110000000000000001111100000000000000110001100000000000001100011000000000000011111110000000000001111111110000000000011100011100000000000110000011000000000011111111111000000000000000000000000" + }, + { + "code": "/Y", + "name": "SAILBOAT", + "pixels": "0000000000000000000000000000001000000000000000000011000000000000000000110000000000000000001110000000000000001011100000000000000010111100000000000001101111000000000000011011111000000000001110111110000000000011101111110000000001111011111100000000011110111111100000001111101000000011000000000011111111100011111111111111110000111111111111111000000000000000000000000011111111111111100000000000000000000000" + }, + { + "code": "/'", + "name": "SMALL ACFT", + "pixels": "0000000000000000000000000000000000000000000001100000000000000000001100000000000000000001100000000000000000011100000000000000000011100000000011000000111100000000011000000111100000000111111111111111111001111111111111111111011111111111111111100110000001111000000011000000111100000000000000001110000000000000000111000000000000000001100000000000000000110000000000000000011000000000000000000000000000000000" + }, + { + "code": "/[", + "name": "PERSON", + "pixels": "0000000000000000000000000000000000000000000000000111000000000000000011111000000000000000111110000000000000000111000000000000000000000000000000000000111110000000000000011111110000000000001111111110000000000110011100110000000001100111001100000000011001110011000000000000111110000000000000001101100000000000000011011000000000000000110110000000000000011101110000000000000111011100000000000000000000000000" + }, + { + "code": "/-", + "name": "VHF HOUSE", + "pixels": "0000000000000000000000000000011111110000000000000000100000000000000000111110000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000" + }, + { + "code": "/\\", + "name": "DIRECTION FIND", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111000000110000000000110000000100000000001000000001100000000110000000001000000001000000000011000000110000000000010000001000000000000110000110000000000000100001000000000000001100110000000000000001001000000000000000011110000000000000000011000000000000000000110000000000000000000000000000000000000000000000000" + }, + { + "code": "/.", + "name": "X", + "pixels": "0000000000000000000000000000000000000000000000000000000000000001100000000001100000011100000000111000000111100000011110000001111100001111100000001111100111110000000001111111111000000000001111111100000000000001111110000000000000111111110000000000011111111110000000001111100111110000000111110000111110000001111000000111100000011100000000111000000110000000000110000000000000000000000000000000000000000000" + }, + { + "code": "/^", + "name": "LARGE ACFT", + "pixels": "0000000000000000000000000000000000000000000000000110000000000000000001100000000000000000111100000000000000001001000000000000000111111000000000000111111111100000000111111111111110000111111111111111111011111110111101111111111000001111000001110000000011110000000000000000111100000000000000001111000000000000000111111000000000000111111111100000000001110110111000000000010001100010000000000000000000000000" + }, + { + "code": "/0", + "name": "CIRCLE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000111111000000000000111000011100000000011000000001100000001100000000001100000011000000000011000001100000000000011000011000000000000110000110000000000001100001100000000000011000011000000000000110000011000000000011000000110000000000110000000110000000011000000000111000011100000000000011111100000000000000000000000000000000000000000000000" + }, + { + "code": "/_", + "name": "WEATHER STN", + "pixels": "0000000000000000000000000000000000000000001000000011000000000011000001110000000000111111110000000000001100000111000000000010001100110000000000000011000000000000000000110000000000000000001100000000000000000111100001110000000011111100011100000000111111000111000000000011000001110000000000110000011100000000001100000010000000000011000000100000000000111111111000000000001100000000000000000000000000000000" + }, + { + "code": "/:", + "name": "FIRE", + "pixels": "0000000000000000000000000000001000000000000000000110000000000000000001100000000000000000111000000000000000001111001000000000000111110010000000000101111111100000000001111111111000000000111110111100100000001111100111011000000111111001111110000001111100011111100000011110000001111000000011100000011100000000111000001111000000000110000011100000000000110001110000000000000000000000000000000000000000000000" + }, + { + "code": "/a", + "name": "AMBULANCE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000110000000000000000011100000000000000000111000000011111111111111100001111110011111001100011111100111110001000111100000011100001001111000000111111111011111100111111111110111111001111111111101100011111111000111010010011111100100110001110000000011100000011100000000111000000010000000000100000000000000000000000000000000000000000000" + }, + { + "code": "/;", + "name": "CAMPGROUND", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100000000000000000011100000000000000000111000000000000000011111000000000000001111111000000000000011111110000000000001111011110000000000111100011100000000001111000111100000000111100000111100000001111000001111000001111111111111111110000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/b", + "name": "BICYCLE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011100000000000000000000100000000000111000001000000000000100000111000000000001111111010000000000101000100100000011110010010011100001000100011001000100100100100100100100101011101110001011101010010010000010010010010001000000010001000011100000000011100000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/<", + "name": "MOTORCYCLE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000011000000000000000000010000000000000000000110000000000000000111100000011111000011111111001000011000111100001000110011111110011000010010011111001001001011010111110101101010110100111001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/f", + "name": "FIRE TRUCK", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000111100000000000001111000000000000011110000000000000111100000000001000111000000000111111001111111111001000111111111111010010000111111111110111100001111111111101111111111111111111011111111111000100011111100011100100010011110010010011101110000001110000111011100000011100000100010000000010000000000000000000000000000000000000000000" + }, + { + "code": "/>", + "name": "CAR", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111000000000001111111111000000000111000100110000000001100001000110000001111111111111111110111111111111111111111110000111111000011111001100111100110011110111100000011110110000110000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/g", + "name": "GLIDER", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111000000000000000000111000001100000000000111001111100000000000111111100010000000001111100000110000011111100000000110011111100000000000111111100000000000000111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/C", + "name": "CANOE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000111100000000000000001110000000000000000110000000000000000011000000011000000001100001101111000000110000111111111111011011111111111111101101111111110111110110111111111000000011000000000000000001100000000000000000110000000000000000111000000000000000011110000000000000000011000000000000000000000000000000000000" + }, + { + "code": "/j", + "name": "JEEP", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111110000000000001001000100000000000010010000100000000001100100001101111111111001111111111111111111111111111111000111111110001111100100111111001001111011100000000111001000111000000001110000000100000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/E", + "name": "EYE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000111111000000000000111000011100000000011000000001100000001100001100001100000110000111100001100011000011111100001100110000111111000011001100001111110000110001100001111000011000001100001100001100000001100000000110000000001110000111000000000000111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/k", + "name": "TRUCK", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000001111111111111100000011111111111111011100111111111111111100101111111111111111001011111111111111010011111111111111110111111100011100011110001110010010010011001001001110001110000111000011100011100001110000010000010000001000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/I", + "name": "TCP/IP", + "pixels": "0000000000000000000000000000000000000000000000011111100000000000000100001000000000000001000010000000000000010000100000000000000111111000000000000000011000000000000000000110000000000000000001100000000000001111111111110000000010000000000100000000100000000001000000111110000001111100001000100000010001000010001000000100010000111110000001111100000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/m", + "name": "MIC-E REPEATER", + "pixels": "0000100000000001000000010000000000001000000100100000010010000010011000000110010000100100111100100100001001001111001001000010010011110010010000100110011001100100000100100110010010000001000001100000100000001000011000010000000000001111000000000000000010010000000000000001100110000000000000010110100000000000000111111000000000000011000011000000000001111111111000000000000000000000000000000000000000000000" + }, + { + "code": "/K", + "name": "SCHOOL", + "pixels": "0000000000000000000000000000001110000000000000000011100000000000000000100000000000000000001000000000000000000111000000000000000011111000000000000001111111000000000000111000111000000000011110001111000000001111111111111000000111111111111111000001111111111111110000011001100011001100000110011000110011000001100110001100110000011111100011111100000111111000111111000000000000000000000000000000000000000000" + }, + { + "code": "/O", + "name": "BALLOON", + "pixels": "0000000000000000000000000000111110000000000000111010111000000000001101110110000000000110111110110000000001101111101100000000011011111011000000000110111110110000000001101111101100000000011001110011000000000011011101100000000000010111010000000000000101110100000000000000101010000000000000001101100000000000000001110000000000000000000000000000000000000111000000000000000001110000000000000000000000000000" + }, + { + "code": "/P", + "name": "POLICE", + "pixels": "0000000000000000000000000000000000000000000000100010001000000000000100100100000000000000000000000000000000100111001000000000000011111000000000000000000000000000000000111111110000000000011001100110000000001100011000110000011111111111111111111111111111111111111111000011111111000011100110011111100110010011110000000011110000011000000000011000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/s", + "name": "POWERED SHIP", + "pixels": "0000000000000000000000000000000000011000000000000000011110000000000000001111100000000000000110000000000000000000000000000000000000110000000000000000001100000000000000001111110000000000001111111100000000000111111111100000000011001010101100000111111111111111110000111111111111111100000111111111111110000000000000000000000000111111111111111100000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/R", + "name": "REC VEHICLE", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111110000011111111111111110001111111111110001100011000011000111110000110000110001100110001100001100011000110011111111000111111110110001110001110001101001001111111001001000111000000000111000001110000000001110000001000000000001000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/v", + "name": "VAN", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111000000111111111111111000001111111111110011000011111111111100011000111111111111000011001111111111111111111011111111111111111110110001111111100011101001001111110010011000111000000001110000001110000000011100000001000000000010000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/y", + "name": "YAGI", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000100010001000100010001000100010001000100011111111111111111100100010001000100010001000100010001000100010000000100000000000000000001000000000000000000010000000000000000000100000000000000000001000000000000000000010000000000000000000100000000000000000000000000000000000000000000000000" + }, + { + "code": "/T", + "name": "SSTV", + "pixels": "0000000000000000000000000001000010000000000000001001000000000000000001100000000000111111111111111100011000000000000001100100111111111111001001011000000000011010010100000000000010100101001110101000101001010001000100001010010100010001000010100101000000000000101001011000000000011010010011111111111100100110000000000000011000111111111111111100000011100000011100000000000000000000000000000000000000000000" + }, + { + "code": "/U", + "name": "BUS", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111100011111111111111111100100100100100100111001001001001001000110010010010010010001100111111111111110011001111111111111111111011000111111100011110100100111110010011101011101111101110111000111000000011100000000100000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "/V", + "name": "ATV", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000010000000000000000000110000000000110000011110000000011110001111110000011111111111111111000000011111111100001000110011111110011000010010011111001001001011010111110101101010110100000001011010010010000000001001000011000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "\\#", + "name": "OVERLAY DIGI", + "pixels": "0000000000000000000000000000111100000000000000111001110000000000111000000111000000011000000000011000000100000110000010000011000001100000110000100000111100000100010001111111111000100100011111111110001001000011111111000010010000011111100000100010000111111000010000110011100111001100000100110000110010000001100000000001100000001110000001110000000000111001110000000000000011110000000000000000000000000000" + }, + { + "code": "\\&", + "name": "OVERLAY GATE", + "pixels": "0000000000000000000000000000111110000000000000011000110000000000011000000011000000001100001000011000000110000111000011000001000011111000010000110001111111000110001000111111111000100010011111111111001000100111111111110010001000111111111000100011000111111100011000010000111110000100000110000111000011000000110000100001100000000110000000110000000000011000110000000000000011111000000000000000000000000000" + }, + { + "code": "\\-", + "name": "HF HOUSE", + "pixels": "0000000000001000000000000001111111111100000000000000100000000000000001111111000000000000000010000000000000000001110000000000000011001000000000000001111010000000000000111111100000000000011111111000000000001111111111000000000111111111111000000001111111111110000000011100110001100000000111001100011000000001110011000110000000011111110001100000000111111100011000000001111111111110000000000000000000000000" + }, + { + "code": "/W", + "name": "NWS SITE", + "pixels": "0000000000000000000000000000001000000000000000001111000000000000000111111100000000000011111111110000000011111111111110000001111111111111110000111111111111111100001111111111111111000001111111111111100000000110011001100000000000000000000000000000000000000000000000001000100010001000000110011001100110000000000000000000000000000010001000100000000001100110011000000000000000000000000000000000000000000000" + }, + { + "code": "\\.", + "name": "AMBIGUOUS", + "pixels": "0000000000000000000000000000000000000000000000011111000000000000001111111000000000000111111111000000000001110001110000000000011100011100000000000000000111000000000000000011100000000000000001110000000000000000111100000000000000001110000000000000000011100000000000000000000000000000000000001110000000000000000011100000000000000000111000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "\\0", + "name": "OVERLAY CIRCLE", + "pixels": "0000000000000000000000000000000000000000000000011111100000000000011100001110000000001100000000110000000110001111000110000001000111111000100000110011111111001100001001111111111001000010011111111110010000100111111111100100001100111111110011000001000111111000100000011000111100011000000011000000001100000000011100001110000000000001111110000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "E0", + "name": "ECHOLINK", + "pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000011000000000000000001001000000000000001100001100000000000111100111100000000001111001111000000000000100001000000000000010000001000000000001000000001000000000010000000010000000001100000000110000000111100000011110000001111000000111100000001100000000110000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "I0", + "name": "IRLP", + "pixels": "0000000000000000000000000000011000000000000000001111000000000000000011110000000000000000111100000000000000000110000000000000000010010000000000000001100110000000000000010000100000000000001100001100000000000010000001000000000001100000011000000000010000000010000000001110000001110000000111110000111110000001111111111111100000011111000011111000000011100000011100000000000000000000000000000000000000000000" + }, + { + "code": "W0", + "name": "WIRES", + "pixels": "0000000000000000000000000000111000000000000000111111100000000000011101011100000000001010010010100000000111111111111100000001001001001001000000100010010010001000001001000100010010000011111111111111100000100100010001001000001000100100100010000001001001001001000000011111111111110000000010100100101000000000011101011100000000000011111110000000000000001110000000000000000000000000000000000000000000000000" + }, + { + "code": "YY", + "name": "YAESU RADIO", + "pixels": "0000000000100000000000000000001000000000000000000010000000000000000000100000000000000000101000000000000000011111000000000000001111111000000000000010000011000000000000101110110000000000001000001100000000000011111111000000000000100100110000000000001111111000000000000010010010000000000000111111100000000000001001001000000000000011111110000000000000011111000000000000000000000000000000000000000000000000" + }, + { + "code": "\\^", + "name": "ALT AIRCRAFT", + "pixels": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111110000000000000000110000000000000000001100000000001000000111100000010111111111111111111111111111111111111111101000000111100000010000000000110000000000000000001100000000000000000011000000000000000011111100000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code": "\\Y", + "name": "RADIOS/DEVICES", + "pixels": "0000000000000000000000000000001100000000000000000011000000000000000000110000000000000000001100000000000000001011000000000000000111110000000000000011111110000000000000100000110000000000001011101100000000000010000011000000000000111111110000000000001010101100000000000011111111000000000000101010110000000000001111111100000000000010101011000000000000011111100000000000000000000000000000000000000000000000" + } + ] +} diff --git a/App/apps/aprsrx/test/aprs_symbols_20_editor.html b/App/apps/aprsrx/test/aprs_symbols_20_editor.html new file mode 100644 index 00000000..c18b572a --- /dev/null +++ b/App/apps/aprsrx/test/aprs_symbols_20_editor.html @@ -0,0 +1,431 @@ +
+ + +
+
+

APRS 20×20

+ 48 pictogrammes · validation pixel par pixel +
+ +
+
+
Planche source
+ +
+
+
Matrice CSS éditable20 × 20
+
+
+
+
Taille écran
+ +
+
+
+ +
+ + + + + + + Sauvegarde automatique active +
+ +
+ +
+ Afficher la planche complète + Planche source des 48 pictogrammes APRS +
+
+ + +
diff --git a/App/apps/aprstx/README.md b/App/apps/aprstx/README.md index e57d5e44..28cf2fad 100644 --- a/App/apps/aprstx/README.md +++ b/App/apps/aprstx/README.md @@ -7,7 +7,7 @@ which is also the test receiver. | Step | State | |---|---| | Frame and bit stream model (`test/tx_model.py`) | **Done**: identical to the RX app's AX.25 reference, decoded by the RX model | -| Radio app (`aprstx_app.c`) | **v0.1 works on the radio** (2026-09-30); v0.2 edits the position on the radio, frequency bottom right as Beacon (builds: 2812 B; radio test pending); v0.3 adds the SSID and path to the editor, the edited field in bold (not built yet) | +| Radio app (`aprstx_app.c`) | **v0.1 works on the radio** (2026-09-30); v0.2 edits the position; v0.3 adds the SSID and path; v0.4 adds the corrected 20x20 bitmap and the 48-symbol selector (not built here) | | On-air test: APRS RX on a second radio, FT3D | **Done** (2026-09-30, v0.1: frames received by a UV-K1 running APRS RX and by the FT3D, first try, default level 66 and twist 0) | ## Station settings (`gen_assets.py`, then rebuild) @@ -18,7 +18,7 @@ which is also the test receiver. | `DEST` | `APZK5` (APZ = experimental software) | | `PATH` | `1` = `WIDE1-1`, an index in `PATHS`: `DIRECT` (no digipeater), `WIDE1-1`, `WIDE1-1,WIDE2-1`; the **default**, until one is chosen on the radio | | `LAT`, `LON` | `4850.90N`, `00216.25E`: the **default** position, until one is edited on the radio (key 5) | -| `SYMBOL` | `/[` (person) | +| `SYMBOL` | `/[` (person): the default, until one of the 48 symbols is chosen on the radio | | `COMMENT` | `UV-K5 & UV-K1 APRS TX` (43 characters at most) | Frame sent: `F4HWN-7>APZK5,WIDE1-1:!4850.90N/00216.25E[UV-K5 & UV-K1 APRS TX` (66 bytes, @@ -27,6 +27,10 @@ If the boot message is not a plain callsign (empty, more than 6 characters once spaces are dropped, or with a `/`), the app shows `No boot callsign` and does not transmit. +In the normal view, the TX frequency is aligned left below the separator and +the selected APRS symbol is shown as a 20x20 bitmap at x=108. The 48-symbol +Yaesu bitmap set is stored in the app assets; the editor keeps the full width. + ## Keys (UV-K5 and UV-K1) | Key | Action | @@ -37,8 +41,8 @@ transmit. | 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, path and view are saved) | +| 5 | Edit the position, SSID, path and symbol | +| EXIT | Quit (level, twist, position, SSID, path, symbol and view are saved) | ### Editor (v0.3) @@ -47,24 +51,27 @@ LAT 48 50.90 N LON 002 16.25 E SSID 7 PATH WIDE1-1 +SYMB /[ -0-9 digit * change F+* back +0-9, * change, F+* back UP/DN move MENU ok EXIT ``` The field under the cursor is drawn in bold: a digit or the N/S, E/W letter of -the position, or the whole SSID or path value. Type the 13 digits in a row, as a +the position, or the whole SSID, path or symbol value. Type the 13 digits in a row, as a frequency: `485090` then `0021625` (degrees, minutes, hundredths of a minute: the APRS format, read as is from a GPS or aprs.fi in degrees-minutes). The cursor skips the separators and N/S, and goes on from the latitude to the longitude; -UP/DOWN also stop on N/S and E/W, then take it on to SSID and PATH. +UP/DOWN also stop on N/S and E/W, then take it on to SSID, PATH and SYMB. +When SYMB is selected, both shortened help rows remain visible and the edited +20x20 symbol is previewed at x=108, the same position as on the normal screen. | Key | Action | |---|---| | 0-9 | Digit at the cursor, then the next one (on a position digit) | -| * | N/S (latitude line), E/W (longitude line), next SSID 0-15 (SSID), next path (PATH) | -| F then * | Previous SSID or path (the `F` icon in the status bar while F is armed, as in FoxHunt; F again disarms it) | -| UP / DOWN | Move the cursor: 6 digits, N/S, 7 digits, E/W, SSID, PATH | +| * | N/S (latitude line), E/W (longitude line), next SSID 0-15 (SSID), next path (PATH), or next symbol (SYMB) | +| F then * | Previous SSID, path or symbol (the `F` icon in the status bar while F is armed, as in FoxHunt; F again disarms it) | +| UP / DOWN | Move the cursor: 6 digits, N/S, 7 digits, E/W, SSID, PATH, SYMB | | MENU | Check (degrees ≤ 90 / 180, minutes < 60) and keep: the frame is rebuilt; `Invalid position` otherwise | | EXIT | Cancel | @@ -72,9 +79,10 @@ 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`, 12 bytes: magic 0xA8, level, twist, 13 +Everything is saved on exit (`cfg_save`, 13 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) +14 compact, anything else scroll, then the symbol index). An older config with +byte 12 erased uses the default person symbol. 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. @@ -109,8 +117,8 @@ SPI writes, a few tens of µs out of 833. `tx_mute` + `tx_end` restore RX. `test/tx_model.py` (needs `../aprsrx/test`): frame from the real assets vs `ax25.build`, bit stream vs `ax25.hdlc_bits`, bad boot callsigns refused, an edited position (south/west) vs `ax25.build`, every path with SSID 0, 15 and 9 -vs `ax25.build`, the config round trip (and a v0.2 config falling back to the -default SSID and path), the editor rows and bold column, the editor keys (13 +and a selected symbol vs `ax25.build`, the config round trip (and a v0.2 config +falling back to the default SSID, path and symbol), the editor rows and bold column, the editor keys (13 digits typed in a row, cursor stops, `*` and F then `*` on each field; the UP/DOWN keys through `nav_dir()`, with SET_NAV on and off), the position limits, then diff --git a/App/apps/aprstx/aprstx_app.c b/App/apps/aprstx/aprstx_app.c index b0b22afd..352f892b 100644 --- a/App/apps/aprstx/aprstx_app.c +++ b/App/apps/aprstx/aprstx_app.c @@ -20,8 +20,8 @@ * * The source is the boot-message callsign (API boot_callsign) with an SSID; * destination, the WIDE path entries, symbol and comment are AX.25-encoded in - * gen_assets.py (assets). The position, the SSID and the path (DIRECT, WIDE1-1 - * or WIDE1-1,WIDE2-1) are edited on the radio (key 5) and saved; gen_assets.py + * 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. * @@ -36,14 +36,14 @@ * 1 px per loop · 1 / F then 1 tone level up / down (deviation, REG_70 gain * 10-127) · 2 / F then 2 twist up / down: 2200 Hz gain = level x (8 + tw) / 8, * tw -4..+8 (-6..+6 dB), for a receiver's de-emphasis · 5 edit the position, - * SSID and path · * scroll view / compact view · EXIT quit. F as FoxHunt's: the next key goes down, an icon + * 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 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) · + * 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 and view are saved on exit (cfg_save, 12 + * Level, twist, position, SSID, path, symbol and view are saved on exit (cfg_save, 13 * bytes). */ @@ -89,16 +89,17 @@ 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 */ + * 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 12u /* magic, level, twist, 13 digits + hemispheres in nibbles, +#define CFG_LEN 13u /* 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) */ + 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) */ @@ -116,6 +117,7 @@ static struct { 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 */ @@ -197,8 +199,9 @@ static char *brk(char *o){ /* 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. */ + * 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; @@ -208,11 +211,19 @@ 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" */ + 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,x,4,false); /* "144.800" */ + 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){ @@ -258,7 +269,7 @@ static void build(void){ if(g.path){ A->asset_read(F_WIDE,f+14,(uint16_t)(k-14u)); f[k-1]|=1u; } f[k++]=0x03; f[k++]=0xF0; /* UI frame, no layer 3 */ uint8_t sym[2]; - A->asset_read(F_SYM,sym,2); + 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 */ @@ -332,13 +343,12 @@ static void editRow(const char *label,uint8_t line,const uint8_t *d,uint8_t n,ch if((uint8_t)cur<=n){ str[0]=str[col]; str[1]='\0'; g.A->print_bold(str,(uint8_t)(col*7u),0,line); } } -/* "SSID 7" / "PATH WIDE1-1" on a normal-font line, the value in bold when the - * cursor is on it */ -static void fieldRow(const char *label,const char *val,uint8_t line,bool sel){ +/* 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,0,0,line); - if(sel) g.A->print_bold(val,FIELD_COL*7u,0,line); + g.A->print_normal(str,x,0,line); + if(sel) g.A->print_bold(val,(uint8_t)(x+FIELD_COL*7u),0,line); } /* ---- display ---- */ @@ -365,10 +375,19 @@ static void draw(void){ (int8_t)(c-(int8_t)(CUR_NS+1u))); char v[3]; *puti(v,g.essid)='\0'; - fieldRow(s+T_SSID,v,2,g.cur==CUR_SSID); - fieldRow(s+T_PATH,s+T_PATHS+g.epath*T_PATHS_STRIDE,3,g.cur==CUR_PATH); - tiny(40,put(str,s+T_HELP1)); - tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2)); + 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){ @@ -427,6 +446,7 @@ static void draw(void){ * 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) @@ -488,7 +508,7 @@ static void handleKeys(void){ 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 */ + 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 */ @@ -503,13 +523,16 @@ static void handleKeys(void){ else if(key==APP_KEY_STAR){ if(cur<=CUR_EW) g.ehemi^=(uint8_t)(lon?2u:1u); /* on a digit or the letter */ else if(cur==CUR_SSID) g.essid=(uint8_t)((g.essid+(back?15u:1u))&15u); - else if(back) g.epath=(uint8_t)(g.epath?g.epath-1u:PATH_MAX); - else g.epath=(uint8_t)(g.epathnav_dir(key); if(d<0 && g.cur) g.cur--; - if(d>0 && g.cur0 && g.curasset_read(POS_DEF,g.pos,POS_DIGITS); api->asset_read(POS_DEF+POS_DIGITS,&g.hemi,1); api->cfg_load(c,CFG_LEN); @@ -565,6 +589,7 @@ void app_main(const app_api_t *api){ 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]cfg_save(c,CFG_LEN); } diff --git a/App/apps/aprstx/build.sh b/App/apps/aprstx/build.sh index 4fe2a58a..66b02fe2 100755 --- a/App/apps/aprstx/build.sh +++ b/App/apps/aprstx/build.sh @@ -8,7 +8,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="APRS TX" # <-- the only per-app line -APP_VER="0.3" +APP_VER="0.4" APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) diff --git a/App/apps/aprstx/gen_assets.py b/App/apps/aprstx/gen_assets.py index 1c8710a7..1389a538 100755 --- a/App/apps/aprstx/gen_assets.py +++ b/App/apps/aprstx/gen_assets.py @@ -17,6 +17,7 @@ sys.dont_write_bytecode = True HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..")) from app_assets import Assets +from aprs_symbols_20 import BITMAPS, BYTES_PER_ICON, CODES, WIDTH # ---- station settings ---- SSID = 7 # F4HWN-7 (0-15, 0 = no SSID) @@ -44,7 +45,8 @@ UI = [ ("T_LON", "LON "), ("T_SSID", "SSID "), # FIELD_COL (5) characters: the value ("T_PATH", "PATH "), # starts at the same column - ("T_HELP1", "0-9 digit * change F+* back"), # * on the bold field + ("T_SYM", "SYMB "), # selected APRS table/code pair + ("T_HELP1", "0-9, * change, F+* back"), # * on the bold field ("T_HELP2", "UP/DN move MENU ok EXIT"), ("T_BADPOS", "Invalid position"), ] @@ -89,8 +91,13 @@ a.const("CFG_PATH", PATH) a.raw("F_DEST", addr(DEST)) a.raw("F_WIDE", b"".join(addr(h) for h in WIDE)) a.u8("POS_DEF", POS + [HEMI]) -a.raw("F_SYM", SYMBOL.encode("ascii")) a.raw("F_COMMENT", COMMENT.encode("ascii")) +a.raw("SYM_CODES", "".join(CODES).encode("ascii")) +a.raw("SYM_BITMAPS", BITMAPS) +a.const("SYM_INDEX", CODES.index(SYMBOL)) +a.const("SYM_COUNT", len(CODES)) +a.const("SYM_W", WIDTH) +a.const("SYM_BYTES", BYTES_PER_ICON) 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) diff --git a/App/apps/aprstx/test/tx_model.py b/App/apps/aprstx/test/tx_model.py index 58c8d126..afe6c8bb 100644 --- a/App/apps/aprstx/test/tx_model.py +++ b/App/apps/aprstx/test/tx_model.py @@ -51,25 +51,26 @@ def pos_ok(d): 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 +def cfg_pack(lvl, tw, pos, hemi, ssid, path, cw=0, sym=5): + c = [0xA8, lvl, tw & 0xFF] + [0] * 10 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 + c[12] = sym return bytes(c) def cfg_unpack(c, D): """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)""" + defaults when byte 10 is erased or from a v0.2 config), view (scroll unless + byte 11 is CW_COMPACT) and symbol (default when byte 12 is erased)""" 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, cw - return pos, c[9] >> 4, D["CFG_SSID"], D["CFG_PATH"], cw + return pos, c[9] >> 4, h & 15, (h >> 4) & 3, cw, c[12] if c[12] < D["SYM_COUNT"] else D["SYM_INDEX"] + return pos, c[9] >> 4, D["CFG_SSID"], D["CFG_PATH"], cw, c[12] if c[12] < D["SYM_COUNT"] else D["SYM_INDEX"] def edit_row(label, d, h, cur): @@ -86,7 +87,7 @@ def edit_row(label, d, h, cur): return o + h, col -CUR_NS, CUR_EW, CUR_SSID, CUR_PATH = 6, 14, 15, 16 +CUR_NS, CUR_EW, CUR_SSID, CUR_PATH, CUR_SYM = 6, 14, 15, 16, 17 def nav_dir(key, set_nav): @@ -96,7 +97,7 @@ def nav_dir(key, set_nav): return d if set_nav else -d -def editor_keys(keys, ed, hemi=0, ssid=7, path=1, cur=0, set_nav=True): +def editor_keys(keys, ed, hemi=0, ssid=7, path=1, sym=5, cur=0, set_nav=True): """aprstx_app.c handleKeys() in the editor: digits 0-9, '*', raw UP 'U' / DOWN 'D' through nav_dir(), 'F'""" ed = list(ed) @@ -116,16 +117,18 @@ def editor_keys(keys, ed, hemi=0, ssid=7, path=1, cur=0, set_nav=True): elif k == "*": if cur <= CUR_EW: hemi ^= 2 if lon else 1 elif cur == CUR_SSID: ssid = (ssid + (15 if back else 1)) & 15 - elif back: path = path - 1 if path else 2 - else: path = path + 1 if path < 2 else 0 + elif cur == CUR_PATH: + path = path - 1 if back and path else 2 if back else path + 1 if path < 2 else 0 + elif back: sym = sym - 1 if sym else 47 + else: sym = sym + 1 if sym < 47 else 0 else: d = nav_dir(k, set_nav) if d < 0 and cur: cur -= 1 - if d > 0 and cur < CUR_PATH: cur += 1 - return ed, hemi, ssid, path, cur + if d > 0 and cur < CUR_SYM: cur += 1 + return ed, hemi, ssid, path, sym, cur -def build_c(blob, D, call=CALL, pos=None, hemi=None, ssid=None, path=None): +def build_c(blob, D, call=CALL, pos=None, hemi=None, ssid=None, path=None, sym=None): """aprstx_app.c build()""" if not call or len(call) > 6 or "/" in call: return None @@ -135,9 +138,10 @@ def build_c(blob, D, call=CALL, pos=None, hemi=None, ssid=None, path=None): hemi = pd[13] if hemi is None else hemi ssid = D["CFG_SSID"] if ssid is None else ssid path = D["CFG_PATH"] if path is None else path - sym = rd("F_SYM").decode() - info = "!" + put_coord(pos[:6], "S" if hemi & 1 else "N") + sym[0] + \ - put_coord(pos[6:], "W" if hemi & 2 else "E") + sym[1] + rd("F_COMMENT").decode() + sym = D["SYM_INDEX"] if sym is None else sym + pair = rd("SYM_CODES")[sym * 2:sym * 2 + 2].decode() + info = "!" + put_coord(pos[:6], "S" if hemi & 1 else "N") + pair[0] + \ + put_coord(pos[6:], "W" if hemi & 2 else "E") + pair[1] + rd("F_COMMENT").decode() f = bytearray(rd("F_DEST")) f += bytes(ord(call[i]) << 1 if i < len(call) else 0x40 for i in range(6)) f.append(0x60 | (ssid << 1) | (0 if path else 1)) @@ -229,10 +233,16 @@ 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)) + f3 = build_c(blob, D, sym=0) + want = build("F4HWN-7", dst="APZK5", path=["WIDE1-1"], + info="!4850.90N/00216.25E#UV-K5 & UV-K1 APRS TX") + ok &= f3 == want + print("selected /# symbol frame:", f3 == want, decode(f3)) 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) + good = len(c) == 13 and cfg_unpack(c, D) == (pos, 3, 9, 2, CW_COMPACT, 5) 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, 0xFF]), D) == (pos, 3, D["CFG_SSID"], D["CFG_PATH"], 0) + good &= all(cfg_unpack(old + bytes([b, 0xFF, 0xFF]), D) == + (pos, 3, D["CFG_SSID"], D["CFG_PATH"], 0, D["SYM_INDEX"]) for b in (0xFF, 0x00)) # byte 11 erased: scroll view ok &= good print("config round trip, v0.2 config -> default SSID and path, scroll view:", good, c.hex()) @@ -250,21 +260,23 @@ def main(): # the cursor. Run for both SET_NAV settings: the raw key moving the cursor # forward (nav_dir +1) is UP with SET_NAV, DOWN without. typed = editor_keys("3352131511256", [0] * 13) - good &= typed == (pos, 0, 7, 1, 13) + good &= typed == (pos, 0, 7, 1, 5, 13) good &= editor_keys("2*", pos)[:2] == ([2] + pos[1:], 1) # '*' on a LAT digit for sn in (True, False): N, P = ("U", "D") if sn else ("D", "U") # next / previous field k = lambda keys, **kw: editor_keys(keys, pos, set_nav=sn, **kw) - good &= k(N)[4] == 1 and k(P)[4] == 0 and k(N + P)[4] == 0 # the raw keys' direction - good &= k(P * 13 + N * 6 + "*")[1:] == (0 ^ 1, 7, 1, CUR_NS) + good &= k(N)[5] == 1 and k(P)[5] == 0 and k(N + P)[5] == 0 # the raw keys' direction + good &= k(P * 13 + N * 6 + "*")[1:] == (0 ^ 1, 7, 1, 5, CUR_NS) good &= k(N * 14 + "*" + "5")[:2] == (pos, 2) # E/W: '*', a digit ignored - good &= k(N * 15 + "**")[2:] == (9, 1, CUR_SSID) - good &= k(N * 16 + "**" + N)[3:] == (0, CUR_PATH) # stops on the last field + good &= k(N * 15 + "**")[2:] == (9, 1, 5, CUR_SSID) + good &= k(N * 16 + "**" + N)[3:] == (0, 5, CUR_SYM) good &= k(N * 15 + "F*F*")[2] == 5 # F then '*': SSID back good &= k(N * 15 + "F*" * 8, ssid=3)[2] == 11 # ... wrapping 0 -> 15 good &= k(N * 16 + "F*F*F*")[3] == 1 # path back, 1 -> 0 -> 2 -> 1 good &= k(N * 15 + "FF*")[2] == 8 # F twice: disarmed - good &= k(N * 15 + "F" + N + "*")[3:] == (2, CUR_PATH) # F used by a move, '*' forward + good &= k(N * 15 + "F" + N + "*")[3:] == (2, 5, CUR_PATH) # F used by a move, '*' forward + good &= k(N * 17 + "**")[4:] == (7, CUR_SYM) # symbol forward + good &= k(N * 17 + "F*")[4:] == (4, CUR_SYM) # symbol backward ok &= good print("editor keys (SET_NAV on and off):", good) checks = [([4,8,5,0,9,0,0,0,2,1,6,2,5], True), ([9,0,0,0,0,0,1,8,0,0,0,0,0], True),