mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
APRS RX/TX: add validated 20x20 symbols and TX selector
This commit is contained in:
1 parent
f940a6b59e
commit
c06246b798
12 files changed
+919
-82
No files matched your search
@@ -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")
|
||||
@@ -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.
|
||||
|
||||
@@ -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++=' ';
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<div id="aprs-pixel-workbench" class="aprs-workbench viz-dotted-background">
|
||||
<style>
|
||||
#aprs-pixel-workbench {
|
||||
--p-bg: light-dark(#f7fbfd, #101820);
|
||||
--p-panel: light-dark(#ffffff, #17232d);
|
||||
--p-ink: light-dark(#102536, #eef8ff);
|
||||
--p-muted: light-dark(#577184, #a8bdca);
|
||||
--p-line: light-dark(#b8d8e8, #355163);
|
||||
--p-grid: light-dark(#d8eef7, #29414f);
|
||||
--p-pixel: light-dark(#06141f, #f6fbff);
|
||||
--p-cyan: light-dark(#007b9d, #43dff8);
|
||||
color-scheme: light dark;
|
||||
color: var(--p-ink);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
padding: 16px;
|
||||
}
|
||||
#aprs-pixel-workbench * { box-sizing: border-box; }
|
||||
#aprs-pixel-workbench .shell {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
background: var(--p-bg);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#aprs-pixel-workbench header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: light-dark(#07385f, #082b48);
|
||||
color: #eefcff;
|
||||
}
|
||||
#aprs-pixel-workbench h1,
|
||||
#aprs-pixel-workbench h2 { margin: 0; font-weight: 500; }
|
||||
#aprs-pixel-workbench header span { color: #43dff8; }
|
||||
#aprs-pixel-workbench .stage {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 1fr) minmax(260px, 1fr) 96px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .pane-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--p-muted);
|
||||
}
|
||||
#aprs-pixel-workbench .source-window {
|
||||
width: 100%;
|
||||
aspect-ratio: 156 / 136;
|
||||
background-image: url("./aprs_symbols_20_mockup.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 897.4359% 823.5294%;
|
||||
border: 1px solid var(--p-line);
|
||||
background-color: #fff;
|
||||
}
|
||||
#aprs-pixel-workbench .matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 1fr);
|
||||
aspect-ratio: 1;
|
||||
width: min(100%, 320px);
|
||||
background: var(--p-panel);
|
||||
border: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .matrix button {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--p-grid);
|
||||
border-bottom: 1px solid var(--p-grid);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
#aprs-pixel-workbench .matrix button.on { background: var(--p-pixel); }
|
||||
#aprs-pixel-workbench .native-wrap {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#aprs-pixel-workbench .native {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 1px);
|
||||
grid-template-rows: repeat(20, 1px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: light-dark(#fff, #eaf3f8);
|
||||
outline: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .native i,
|
||||
#aprs-pixel-workbench .mini i { background: transparent; }
|
||||
#aprs-pixel-workbench .native i.on,
|
||||
#aprs-pixel-workbench .mini i.on { background: #07141d; }
|
||||
#aprs-pixel-workbench .selected-code {
|
||||
font-size: 22px;
|
||||
color: var(--p-cyan);
|
||||
text-align: center;
|
||||
}
|
||||
#aprs-pixel-workbench .tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 16px 16px;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
#aprs-pixel-workbench .tools button,
|
||||
#aprs-pixel-workbench .tools label,
|
||||
#aprs-pixel-workbench .symbol {
|
||||
color: var(--p-ink);
|
||||
background: var(--p-panel);
|
||||
border: 1px solid var(--p-line);
|
||||
font: inherit;
|
||||
}
|
||||
#aprs-pixel-workbench .tools button,
|
||||
#aprs-pixel-workbench .tools label {
|
||||
padding: 7px 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
#aprs-pixel-workbench .tools input { display: none; }
|
||||
#aprs-pixel-workbench .save-status {
|
||||
align-self: center;
|
||||
color: var(--p-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
#aprs-pixel-workbench .symbols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(72px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol {
|
||||
min-width: 0;
|
||||
padding: 7px 4px 6px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol[aria-pressed="true"] {
|
||||
border-color: var(--p-cyan);
|
||||
box-shadow: inset 0 0 0 1px var(--p-cyan);
|
||||
}
|
||||
#aprs-pixel-workbench .mini {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 2px);
|
||||
grid-template-rows: repeat(20, 2px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f7fbfd;
|
||||
}
|
||||
#aprs-pixel-workbench .symbol small {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--p-muted);
|
||||
}
|
||||
#aprs-pixel-workbench details { margin: 0 16px 16px; }
|
||||
#aprs-pixel-workbench summary { color: var(--p-cyan); }
|
||||
#aprs-pixel-workbench details img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--p-line);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
#aprs-pixel-workbench .stage { grid-template-columns: 1fr 1fr; }
|
||||
#aprs-pixel-workbench .native-wrap { grid-column: 1 / -1; }
|
||||
#aprs-pixel-workbench .symbols { grid-template-columns: repeat(4, minmax(64px, 1fr)); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#aprs-pixel-workbench .stage { grid-template-columns: 1fr; }
|
||||
#aprs-pixel-workbench .symbols { grid-template-columns: repeat(3, minmax(64px, 1fr)); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="shell" aria-label="Atelier de validation APRS 20 par 20">
|
||||
<header>
|
||||
<h1>APRS 20×20</h1>
|
||||
<span>48 pictogrammes · validation pixel par pixel</span>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section>
|
||||
<div class="pane-title"><span>Planche source</span><strong id="source-name"></strong></div>
|
||||
<div id="source-window" class="source-window" role="img"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="pane-title"><span>Matrice CSS éditable</span><strong>20 × 20</strong></div>
|
||||
<div id="matrix" class="matrix" aria-label="Matrice de pixels éditable"></div>
|
||||
</section>
|
||||
<section class="native-wrap">
|
||||
<div class="pane-title">Taille écran</div>
|
||||
<div id="native" class="native" role="img" aria-label="Aperçu à taille réelle"></div>
|
||||
<div id="selected-code" class="selected-code"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<button id="undo-one" type="button">Rétablir ce picto</button>
|
||||
<button id="undo-all" type="button">Rétablir les 48</button>
|
||||
<button id="invert" type="button">Inverser les pixels</button>
|
||||
<button id="export-json" type="button">Exporter JSON</button>
|
||||
<label for="import-json">Importer JSON</label>
|
||||
<input id="import-json" type="file" accept="application/json,.json">
|
||||
<span id="save-status" class="save-status" aria-live="polite">Sauvegarde automatique active</span>
|
||||
</div>
|
||||
|
||||
<div id="symbols" class="symbols" aria-label="Sélection des 48 pictogrammes"></div>
|
||||
|
||||
<details>
|
||||
<summary>Afficher la planche complète</summary>
|
||||
<img src="./aprs_symbols_20_mockup.png" alt="Planche source des 48 pictogrammes APRS">
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.getElementById('aprs-pixel-workbench');
|
||||
const codes = ['/\x23','/X','/&','/Y',"/'",'/[','/-','/\\','/.','/^','/0','/_','/:','/a','/;','/b','/<','/f','/>','/g','/C','/j','/E','/k','/I','/m','/K','/O','/P','/s','/R','/v','/y','/T','/U','/V','\\\x23','\\&','\\-','/W','\\.','\\0','E0','I0','W0','YY','\\^','\\Y'];
|
||||
const names = ['DIGI','HELICOPTER','HF GATEWAY','SAILBOAT','SMALL ACFT','PERSON','VHF HOUSE','DIRECTION FIND','X','LARGE ACFT','CIRCLE','WEATHER STN','FIRE','AMBULANCE','CAMPGROUND','BICYCLE','MOTORCYCLE','FIRE TRUCK','CAR','GLIDER','CANOE','JEEP','EYE','TRUCK','TCP/IP','MIC-E REPEATER','SCHOOL','BALLOON','POLICE','POWERED SHIP','REC VEHICLE','VAN','YAGI','SSTV','BUS','ATV','OVERLAY DIGI','OVERLAY GATE','HF HOUSE','NWS SITE','AMBIGUOUS','OVERLAY CIRCLE','ECHOLINK','IRLP','WIRES','YAESU RADIO','ALT AIRCRAFT','RADIOS/DEVICES'];
|
||||
const sourceX = [17,189,362,535,708,881,1054,1227];
|
||||
const sourceY = [87,256,427,598,768,939];
|
||||
const encoded = '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';
|
||||
const raw = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
|
||||
const original = new Uint8Array(48 * 400);
|
||||
|
||||
for (let n = 0; n < 48; n++) {
|
||||
for (let y = 0; y < 20; y++) {
|
||||
for (let x = 0; x < 20; x++) {
|
||||
original[n * 400 + y * 20 + x] = (raw[n * 60 + Math.floor(y / 8) * 20 + x] >> (y & 7)) & 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function packPixels(values) {
|
||||
const bytes = new Uint8Array(values.length / 8);
|
||||
for (let i = 0; i < values.length; i++) if (values[i]) bytes[i >> 3] |= 1 << (i & 7);
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function unpackPixels(encoded) {
|
||||
const bytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
|
||||
if (bytes.length !== 2400) throw new Error('Sauvegarde locale invalide');
|
||||
const values = new Uint8Array(48 * 400);
|
||||
for (let i = 0; i < values.length; i++) values[i] = (bytes[i >> 3] >> (i & 7)) & 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
const storageKey = 'aprs-symbols-20-editor-v1';
|
||||
let localSaved = null;
|
||||
try { localSaved = JSON.parse(localStorage.getItem(storageKey)); } catch (_) {}
|
||||
const saved = window.openai?.widgetState?.privateContent || localSaved;
|
||||
let pixels = new Uint8Array(original);
|
||||
let selected = 5;
|
||||
if (saved) {
|
||||
try {
|
||||
if (typeof saved.packed === 'string') pixels = unpackPixels(saved.packed);
|
||||
else if (saved.pixels && saved.pixels.length === 19200)
|
||||
pixels = Uint8Array.from(saved.pixels, c => c === '1' ? 1 : 0);
|
||||
selected = Number.isInteger(saved.selected) ? Math.max(0, Math.min(47, saved.selected)) : selected;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const matrix = root.querySelector('#matrix');
|
||||
const native = root.querySelector('#native');
|
||||
const symbols = root.querySelector('#symbols');
|
||||
const source = root.querySelector('#source-window');
|
||||
const sourceName = root.querySelector('#source-name');
|
||||
const selectedCode = root.querySelector('#selected-code');
|
||||
const saveStatus = root.querySelector('#save-status');
|
||||
|
||||
function cells(container, index, editable) {
|
||||
container.replaceChildren();
|
||||
for (let p = 0; p < 400; p++) {
|
||||
const cell = document.createElement(editable ? 'button' : 'i');
|
||||
if (editable) {
|
||||
cell.type = 'button';
|
||||
cell.setAttribute('aria-label', `Pixel ${p % 20 + 1}, ${Math.floor(p / 20) + 1}`);
|
||||
cell.addEventListener('click', () => {
|
||||
pixels[index * 400 + p] ^= 1;
|
||||
renderSelected();
|
||||
renderMini(index);
|
||||
remember();
|
||||
});
|
||||
}
|
||||
cell.classList.toggle('on', !!pixels[index * 400 + p]);
|
||||
container.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMini(index) {
|
||||
const mini = symbols.children[index]?.querySelector('.mini');
|
||||
if (mini) cells(mini, index, false);
|
||||
}
|
||||
|
||||
function renderSelected() {
|
||||
cells(matrix, selected, true);
|
||||
cells(native, selected, false);
|
||||
selectedCode.textContent = codes[selected];
|
||||
sourceName.textContent = names[selected];
|
||||
const col = selected % 8;
|
||||
const row = Math.floor(selected / 8);
|
||||
const scale = source.clientWidth / 156;
|
||||
source.style.backgroundSize = `${1400 * scale}px ${1120 * scale}px`;
|
||||
source.style.backgroundPosition = `${-sourceX[col] * scale}px ${-sourceY[row] * scale}px`;
|
||||
source.setAttribute('aria-label', `${codes[selected]} ${names[selected]}, planche source`);
|
||||
[...symbols.children].forEach((button, i) => button.setAttribute('aria-pressed', String(i === selected)));
|
||||
}
|
||||
|
||||
function remember() {
|
||||
const packed = packPixels(pixels);
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify({ selected, packed }));
|
||||
saveStatus.textContent = 'Sauvegardé automatiquement';
|
||||
} catch (_) {
|
||||
saveStatus.textContent = 'Sauvegarde locale indisponible — exportez le JSON';
|
||||
}
|
||||
if (window.openai?.setWidgetState) {
|
||||
window.openai.setWidgetState({
|
||||
modelContent: { selectedCode: codes[selected], edited: !pixels.every((v, i) => v === original[i]) },
|
||||
privateContent: { selected, packed }
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function exportDocument() {
|
||||
return {
|
||||
format: 'aprs-symbols-20',
|
||||
version: 1,
|
||||
width: 20,
|
||||
height: 20,
|
||||
symbols: codes.map((code, index) => ({
|
||||
code,
|
||||
name: names[index],
|
||||
pixels: Array.from(pixels.subarray(index * 400, index * 400 + 400), p => p ? '1' : '0').join('')
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function pixelsFromDocument(document) {
|
||||
if (!document || document.format !== 'aprs-symbols-20' || document.version !== 1 ||
|
||||
document.width !== 20 || document.height !== 20 || !Array.isArray(document.symbols) ||
|
||||
document.symbols.length !== 48) throw new Error('Format APRS 20x20 invalide');
|
||||
const next = new Uint8Array(48 * 400);
|
||||
document.symbols.forEach((symbol, index) => {
|
||||
if (!symbol || symbol.code !== codes[index] || typeof symbol.pixels !== 'string' ||
|
||||
!/^[01]{400}$/.test(symbol.pixels)) throw new Error(`Symbole ${codes[index]} invalide`);
|
||||
for (let p = 0; p < 400; p++) next[index * 400 + p] = symbol.pixels[p] === '1' ? 1 : 0;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
codes.forEach((code, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'symbol';
|
||||
button.setAttribute('aria-label', `${code} ${names[index]}`);
|
||||
button.innerHTML = `<span class="mini" aria-hidden="true"></span><strong>${code}</strong><small>${names[index]}</small>`;
|
||||
button.addEventListener('click', () => { selected = index; renderSelected(); remember(); });
|
||||
symbols.appendChild(button);
|
||||
renderMini(index);
|
||||
});
|
||||
|
||||
root.querySelector('#undo-one').addEventListener('click', () => {
|
||||
pixels.set(original.subarray(selected * 400, selected * 400 + 400), selected * 400);
|
||||
renderSelected(); renderMini(selected); remember();
|
||||
});
|
||||
root.querySelector('#undo-all').addEventListener('click', () => {
|
||||
pixels = new Uint8Array(original);
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected(); remember();
|
||||
});
|
||||
root.querySelector('#invert').addEventListener('click', () => {
|
||||
for (let p = 0; p < 400; p++) pixels[selected * 400 + p] ^= 1;
|
||||
renderSelected(); renderMini(selected); remember();
|
||||
});
|
||||
root.querySelector('#export-json').addEventListener('click', () => {
|
||||
const blob = new Blob([JSON.stringify(exportDocument(), null, 2) + '\n'], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'aprs_symbols_20_edited.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
saveStatus.textContent = 'JSON exporté';
|
||||
});
|
||||
root.querySelector('#import-json').addEventListener('change', async event => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
pixels = pixelsFromDocument(JSON.parse(await file.text()));
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected();
|
||||
remember();
|
||||
saveStatus.textContent = 'JSON importé et sauvegardé';
|
||||
} catch (error) {
|
||||
saveStatus.textContent = error instanceof Error ? error.message : 'Import impossible';
|
||||
}
|
||||
event.target.value = '';
|
||||
});
|
||||
|
||||
window.addEventListener('openai:set_globals', event => {
|
||||
const next = event.detail?.globals?.widgetState?.privateContent;
|
||||
if (!next) return;
|
||||
try {
|
||||
if (typeof next.packed === 'string') pixels = unpackPixels(next.packed);
|
||||
else if (next.pixels && next.pixels.length === 19200)
|
||||
pixels = Uint8Array.from(next.pixels, c => c === '1' ? 1 : 0);
|
||||
else return;
|
||||
selected = Math.max(0, Math.min(47, next.selected || 0));
|
||||
for (let i = 0; i < 48; i++) renderMini(i);
|
||||
renderSelected();
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
new ResizeObserver(renderSelected).observe(source);
|
||||
|
||||
renderSelected();
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
+22
-14
@@ -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
|
||||
|
||||
@@ -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);
|
||||
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 if(cur==CUR_PATH){
|
||||
if(back) g.epath=(uint8_t)(g.epath?g.epath-1u:PATH_MAX);
|
||||
else g.epath=(uint8_t)(g.epath<PATH_MAX?g.epath+1u:0u);
|
||||
} else if(back) g.esym=(uint8_t)(g.esym?g.esym-1u:SYM_COUNT-1u);
|
||||
else g.esym=(uint8_t)(g.esym+1u<SYM_COUNT?g.esym+1u:0u);
|
||||
}
|
||||
else if(key==APP_KEY_MENU){
|
||||
if(!posOk(g.ed)){ g.status=ST_BADPOS; return; }
|
||||
memcpy(g.pos,g.ed,POS_DIGITS);
|
||||
g.hemi=g.ehemi; g.ssid=g.essid; g.path=g.epath;
|
||||
g.hemi=g.ehemi; g.ssid=g.essid; g.path=g.epath; g.sym=g.esym;
|
||||
g.edit=false;
|
||||
build();
|
||||
if(!g.flen) g.status=ST_NOCALL;
|
||||
@@ -518,7 +541,7 @@ static void handleKeys(void){
|
||||
else {
|
||||
int8_t d=A->nav_dir(key);
|
||||
if(d<0 && g.cur) g.cur--;
|
||||
if(d>0 && g.cur<CUR_PATH) g.cur++;
|
||||
if(d>0 && g.cur<CUR_SYM) g.cur++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -535,7 +558,7 @@ static void handleKeys(void){
|
||||
else if(key==APP_KEY_STAR){ g.cw^=CW_COMPACT; g.top=0; } /* the view */
|
||||
else if(key==APP_KEY_5){
|
||||
memcpy(g.ed,g.pos,POS_DIGITS);
|
||||
g.ehemi=g.hemi; g.essid=g.ssid; g.epath=g.path;
|
||||
g.ehemi=g.hemi; g.essid=g.ssid; g.epath=g.path; g.esym=g.sym;
|
||||
g.cur=0;
|
||||
g.top=0; /* the frame may change: back to its top */
|
||||
g.edit=true;
|
||||
@@ -553,6 +576,7 @@ void app_main(const app_api_t *api){
|
||||
g.lvl=LVL_DEF;
|
||||
g.ssid=CFG_SSID;
|
||||
g.path=CFG_PATH;
|
||||
g.sym=SYM_INDEX;
|
||||
api->asset_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]<SYM_COUNT) g.sym=c[12];
|
||||
}
|
||||
build();
|
||||
if(!g.flen) g.status=ST_NOCALL;
|
||||
@@ -591,5 +616,6 @@ void app_main(const app_api_t *api){
|
||||
c[9]|=(uint8_t)(g.hemi<<4); /* the 14th nibble */
|
||||
c[10]=(uint8_t)(0x80u|(g.path<<4)|g.ssid);
|
||||
c[11]=g.cw;
|
||||
c[12]=g.sym;
|
||||
api->cfg_save(c,CFG_LEN);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user