diff --git a/App/apps/aprsrx/README.md b/App/apps/aprsrx/README.md new file mode 100644 index 00000000..223699ae --- /dev/null +++ b/App/apps/aprsrx/README.md @@ -0,0 +1,156 @@ +# APRS RX: on-radio APRS receiver (work in progress) + +Goal: an overlay app that receives APRS (AX.25 UI frames, Bell 202 AFSK 1200 +bauds) on the UV-K1 / UV-K5 v3 and shows the last frame, then an APRS TX app. + +Status: + +| Step | State | +|---|---| +| Hardware demodulation by the BK4829 FSK block | **Dead end** (2026-09-25, FT3D on 144.800: the chip has no Bell 202 demodulator) | +| 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 (builds: 3976 B, 120 B free; radio test pending) | +| 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 | + +## Using the app + +1. Set the VFO to the APRS frequency, **FM**: 144.800 MHz, or 433.650 MHz for the + Flipper test files. +2. Launch **APRS RX**. +3. Lower the volume: the RX audio must stay on (it is what reaches PA4), so the + speaker plays the channel. + +Since v0.4 the app decodes **continuously**, as a TNC does: no RSSI trigger, +the FCS and a UI-frame check (control 0x03, PID 0xF0) sort frames from noise. +Up to v0.3, captures started 10 dB above a tracked noise floor; on a busy +144.800 that floor crept up (−132 → −104 dBm), so weak stations were never +sampled. Several frames in one transmission are now received too. + +Keys and the screen cost sample time (the key scan alone is ~400 µs), so they +are served every 50 ms only while no slicer is inside a preamble (two +back-to-back flags) or a frame whose first bytes look like a callsign (busy +~4 % of the time on noise, in the model), and after 3 s of busy at the latest. +The screen is redrawn after a new frame, a key, or every 5 s. + +| Screen | Content | +|---|---| +| Status bar | `APRS RX` title, then a `WAIT` capsule until the first frame | +| Line 0 | Source call and frame number, e.g. `F4HWN-7 #3` | +| Tiny row y=8 | `>DEST,DIGI*,...` (as many path entries as fit on one row) | +| Tiny rows y=13/19/25 | Info field, 3 × 32 characters (non-ASCII shown as `.`); for Mic-E (most Yaesu/Kenwood beacons, v0.3): position `48 50.89N 002 16.25E`, then speed km/h, course, symbol and message type (`Off Duty`, `En Route`...), then altitude and comment without the device markers; for an uncompressed position (`!` `=` `/` `@`, v0.5): position, then the timestamp if any, the symbol (table + code) and the comment over two rows. Compressed positions stay raw text | +| Lines 4-5 | Under a dotted separator (y=30), the frequency drawn as the main screen draws it (big digits up to the kHz, the last two in the small font), centred; same code as APRS TX | +| Bottom row y=49 | `ok 12 -89dBm sl 5/12/7`: frames received, RSSI at the end of the last frame, frames per slicer (space-favouring / neutral / mark-favouring; a frame found by several slicers counts in each, but once in `ok`) | + +Keys (UV-K5 and UV-K1): + +| Key | Action | +|---|---| +| MENU | Clear the last frame and the counters | +| EXIT | Quit | + +The receive path is the firmware's own (STD: 300 Hz high-pass, de-emphasis, +3 kHz low-pass). Up to v0.3, key 1 switched to RAW (filters and AFC off, as +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. + +## Demodulator (`test/model_rx.py`, class `Demod`; the C is a transcription) + +Per ADC sample (9.6 kHz, 8 samples per bit): + +1. DC removal (1-pole tracker, 64 samples). +2. Band-pass biquad centred on √(1200·2200) = 1625 Hz, Q 0.9 (Q14): equal gain + on both tones, removes the out-of-band noise the box correlators let through. +3. Four sliding correlators over one bit: I/Q at 1200 Hz (8-entry cos table) and + 2200 Hz (48 entries = 11 cycles). The sine is the cos table read 3/4 period + ahead (+6, +12). Products `>> 8`, so the decision below never overflows int32. +4. Magnitudes `max + 3/8 min`, per-tone peak trackers (attack 1/32, decay + 1/1024): each tone normalised by its own peak, so the twist (pre-emphasis or + not, de-emphasis or not) does not bias the decision much. +5. **3 slicers** (since v0.4), decision `sign(wa·Mm·Ps − wb·Ms·Pm)` with + wa:wb = 2:3, 1:1, 3:2, each with its own DPLL, NRZI and HDLC below (Direwolf's + multi-slicer idea): the AGC does not cancel a strong twist in noise. A frame + from any slicer is accepted; a copy from another slicer within 1 s is dropped. +6. DPLL: 65536 per bit, a bit at each wrap; each transition pulls the phase 1/4 of + the way toward mid-bit + half a sample (the transition is only seen at the + next sample: without that half-sample, a +1 % clock failed long frames). +7. NRZI, HDLC (flag, destuffing, abort after 7 ones), CRC-16/X.25 per byte, + frame accepted on the residue 0xF0B8 **and** as a UI frame: noise brings ~0.7 + candidates per second to the FCS check, so without the UI check a false frame + would pass about once a day. + +### Slicers (`test/variants.py`, 4 seeds, 24 hard cases: noise 3000-4000 Hz, ±1 % clock, twist −9 to +9 dB at noise 2500 Hz, RAW and STD) + +| Variant | Frames /96 | +|---|---| +| 1 slicer (v0.1-v0.3) | 57 | +| 3 slicers, weights ×2 | 65 | +| **3 slicers, weights ×1.5 (v0.4)** | **71** | +| 5 slicers (×1.5 and ×2) | 71 | + +The gain is on strong twist: sine AFSK at +6 / +9 dB in RAW goes from 0/4 to 4/4. +Noise only (30 s, 2 seeds, 50 and 400 LSB rms): no frame accepted. + +### Model results (`test/model_rx.py`, 3 seeds per case, 1 slicer; 3 slicers: all ≥ these) + +Channel: discriminator output + white noise (rms over 48 kHz) → radio audio path +(RAW: 5 kHz low-pass; STD: 300 Hz high-pass, 750 µs de-emphasis, 3 kHz low-pass) +→ 12-bit ADC at 9.6 kHz, 0.065 LSB/Hz (EPIRB 406 measurement), optional clock +error. 150 ms of no-carrier noise before and after each burst. + +| Case | RAW | STD | +|---|---|---| +| Flipper, clean: `pos`, `long` (233 bytes) | 3/3, 3/3 | 3/3, 3/3 | +| Flipper `badfcs` (must be rejected) | 0 frames | 0 frames | +| Flipper, noise 1500 / 3000 / 4500 Hz | 3/3, 2/3, 0/3 | 3/3, 2/3, 0/3 | +| Flipper, ±4 kHz carrier offset | 3/3 | 3/3 | +| Flipper `long`, ±1 % sample clock | 3/3 | 3/3 (−1 %), 3/3 (+1 %) | +| Sine AFSK (a real station), twist −6 / 0 / +6 dB, noise 1500 | 3/3, 3/3, 2/3 | 2/3, 3/3, 3/3 | + +Noise 4500 Hz is Eb/N0 ≈ 9.6 dB: the theoretical frame success of non-coherent +FSK on a 520-bit frame is then ~6 %, so the failures there are expected. +(At ±0.5 % clock, 6/6 in every variant tried.) + +## Flipper Zero test transmitter (`test/flipper_aprs.py`) + +The Flipper's CC1101 cannot put an audio tone on FM, only switch between two +frequencies (preset `2FSKDev238Async`, ±2.38 kHz). Each AFSK tone is therefore +sent as a square wave: the carrier toggles at twice the tone frequency, +phase-continuous across bits, with edge times rounded from exact values (no +drift). The receiver's discriminator outputs that square wave and its audio +low-pass leaves mostly the fundamental. The Flipper cannot reach 2 m anyway; the +script refuses 144-146 MHz. + +``` +test/flipper_aprs.py test/flipper [--call F4HWN] [--freq 433650000] +``` + +writes `aprs_pos.sub`, `_digi`, `_msg`, `_status`, `_long` (233 bytes, 2 s) and +`_badfcs` (one info bit flipped: must not be shown). 100 ms of carrier, 40 flags +(267 ms), the frame, 3 flags, 10 ms of carrier. Copy them to `subghz/` on the +Flipper, set the radio to 433.650 MHz FM, launch the app, then Send one file at +a time, about 1 s apart. (Up to v0.4, UP/DOWN tuned ±5 kHz for a Flipper whose +crystal puts the carrier off the channel; never needed on the bench, dropped in +v0.5 to fit 4 KiB.) + +`test/ax25.py` builds the frames (shared by the generator and the model); +`test/mice.py` checks the Mic-E display decoder against a spec encoder, and +the standard position display against real frames; +`test/variants.py` compares demodulator variants on the hard cases (dev tool). + +## Bench checklist + +- Level reaching the ADC (`pp`, shown up to v0.4, dropped in v0.5 to make + room): estimated ~300 LSB peak-to-peak for the Flipper (±2.38 kHz at + 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. +- The per-slicer counters show which decision threshold the stations need: if + one outer slicer does most of the work, the twist of the path is off-centre. diff --git a/App/apps/aprsrx/app.ld b/App/apps/aprsrx/app.ld new file mode 100644 index 00000000..4a4525de --- /dev/null +++ b/App/apps/aprsrx/app.ld @@ -0,0 +1,43 @@ +/* Overlay-app link script (POC). + * + * The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with + * the multiboot RAM stub — never both active at once). Code + rodata + data + + * bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+ + * data only; the loader zeroes the overlay before copying, so bss starts clean. + * + * VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM + * layout shifts, update APP_VMA and the loader's compile-time assert catches a + * mismatch. */ + +/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It + * varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x... + * The default is only a fallback for a standalone size check. */ +APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280; +APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */ + +ENTRY(app_main) + +MEMORY { + APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH +} + +SECTIONS { + .app APP_VMA : { + KEEP(*(.text.entry)) /* app_main pinned to offset 0 */ + *(.text .text.*) + *(.rodata .rodata.*) + . = ALIGN(4); + *(.data .data.*) + . = ALIGN(4); + __app_bss_start = .; + *(.bss .bss.* COMMON) + . = ALIGN(4); + __app_bss_end = .; + } > APP + + __app_end = .; + ASSERT(__app_end <= APP_VMA + APP_LENGTH, + "APRS RX overlay app overflows the 4 KiB overlay") + + /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) } +} diff --git a/App/apps/aprsrx/aprsrx_app.c b/App/apps/aprsrx/aprsrx_app.c new file mode 100644 index 00000000..dda33871 --- /dev/null +++ b/App/apps/aprsrx/aprsrx_app.c @@ -0,0 +1,625 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * APRS RX - receives APRS (AX.25 UI frames, Bell 202 AFSK 1200 bauds) in + * software and shows the last frame: source, destination and path, the info + * field. Tune the VFO (FM; 144.800 MHz, or 433.650 MHz for the Flipper Zero + * bench files of test/flipper_aprs.py), then launch. + * + * The BK4829 has no Bell 202 demodulator (see README.md), so the RX audio is + * sampled as EPIRB 406 does: it reaches PA4 (the voice DAC pin), held at + * mid-scale by the MCU DAC (unbuffered), and is read on ADC channel 4 at + * 9.6 kHz, timed from SysTick, continuously, as a TNC does: no RSSI gate, the + * FCS and the UI check sort frames from noise. Each sample goes through the + * demodulator modelled and tested in test/model_rx.py: band-pass, 1200/2200 Hz + * sliding correlators over one bit, per-tone AGC, then 3 slicers (mark/space + * weights 2:3, 1:1, 3:2), each with its DPLL, NRZI, HDLC and FCS. Keep the two + * in step. + * Keys and the screen cost sample time (the key scan alone is ~400 us), so they + * are served every 50 ms only while no slicer is inside a preamble or a + * plausible frame; the redraw follows a new frame, a key, or every 5 s. + * + * Keys (UV-K5 and UV-K1): MENU clear · EXIT quit. The speaker plays the + * channel: lower the volume. + * The loader re-runs RADIO_SetupRegisters on exit; the app restores the ADC, + * PA4, the DAC and its clock itself. + */ + +#include +#include +#include +#include "../app_api.h" +#include "aprsrx_assets.h" /* generated by gen_assets.py */ + +/* ---- MCU registers (PY32F071, core and SysTick at 48 MHz, 10 ms period) ---- */ +#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u) +#define SYST_VAL (*(volatile uint32_t *)0xE000E018u) +#define ADC_SR (*(volatile uint32_t *)0x40012400u) +#define ADC_CR2 (*(volatile uint32_t *)0x40012408u) +#define ADC_SMPR3 (*(volatile uint32_t *)0x40012414u) +#define ADC_SQR3 (*(volatile uint32_t *)0x40012438u) +#define ADC_DR (*(volatile uint32_t *)0x40012450u) +#define GPIOA_MODER (*(volatile uint32_t *)0x50000000u) +#define DAC_CR (*(volatile uint32_t *)0x40007400u) +#define DAC_SWTRIGR (*(volatile uint32_t *)0x40007404u) +#define DAC_DHR12R1 (*(volatile uint32_t *)0x40007408u) +#define RCC_APBENR1 (*(volatile uint32_t *)0x4002103Cu) +#define RCC_DACEN (1u << 29) +/* DAC channel 1 on, output buffer off, software trigger (TSEL1 = 111) */ +#define DAC_CR_BIAS ((1u << 0) | (1u << 1) | (1u << 2) | (7u << 3)) +#define BIAS_CODE 2048u +#define ADC_SR_EOC (1u << 1) +#define ADC_CR2_START ((1u << 22) | (1u << 20)) /* SWSTART | EXTTRIG */ +#define SMP8_POS 24u +#define SMP4_POS 12u +#define ADC_CH_PA4 4u +#define FS 9600u +#define CYC_PER_SAMPLE (48000000u / FS) /* 5000 */ +#define HOUSE_EVERY 480u /* samples between key/screen slots (50 ms) */ +#define BUSY_MAX 60u /* serve them anyway after 60 busy slots (3 s) */ +#define REFRESH_MS 5000u /* periodic redraw (battery) */ +#define DUP_MS 1000u /* the same frame from another slicer */ + +/* ---- BK4829 ---- */ + +/* ---- demodulator (test/model_rx.py, class Demod) ---- */ +#define BP_B0 5356 /* band-pass 1625 Hz, Q 0.9, Q14 (b2 = -b0, b1 = 0) */ +#define BP_A1 (-10714) +#define BP_A2 5673 +#define AGC_ATT 5 /* peak tracker: attack 1/32 ... */ +#define AGC_DEC 10 /* ... decay 1/1024 per sample */ +#define PK_MIN 16 +#define PLL_STEP 8192 /* 65536 per bit / 8 samples per bit */ +#define PLL_CTR 36864 /* transitions pulled to mid-bit + half a sample */ +#define PLL_SHIFT 2 /* 1/4 of the error per transition */ +#define NSL 3 /* slicers */ +#define PRE_BITS 24u /* busy for 3 bytes after two back-to-back flags */ + +/* ---- AX.25 ---- */ +#define FRAME_MAX 330u /* 10 addresses + control + PID + 256 info + FCS */ +#define FRAME_MIN 18u /* 2 addresses + control + PID + FCS */ +#define FCS_GOOD 0xF0B8u +#define COUNT_MAX 999u /* counters stop here: the stats row stays within 32 tiny chars */ + +/* the front end shared by the slicers */ +typedef struct { + int32_t dc; /* DC tracker, Q4 */ + int32_t x1, x2, y1, y2; /* band-pass state */ + int32_t im, qm, is, qs; /* sliding correlator sums */ + int32_t pm, ps; /* per-tone magnitude peaks (AGC) */ + int16_t ring[8][4]; /* the products of the last bit */ + uint8_t r, km, ks; /* ring slot, table phases */ + int8_t cm[8], cs[48]; /* cos tables (assets) */ +} dem_t; + +/* one slicer: decision weights, DPLL, NRZI, HDLC */ +typedef struct { + int32_t dprev, phase; /* last decision, DPLL phase (65536/bit) */ + uint16_t n, crc; /* bytes, running FCS */ + uint8_t wa, wb, last; /* mark/space weights, last level */ + uint8_t sr, ones, byte, nb; /* HDLC: shift reg, 1 run, byte, bit count */ + uint8_t inframe, hdr; /* between flags, first bytes look like a call */ + uint8_t bits, pre; /* bits since the last flag, back-to-back flags */ + uint8_t buf[FRAME_MAX]; +} sl_t; + +/* The app state in one struct: a function loads one base address instead of one + * literal-pool word per global. Words, then bytes, then halfwords, so every field + * stays within the Thumb load/store immediate ranges (ldrb <= 31, ldrh <= 62, + * ldr <= 124). The loader zeroes the overlay (.bss) at every launch. */ +static struct { + const app_api_t *A; + uint8_t *frm; /* last good frame (app_main's stack) */ + uint32_t vfoFreq; /* VFO RX frequency at launch, 10 Hz units */ + uint32_t tPrev, tCyc; /* SysTick cycle counter */ + uint32_t tFrame, tDraw; /* ticks_ms of the last frame, last redraw */ + uint32_t savedSqr3, savedSmpr3, savedModer, savedDac, savedRcc, savedDhr; + uint8_t seq, prevKey, redraw; + bool running; + int16_t rssi; /* RSSI at the end of the last frame */ + uint16_t flen, nOk; /* frame length, frames */ + uint16_t good[NSL]; /* frames per slicer (duplicates included) */ +} g; +static char str[34]; + +/* ---- tiny freestanding helpers: GCC may turn the zeroing / copy loops into calls ---- */ +void *memset(void *d, int c, size_t n) { + uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d; +} +void *memcpy(void *d, const void *s, size_t n) { + uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d; +} + +/* ---- formatting ---- */ +static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; } +/* Formatting by repeated subtraction: no division. Values printed stay below 100000. */ +static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; } +static const uint16_t P10[]={10000u,1000u,100u,10u,1u}; +static char *puti(char *o,int32_t v){ + uint32_t u; + if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v; + bool lead=false; + for(uint8_t i=0;i<5;i++){ + uint8_t c=sub(&u,P10[i]); + if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; } + } + return o; +} +/* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so + * every row built below stays within that. */ +static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); } + +#define WAIT_CAPS_X 40u /* "WAIT" capsule in the status bar, after the title */ + +/* Tiny rows under the source line: the path (capitals and digits: no + * descenders), then 3 info rows 6 px apart (the 3x5 font is 5 px, 6 with + * descenders), ending at y = 29 above the separator (y = 30). */ +#define Y_PATH 8u +#define Y_INFO 13u +#define ROW_STEP 6u + +/* The frequency (10 Hz units) drawn as the main screen draws it (ui/main.c, + * ENABLE_BIG_FREQ): the big digits up to the kHz on lines 4-5, the last two + * digits in the small font on line 5 right after them (from 1 GHz, the whole + * string in the big font), the group centred. Big digits are 13 px (drawn from + * +2), the point 3 px: the group shows (n big digits x 13 + 14) px. No division. + * Same code as APRS TX. */ +static void drawFreq(uint32_t f){ + const app_api_t *A=g.A; + uint16_t m=0; + while(f>=100000u){ f-=100000u; m++; } + char *o=puti(str,m); + *o++='.'; + for(uint8_t i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i])); + *o='\0'; + uint8_t n=(uint8_t)(o-str), big=(uint8_t)(n-3u); /* digits before "00" */ + if(m>=1000u){ A->print_string(str,(uint8_t)((128u-n*8u)>>1),0,4,8); return; } + uint8_t x=(uint8_t)(((128u-(big*13u+14u))>>1)-2u); + A->print_normal(str+n-2u,(uint8_t)(x+big*13u+3u),0,5); /* "00" */ + str[n-2u]='\0'; + A->display_freq(str,x,4,false); /* "144.800" */ +} + +/* ---- SysTick cycle counter (call at least every 10 ms; wraps every 89 s, + * so compare with a signed difference) ---- */ +static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; } +static uint32_t clkCyc(void){ + uint32_t v=SYST_VAL; + g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v); + g.tPrev=v; + return g.tCyc; +} + +/* ---- PA4 bias: the pin has no DC reference of its own (AC-coupled audio); the + * MCU DAC on PA4, output buffer off, at mid-scale, holds it at ~VDD/2. + * Set for the whole run, restored on exit (see EPIRB 406). ---- */ +static void biasOn(void){ + GPIOA_MODER=g.savedModer|(3u<<8); /* PA4 analog */ + RCC_APBENR1=g.savedRcc|RCC_DACEN; + DAC_CR=DAC_CR_BIAS; + DAC_DHR12R1=BIAS_CODE; + DAC_SWTRIGR=1u; /* DHR -> DOR */ +} +static void biasOff(void){ DAC_CR=g.savedDac; DAC_DHR12R1=g.savedDhr; RCC_APBENR1=g.savedRcc; GPIOA_MODER=g.savedModer; } + +/* ---- ADC on PA4 (channel 4); channel 8 (battery) restored afterwards ---- */ +static void adcSelPA4(void){ + ADC_SMPR3=(g.savedSmpr3&~(7u<>SMP8_POS)&7u)<='A'&&c<='Z')||(c>='0'&&c<='9')||c==' '; } + +/* An APRS frame is a UI frame: the address field ends (bit 0 of an SSID byte) + * and is followed by control 0x03 and PID 0xF0. Rejects the noise burst that + * passes the 16-bit FCS about once a day in continuous decoding. */ +static bool isUI(const uint8_t *f,uint16_t n){ + uint16_t end=(uint16_t)(n-2u), e=13; + while(!(f[e]&1u) && e+7ubuf, m->n bytes, until the next byte). ---- */ +static bool hdlcBit(sl_t *m,uint8_t b){ + if(m->bits<255u) m->bits++; + m->sr=(uint8_t)((m->sr>>1)|(b<<7)); + if(m->sr==0x7Eu){ + bool ok = m->inframe && m->n>=FRAME_MIN && m->crc==FCS_GOOD && isUI(m->buf,m->n); + m->pre = m->bits==8u ? (uint8_t)(m->pre<255u?m->pre+1u:255u) : 0; + m->bits=0; + if(ok) return true; /* n kept for the caller, reset below */ + m->inframe=1; m->hdr=1; m->n=0; m->crc=0xFFFFu; + m->ones=m->byte=m->nb=0; + return false; + } + if(b){ + if(++m->ones>6u){ m->ones=7; m->inframe=0; return false; } /* abort / noise */ + } else { + if(m->ones==5u){ m->ones=0; return false; } /* stuffed zero */ + m->ones=0; + } + if(!m->inframe) return false; + m->byte|=(uint8_t)(b<nb); + if(++m->nb==8u){ + if(m->ncrc^m->byte; /* CRC-16/X.25, reflected 0x1021 */ + for(uint8_t i=0;i<8u;i++) c=(c&1u)?(uint16_t)((c>>1)^0x8408u):(uint16_t)(c>>1); + m->crc=c; + if(m->n<6u && ((m->byte&1u) || !callChar((uint8_t)(m->byte>>1)))) m->hdr=0; + m->buf[m->n++]=m->byte; + } else m->inframe=0; + m->byte=m->nb=0; + } + return false; +} + +/* A slicer is busy inside a preamble (two back-to-back flags, then 3 bytes) or + * a frame whose first bytes look like a callsign: noise is busy ~4 % of the time. */ +static bool busy(const sl_t *m){ + return (m->pre && m->bitsinframe && m->n && m->hdr); +} + +/* |(i, q)| ~ max + 3/8 min */ +static int32_t mag(int32_t i,int32_t q){ + if(i<0) i=-i; + if(q<0) q=-q; + if(i>2)+(q>>3); +} + +/* ---- front end, one ADC sample: *a = Mm*Ps, *b = Ms*Pm (each tone over its + * own peak, without division; both stay below ~2^21) ---- */ +static void front(dem_t *m,int32_t adc,int32_t *a,int32_t *b){ + m->dc+=((adc<<4)-m->dc)>>6; + int32_t x=adc-(m->dc>>4); + int32_t y=(BP_B0*(x-m->x2)-BP_A1*m->y1-BP_A2*m->y2)>>14; + m->x2=m->x1; m->x1=x; m->y2=m->y1; m->y1=y; + + /* sliding correlators over one bit; the sine is the cos table 3/4 period ahead */ + int16_t *o=m->ring[m->r]; + uint8_t k=(uint8_t)(m->ks+12u); if(k>=48u) k-=48u; + int32_t p; + p=(y*m->cm[m->km])>>8; m->im+=p-o[0]; o[0]=(int16_t)p; + p=(y*m->cm[(m->km+6u)&7u])>>8; m->qm+=p-o[1]; o[1]=(int16_t)p; + p=(y*m->cs[m->ks])>>8; m->is+=p-o[2]; o[2]=(int16_t)p; + p=(y*m->cs[k])>>8; m->qs+=p-o[3]; o[3]=(int16_t)p; + m->r=(uint8_t)((m->r+1u)&7u); + m->km=(uint8_t)((m->km+1u)&7u); + if(++m->ks>=48u) m->ks=0; + + int32_t mm=mag(m->im,m->qm), ms=mag(m->is,m->qs); + m->pm+= mm>m->pm ? (mm-m->pm)>>AGC_ATT : -(m->pm>>AGC_DEC); + m->ps+= ms>m->ps ? (ms-m->ps)>>AGC_ATT : -(m->ps>>AGC_DEC); + if(m->pmpm=PK_MIN; + if(m->psps=PK_MIN; + *a=mm*m->ps; + *b=ms*m->pm; +} + +/* ---- slicer: decision sign(wa*a - wb*b), DPLL, NRZI, HDLC ---- */ +static bool slice(sl_t *m,int32_t a,int32_t b){ + int32_t d=a*m->wa-b*m->wb; + /* DPLL: a bit at each phase wrap (mid-bit), transitions pulled to mid-phase */ + m->phase+=PLL_STEP; + if((d>0)!=(m->dprev>0)) m->phase+=(PLL_CTR-m->phase)>>PLL_SHIFT; + m->dprev=d; + if(m->phase>=65536){ + m->phase-=65536; + uint8_t lv=d>0; + bool r=hdlcBit(m,lv==m->last); /* NRZI: no change = 1 */ + m->last=lv; + return r; + } + return false; +} + +/* ---- a good frame from slicer k: shown unless another slicer just gave it ---- */ +static void accept(sl_t *m,uint8_t k){ + const app_api_t *A=g.A; + uint16_t n=m->n; + uint32_t t=A->ticks_ms(); + if(g.good[k]buf[n-1] && g.frm[n-2]==m->buf[n-2] + && t-g.tFramebuf,n); + g.flen=n; + if(g.nOkrssi_dbm(); /* the carrier is still up: closing flags */ + g.redraw=1; + } + g.tFrame=t; + m->inframe=1; m->hdr=1; m->n=0; m->crc=0xFFFFu; /* this flag opens the next frame */ + m->ones=m->byte=m->nb=0; +} + +/* ---- AX.25 address field -> "F4HWN-7" ---- */ +static char *putCall(char *o,const uint8_t *a){ + for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; } + uint8_t ssid=(uint8_t)((a[6]>>1)&15u); + if(ssid){ *o++='-'; o=puti(o,ssid); } + return o; +} + +/* The tiny font covers 0x20-0x7F: anything else shows as '.'. */ +static char safe(uint8_t ch){ return (ch<0x20u||ch>0x7Eu)?'.':(char)ch; } + +/* Info bytes f[i..end) on one tiny row from o (up to 32 characters); returns + * the next byte. */ +static uint16_t textRow(char *o,const uint8_t *f,uint16_t i,uint16_t end,uint8_t y){ + for(; o='0'&&c<='9') return (uint8_t)(c-'0'); + if(c>='A'&&c<='J') return (uint8_t)(c-'A'); + if(c>='P'&&c<='Y') return (uint8_t)(c-'P'); + return 0; /* K, L, Z: position ambiguity */ +} +static bool micBit(uint8_t c){ return c>='P' || (c>='A'&&c<='K'); } +static bool micType(uint8_t t){ return t=='`'||t=='\''||t==0x1Cu||t==0x1Du; } + +static void drawMicE(const uint8_t *f,uint16_t i,uint16_t end,const char *s){ + const app_api_t *A=g.A; + uint8_t c[6]; + for(uint8_t k=0;k<6u;k++) c[k]=(uint8_t)(f[k]>>1); + + /* "48 50.89N 002 16.25E" */ + char *o=str; + for(uint8_t k=0;k<6u;k++){ + *o++=(char)('0'+micDigit(c[k])); + if(k==1u) *o++=' '; + if(k==3u) *o++='.'; + } + *o++=micBit(c[3])?'N':'S'; + uint32_t deg=(uint8_t)(f[i+1]-28u); + if(micBit(c[4])) deg+=100u; + if(deg>=180u && deg<=189u) deg-=80u; + else if(deg>=190u && deg<=199u) deg-=190u; + uint32_t mn=(uint8_t)(f[i+2]-28u); + if(mn>=60u) mn-=60u; + *o++=' '; o=put3(o,deg); *o++=' '; o=put2(o,mn); *o++='.'; o=put2(o,(uint8_t)(f[i+3]-28u)); + *o++=micBit(c[5])?'W':'E'; + tiny(Y_INFO,o); + + /* "12km/h 245 [/ En Route": speed in knots x 1.8525 (1897/1024) */ + uint32_t dc=(uint8_t)(f[i+5]-28u); + uint32_t sp=(uint32_t)(uint8_t)(f[i+4]-28u)*10u+sub(&dc,10u); + uint32_t crs=dc*100u+(uint8_t)(f[i+6]-28u); + if(sp>=800u) sp-=800u; + if(crs>=400u) crs-=400u; + o=puti(str,(int32_t)((sp*1897u)>>10)); o=put(o,s+T_KMH); + o=puti(o,(int32_t)crs); *o++=' '; + *o++=safe(f[i+7]); *o++=safe(f[i+8]); *o++=' '; + uint8_t idx=(uint8_t)(micBit(c[0])*4u+micBit(c[1])*2u+micBit(c[2])); + bool custom=false; + for(uint8_t k=0;k<3u;k++) if(c[k]>='A'&&c[k]<='K') custom=true; + if(custom && idx){ o=put(o,s+T_CUSTOM); *o++=(char)('0'+7u-idx); } + else o+=A->asset_read((uint16_t)(T_MSG+idx*T_MSG_STRIDE),o,T_MSG_STRIDE); /* NUL-padded */ + tiny(Y_INFO+ROW_STEP,o); + + /* comment: trailing CR/LF, the device markers (Yaesu/other: '`' or '\'' + + * text + 2 chars; Kenwood: '>' or ']' + text [+ '=' or '^']) and a leading + * "xxx}" altitude (base 91, metres + 10000) */ + uint16_t j=(uint16_t)(i+9u); + while(end>j && (f[end-1]=='\r'||f[end-1]=='\n')) end--; + if(j=2) end-=2u; } + else if(m=='>'||m==']'){ j++; if(end>j && (f[end-1]=='='||f[end-1]=='^')) end--; } + } + o=str; + if(end-j>=4 && f[j+3]=='}'){ + int32_t alt=(((int32_t)f[j]-33)*91+((int32_t)f[j+1]-33))*91+((int32_t)f[j+2]-33)-10000; + o=puti(o,alt); *o++='m'; *o++=' '; + j+=4u; + } + textRow(o,f,j,end,Y_INFO+2u*ROW_STEP); +} + +/* ---- uncompressed position ('!' '=' or, after a 7-character timestamp, '/' + * '@'; test/mice.py draw_pos is the reference): "49 21.07N 001 50.50E", then + * the timestamp, the symbol and the comment. False for anything else (compressed + * positions included), which is then shown as raw text. ---- */ +static bool drawPos(const uint8_t *f,uint16_t i,uint16_t end){ + uint8_t t=f[i]; + uint16_t p=(uint16_t)(i+1u); + if(t=='/'||t=='@') p+=7u; + else if(t!='!'&&t!='=') return false; + if(endi+1u){ for(uint16_t k=(uint16_t)(i+1u);kasset_read(0,s,UI_SIZE); + A->display_clear(); + A->status_clear(); + A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u)); + A->draw_battery(); + + if(!g.flen) /* no frame yet: a capsule, as APRS TX's TRANSMIT */ + A->print_inverse(s+T_WAIT,WAIT_CAPS_X,0,true,true,(uint8_t)(WAIT_CAPS_X+T_WAIT_CHARS*4u)); + else { + const uint8_t *f=g.frm; + uint16_t end=(uint16_t)(g.flen-2u); /* the FCS is not shown */ + + o=putCall(str,f+7); *o++=' '; *o++='#'; o=puti(o,g.seq); *o='\0'; + A->print_normal(str,0,0,0); /* source */ + + /* ">DEST,DIGI*,..." while it fits one tiny row, then the rest of the + * address field is skipped up to its end bit */ + o=str; *o++='>'; o=putCall(o,f); + uint16_t e=13; /* SSID byte of the current address */ + while(!(f[e]&1u) && e+7u=i+9u && micType(f[i])) drawMicE(f,i,end,s); + else if(!drawPos(f,i,end)) + for(uint8_t row=0;row<3u;row++) i=textRow(str,f,i,end,(uint8_t)(Y_INFO+row*ROW_STEP)); + } + + /* dotted separator above the frequency: y = 30, bit 6 of line 3 (the big + * digits start at y = 33) */ + for(uint8_t x=0;x<128u;x+=2u) A->fb[3][x]|=0x40u; + drawFreq(g.vfoFreq); + + /* bottom line: "ok 12 -89dBm sl 5/12/7": frames, RSSI of the last one, + * frames per slicer (<= 31 characters) */ + o=put(str,s+T_OK); o=puti(o,g.nOk); *o++=' '; *o++=' '; + o=puti(o,g.rssi); o=put(o,s+T_DBM); + for(uint8_t k=0;kget_key(); + if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; } + g.prevKey=key; + g.redraw=1; + if(key==APP_KEY_EXIT) g.running=false; + else if(key==APP_KEY_MENU){ + g.flen=0; g.nOk=0; g.seq=0; g.rssi=0; + for(uint8_t k=0;kbacklight_on(); /* keep the screen lit while listening */ + uint32_t t=A->ticks_ms(); + if(t-g.tDraw>=REFRESH_MS) g.redraw=1; + if(!g.redraw) return; + g.redraw=0; + g.tDraw=t; + adcRestore(); /* the battery is on ADC channel 8 */ + A->battery_sample(); + adcSelPA4(); + draw(); + A->blit_status(); + A->blit_full(); +} + +/* ---- the receiver: PA4 at 9.6 kHz, continuously, until EXIT ---- */ +static void listen(void){ + const app_api_t *A=g.A; + dem_t d; + sl_t sl[NSL]; + memset(&d,0,sizeof d); + memset(sl,0,sizeof sl); + A->asset_read(COS1200,d.cm,COS1200_LEN); + A->asset_read(COS2200,d.cs,COS2200_LEN); + d.dc=(int32_t)BIAS_CODE<<4; + d.pm=d.ps=64; + sl[0].wa=2; sl[0].wb=3; /* favours space */ + sl[1].wa=1; sl[1].wb=1; + sl[2].wa=3; sl[2].wb=2; /* favours mark */ + for(uint8_t k=0;kbacklight_on(); + g.vfoFreq=api->rx_freq(); + api->audio_path(true); + api->set_af(APP_AF_FM); + api->delay_ms(50); + + g.running=true; + listen(); + + biasOff(); + api->set_af(APP_AF_MUTE); + api->audio_path(false); +} diff --git a/App/apps/aprsrx/build.sh b/App/apps/aprsrx/build.sh new file mode 100755 index 00000000..9cbb3b30 --- /dev/null +++ b/App/apps/aprsrx/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside +# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is +# derived from the directory name; only APP_NAME - the human label baked into +# the 64-byte blob header - is per-app. The .app file is named after it (spaces +# stripped), so "Broadcast FM" -> BroadcastFM.app. +set -euo pipefail + +APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... +APP_NAME="APRS RX" # <-- the only per-app line +APP_VER="0.5" +APP_API_MIN=2 +APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) +OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) + +CC=/opt/toolchain/bin/arm-none-eabi-gcc +OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy +command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; } + +CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -fno-jump-tables -std=gnu11 -ffreestanding -fno-builtin -fno-common \ + -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra" +LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ + -Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments" + +rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous + +step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } +trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR + +step 1 assets ; APP_VER="$APP_VER" python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ + --shortcut none --assets "${APP}_assets.bin" >/dev/null +trap - ERR + +BYTES=$(wc -c < "${APP}.bin") +if [ "$BYTES" -gt 4096 ]; then + printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1 +fi +printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \ + "$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT" diff --git a/App/apps/aprsrx/gen_assets.py b/App/apps/aprsrx/gen_assets.py new file mode 100755 index 00000000..89d6ffa1 --- /dev/null +++ b/App/apps/aprsrx/gen_assets.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# APRS RX read-only assets: the screen texts and the correlator tables. +# +# The UI texts come first, each padded to a multiple of 4 bytes: draw() reads +# them in one asset_read into a word-aligned stack block, so every text costs a +# 2-byte sp-relative add instead of a literal-pool load and a pool word. +# The Mic-E message names are a fixed-stride table read straight into the row. +# The tables are 127 * cos(2 pi f n / 9600): one period of 1200 Hz (8 samples) +# and of 2200 Hz (48 samples = 11 cycles). The sine is the same table read 3/4 +# of a period ahead (+6 and +12), as in test/model_rx.py. +# +# ./gen_assets.py aprsrx_assets.bin aprsrx_assets.h +import math, os, sys +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 + +FS = 9600 +TITLE = "APRS RX" # the version is in the .app header +WAIT_CAPS = "WAIT" # status-bar capsule until a frame + +UI = [ + ("T_TITLE", TITLE), + ("T_WAIT", WAIT_CAPS), + ("T_DBM", "dBm sl "), + ("T_OK", "ok "), + ("T_KMH", "km/h "), + ("T_CUSTOM", "Custom-"), +] + +# Mic-E message types, indexed by the destination's message bits A B C +MIC_MSG = ["Emergency", "Priority", "Special", "Committed", + "Returning", "In Service", "En Route", "Off Duty"] + +def cos_table(f): + per = FS // math.gcd(FS, f) + return [int(round(127 * math.cos(2 * math.pi * f * n / FS))) for n in range(per)] + +a = Assets("APRSRX") +ui_size = 0 +for name, s in UI: + pad = -(len(s) + 1) % 4 # keep every offset word-aligned + a.text(name, s + "\0" * pad) + ui_size += len(s) + 1 + pad +a.const("UI_SIZE", ui_size) # the UI block read by draw() +a.const("T_TITLE_CHARS", len(TITLE)) +a.const("T_WAIT_CHARS", len(WAIT_CAPS)) +a.table("T_MSG", MIC_MSG) # Mic-E standard messages +a.i8("COS1200", cos_table(1200)) # 8 entries +a.i8("COS2200", cos_table(2200)) # 48 entries +a.main() diff --git a/App/apps/aprsrx/test/ax25.py b/App/apps/aprsrx/test/ax25.py new file mode 100644 index 00000000..4d075f1f --- /dev/null +++ b/App/apps/aprsrx/test/ax25.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""AX.25 UI frames for APRS, and their Bell 202 bit stream. + +build(src, dst, path, info) -> frame bytes (addresses + 0x03 0xF0 + info + FCS) +hdlc_bits(frame) -> bits on the air before NRZI: flags, the frame + LSB first with bit stuffing, flags +decode(frame) -> "SRC>DST,PATH:info" (None if the FCS is wrong) + + ax25.py [--call F4HWN] prints the test frames and their length +""" +import argparse + +TXDELAY_FLAGS = 40 # ~267 ms of 0x7E before the frame (1 flag = 6.67 ms) +TAIL_FLAGS = 3 + + +def crc16(data): + """CRC-16/X.25 (the AX.25 FCS): reflected 0x1021, init/xorout 0xFFFF.""" + crc = 0xFFFF + for b in data: + crc ^= b + for _ in range(8): + crc = (crc >> 1) ^ 0x8408 if crc & 1 else crc >> 1 + return crc ^ 0xFFFF + + +def addr(call, last=False, repeated=False): + """One 7-byte address field: 6 chars shifted left, then the SSID byte.""" + call, _, ssid = call.upper().rstrip("*").partition("-") + out = bytes((ord(c) << 1) for c in call.ljust(6)[:6]) + sb = 0x60 | ((int(ssid) if ssid else 0) << 1) + if repeated: + sb |= 0x80 # H bit: this digipeater has relayed it + if last: + sb |= 0x01 # end of the address field + return out + bytes([sb]) + + +def build(src, dst="APZK5", path=(), info=""): + hops = list(path) + f = addr(dst) + addr(src, last=not hops) + for i, h in enumerate(hops): + f += addr(h, last=i == len(hops) - 1, repeated=h.endswith("*")) + f += b"\x03\xF0" + info.encode("latin-1") + fcs = crc16(f) + return f + bytes([fcs & 0xFF, fcs >> 8]) + + +def hdlc_bits(frame, lead=TXDELAY_FLAGS, tail=TAIL_FLAGS): + flag = [(0x7E >> i) & 1 for i in range(8)] + bits = flag * lead + ones = 0 + for b in frame: + for i in range(8): + bit = (b >> i) & 1 + bits.append(bit) + ones = ones + 1 if bit else 0 + if ones == 5: + bits.append(0) # stuffed zero + ones = 0 + return bits + flag * tail + + +def decode(frame): + """Readable form of a received frame, or None if too short / bad FCS.""" + if len(frame) < 18 or crc16(frame[:-2]) != frame[-2] | (frame[-1] << 8): + return None + calls, i = [], 0 + while i + 7 <= len(frame) - 2: + c = "".join(chr(x >> 1) for x in frame[i:i + 6]).rstrip() + ssid = (frame[i + 6] >> 1) & 15 + if ssid: + c += "-%d" % ssid + if len(calls) >= 2 and frame[i + 6] & 0x80: + c += "*" + calls.append(c) + i += 7 + if frame[i - 1] & 1: + break + info = frame[i + 2:-2].decode("latin-1") + return "%s>%s%s:%s" % (calls[1], calls[0], + "".join("," + c for c in calls[2:]), info) + + +def test_frames(call="F4HWN"): + """name -> frame bytes. 'badfcs' has one flipped info bit: must be rejected.""" + pos = "!4850.90N/00216.25E-UV-K5 APRS RX test" + fr = { + "pos": build(call, path=["WIDE1-1"], info=pos), + "digi": build(call, path=["F1ZZZ-10*", "WIDE2-1"], info=pos), + "msg": build(call, path=["WIDE1-1"], info=":%-9s:Hello from the Flipper{01" % call), + "status": build(call, info=">Flipper Zero AFSK square-wave test"), + "long": build(call, path=["WIDE1-1", "WIDE2-2"], + info=">" + "".join(chr(0x41 + i % 26) for i in range(200))), + } + bad = bytearray(fr["pos"]) + bad[20] ^= 0x04 + fr["badfcs"] = bytes(bad) + return fr + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--call", default="F4HWN") + a = ap.parse_args() + for name, f in test_frames(a.call).items(): + print("%-7s %3d bytes %s" % (name, len(f), decode(f))) diff --git a/App/apps/aprsrx/test/flipper/aprs_badfcs.sub b/App/apps/aprsrx/test/flipper/aprs_badfcs.sub new file mode 100644 index 00000000..ccdd7749 --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_badfcs.sub @@ -0,0 +1,11 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 +RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 +RAW_Data: 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 10278 diff --git a/App/apps/aprsrx/test/flipper/aprs_digi.sub b/App/apps/aprsrx/test/flipper/aprs_digi.sub new file mode 100644 index 00000000..5369a7ea --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_digi.sub @@ -0,0 +1,11 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 +RAW_Data: 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 +RAW_Data: 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 +RAW_Data: 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 10000 diff --git a/App/apps/aprsrx/test/flipper/aprs_long.sub b/App/apps/aprsrx/test/flipper/aprs_long.sub new file mode 100644 index 00000000..54350670 --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_long.sub @@ -0,0 +1,18 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 +RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 +RAW_Data: 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 +RAW_Data: 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 +RAW_Data: 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228 +RAW_Data: 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 +RAW_Data: 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 +RAW_Data: 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228 +RAW_Data: 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 +RAW_Data: 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -417 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 +RAW_Data: 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -10139 diff --git a/App/apps/aprsrx/test/flipper/aprs_msg.sub b/App/apps/aprsrx/test/flipper/aprs_msg.sub new file mode 100644 index 00000000..f64c5984 --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_msg.sub @@ -0,0 +1,10 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 +RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 +RAW_Data: 416 -417 417 -416 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -417 416 -417 417 -353 227 -228 227 -353 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -10277 diff --git a/App/apps/aprsrx/test/flipper/aprs_pos.sub b/App/apps/aprsrx/test/flipper/aprs_pos.sub new file mode 100644 index 00000000..9295714d --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_pos.sub @@ -0,0 +1,11 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 291 -417 +RAW_Data: 353 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 +RAW_Data: 227 -353 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -417 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 353 -227 228 -227 227 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -416 417 -227 228 -227 227 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 416 -417 353 -228 227 -227 354 -416 417 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 354 -416 417 -417 416 -417 417 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -417 416 -417 417 -416 417 -353 228 -227 227 -354 416 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -291 227 -227 228 -416 417 -417 416 -417 417 -416 417 -417 +RAW_Data: 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 353 -228 227 -227 10076 diff --git a/App/apps/aprsrx/test/flipper/aprs_status.sub b/App/apps/aprsrx/test/flipper/aprs_status.sub new file mode 100644 index 00000000..a11936b7 --- /dev/null +++ b/App/apps/aprsrx/test/flipper/aprs_status.sub @@ -0,0 +1,10 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433650000 +Preset: FuriHalSubGhzPreset2FSKDev238Async +Protocol: RAW +RAW_Data: 100227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 +RAW_Data: 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -291 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -417 417 -353 227 -228 227 -353 417 -417 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 353 -228 227 -227 228 -227 227 -227 228 -227 227 -291 416 -354 227 -227 228 -353 417 -290 227 -228 227 -417 416 -228 227 -227 291 -416 417 -417 353 -227 228 -227 353 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 227 -227 228 -227 353 -417 291 -227 227 -227 417 -417 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -354 416 -291 227 -227 228 -416 417 -417 416 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 227 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 228 -227 +RAW_Data: 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 291 -417 416 -417 417 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 417 -416 417 -417 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -417 417 -290 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 353 -417 416 -417 417 -416 417 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 353 -417 416 -417 417 -416 291 -227 227 -228 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 417 -417 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -228 227 -227 291 -416 354 -227 227 -228 353 -417 290 -227 228 -227 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -417 417 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -291 417 -353 227 -228 227 -227 227 -228 227 -417 416 -417 417 -416 417 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -290 227 -228 227 -227 228 -227 290 -417 353 -228 227 -227 354 -416 291 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -353 417 -291 227 -227 227 -228 227 -227 227 -228 227 -227 354 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -417 416 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -228 227 -227 291 -416 417 -417 416 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -290 417 -353 228 -227 227 -228 227 -227 227 -228 227 -227 291 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -227 228 -227 290 -417 417 -416 354 -227 227 -228 227 -227 227 -228 416 -417 227 -228 227 -227 227 -228 227 -227 227 -228 227 -417 416 -417 417 -227 227 -227 +RAW_Data: 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -291 416 -417 417 -416 417 -417 416 -354 227 -227 228 -227 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 416 -417 291 -227 227 -227 417 -417 227 -227 227 -291 417 -416 417 -417 416 -354 227 -227 228 -353 417 -416 417 -417 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 227 -227 227 -228 416 -417 417 -416 228 -227 227 -227 228 -227 227 -354 416 -291 227 -227 228 -227 227 -227 228 -227 227 -228 353 -417 290 -227 228 -227 417 -416 228 -227 227 -291 416 -354 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 353 -228 227 -227 228 -227 227 -227 417 -417 416 -417 417 -416 417 -417 227 -227 227 -228 227 -227 228 -353 417 -416 417 -291 227 -227 227 -417 417 -416 417 -417 416 -228 227 -227 227 -228 227 -227 228 -227 227 -227 417 -417 227 -227 227 -228 227 -227 228 -227 227 -227 228 -227 227 -227 291 -417 353 -227 228 -227 353 -417 417 -416 291 -227 227 -228 227 -227 227 -228 227 -227 228 -227 227 -227 228 -416 417 -227 228 -227 290 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 290 -227 228 -227 417 -416 417 -417 227 -227 227 -291 417 -416 417 -353 228 -227 227 -228 227 -227 227 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 417 -227 227 -227 291 -417 416 -417 417 -416 417 -417 416 -417 417 -416 417 -417 353 -227 228 -227 353 -417 417 -416 417 -417 416 -417 417 -416 417 -417 416 -417 291 -227 227 -227 10000 diff --git a/App/apps/aprsrx/test/flipper_aprs.py b/App/apps/aprsrx/test/flipper_aprs.py new file mode 100644 index 00000000..3fd12666 --- /dev/null +++ b/App/apps/aprsrx/test/flipper_aprs.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Flipper Zero test transmitter for the APRS RX app: write Sub-GHz RAW files +(.sub) that play an APRS frame (AX.25, Bell 202 AFSK 1200 bauds) on 433.650 MHz +(433 MHz SRD band, the same bench channel as EPIRB 406). + +The Flipper's CC1101 cannot frequency-modulate with an audio tone, only switch +between two frequencies (preset 2FSKDev238Async, +/-2.38 kHz). So each AFSK +tone is sent as a square wave: the carrier toggles at twice the tone frequency +(1200 Hz mark, 2200 Hz space), phase-continuous across bit boundaries, and the +receiver's discriminator outputs that square wave; its audio low-pass leaves +mostly the fundamental. The RAW durations are the square wave's half-periods, +rounded from exact edge times so the timing error never accumulates. + + flipper_aprs.py [--call F4HWN] [--freq 433650000] + -> aprs_pos.sub, _digi, _msg, _status, _long, _badfcs (see ax25.test_frames) +One frame per file: press Send once, wait ~1 s before the next one. +""" +import argparse, os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from ax25 import test_frames, hdlc_bits, decode + +PRESET = "FuriHalSubGhzPreset2FSKDev238Async" +BAUD = 1200 +MARK, SPACE = 1200.0, 2200.0 +LEAD_US = 100000 # unmodulated carrier before the flags (squelch / RSSI trigger) +TAIL_US = 10000 # carrier kept after the last flag +PER_LINE = 512 # values per RAW_Data line, as the Flipper writes them + + +def nrzi_tones(bits): + """Bell 202 NRZI: a 0 changes the tone, a 1 keeps it. Starts on mark.""" + f, out = MARK, [] + for b in bits: + if not b: + f = SPACE if f == MARK else MARK + out.append(f) + return out + + +def edges(tones, lead_us=LEAD_US, tail_us=TAIL_US): + """Exact edge times (us) of the square wave; level is high at t=0. + Returns (edge_times, end_time).""" + t0 = float(lead_us) # lead carrier holds the high level + ph = 0.0 # tone phase in cycles (high while frac < 0.5) + tb = 1e6 / BAUD + ed = [] + for i, f in enumerate(tones): + start, end = t0 + i * tb, t0 + (i + 1) * tb + t = start + while True: + nxt = (int(ph * 2) + 1) / 2.0 # next half-cycle boundary + dt = (nxt - ph) / f * 1e6 + if t + dt > end: + ph += (end - t) * f / 1e6 + break + t += dt + ph = nxt + ed.append(t) + return ed, t0 + len(tones) * tb + tail_us + + +def raw_durations(ed, end): + """Signed RAW durations: positive = high tone, negative = low tone.""" + pts = [0] + [round(t) for t in ed] + [round(end)] + out, lv = [], 1 + for a, b in zip(pts, pts[1:]): + out.append((b - a) if lv else -(b - a)) + lv ^= 1 + return out + + +def sub_durations(frame): + ed, end = edges(nrzi_tones(hdlc_bits(frame))) + return raw_durations(ed, end) + + +def write_sub(path, frame, freq): + d = sub_durations(frame) + lines = ["Filetype: Flipper SubGhz RAW File", "Version: 1", + "Frequency: %d" % freq, "Preset: %s" % PRESET, "Protocol: RAW"] + for i in range(0, len(d), PER_LINE): + lines.append("RAW_Data: " + " ".join(str(x) for x in d[i:i + PER_LINE])) + with open(path, "w") as f: + f.write("\n".join(lines) + "\n") + return d + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("out", nargs="?", default=".") + ap.add_argument("--call", default="F4HWN") + ap.add_argument("--freq", type=int, default=433650000) + a = ap.parse_args() + if 144000000 <= a.freq <= 146000000: + sys.exit("refusing 2 m: the Flipper cannot reach it anyway, and 144.800 is live APRS") + os.makedirs(a.out, exist_ok=True) + for name, fr in test_frames(a.call).items(): + p = os.path.join(a.out, "aprs_%s.sub" % name) + d = write_sub(p, fr, a.freq) + print("%-28s %5.0f ms %5d edges %s" % (p, sum(abs(x) for x in d) / 1000, + len(d), decode(fr) or "(bad FCS)")) diff --git a/App/apps/aprsrx/test/mice.py b/App/apps/aprsrx/test/mice.py new file mode 100644 index 00000000..43bdb36e --- /dev/null +++ b/App/apps/aprsrx/test/mice.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Mic-E: an encoder written from the APRS 1.0.1 spec (chapter 10), and the +display decoder of aprsrx_app.c transliterated byte for byte (uint8 wrap, no +division), checked against each other and against a real FT3D frame. +Also the standard (uncompressed) position display, against real frames. + + mice.py runs the tests and prints the screen rows +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +MSG = ["Emergency", "Priority", "Special", "Committed", + "Returning", "In Service", "En Route", "Off Duty"] # index = bits A B C + + +# ------------------------------------------------------------ encoder (spec) -- + +def encode(call, lat, lon, speed_kn=0, course=0, sym="[/", msg=7, custom=False, + comment="", path=(), marker="`", suffix="_0", alt_m=None): + """lat/lon in signed decimal degrees -> AX.25 frame bytes.""" + north, east = lat >= 0, lon >= 0 + lat, lon = abs(lat), abs(lon) + ld = int(lat); lm = (lat - ld) * 60 + lmi = int(lm); lh = int(round((lm - lmi) * 100)) + if lh == 100: lmi, lh = lmi + 1, 0 + digits = "%02d%02d%02d" % (ld, lmi, lh) + od = int(lon); om = (lon - od) * 60 + omi = int(om); oh = int(round((om - omi) * 100)) + if oh == 100: omi, oh = omi + 1, 0 + offset = od < 10 or od >= 100 + bits = [(msg >> 2) & 1, (msg >> 1) & 1, msg & 1] + dest = "" + for k, ch in enumerate(digits): + d = int(ch) + if k < 3: + one = bits[k] + dest += chr((ord("A") if custom else ord("P")) + d) if one else chr(ord("0") + d) + elif k == 3: + dest += chr(ord("P") + d) if north else chr(ord("0") + d) + elif k == 4: + dest += chr(ord("P") + d) if offset else chr(ord("0") + d) + else: + dest += chr(ord("P") + d) if not east else chr(ord("0") + d) + if od <= 9: b1 = od + 118 + elif od <= 99: b1 = od + 28 + elif od <= 109: b1 = od + 8 + else: b1 = od - 72 + b2 = omi + 88 if omi <= 9 else omi + 28 + b3 = oh + 28 + b4 = speed_kn // 10 + 28 + b5 = (speed_kn % 10) * 10 + course // 100 + 28 + b6 = course % 100 + 28 + info = bytes([0x60, b1, b2, b3, b4, b5, b6]) + sym.encode() + body = comment + if alt_m is not None: + v = alt_m + 10000 + body = chr(v // 8281 + 33) + chr(v // 91 % 91 + 33) + chr(v % 91 + 33) + "}" + body + if marker: + body = marker + body + suffix + return _frame(call, dest, path, info + body.encode("latin-1")) + + +def _frame(call, dest, path, info): + from ax25 import addr, crc16 + hops = list(path) + f = addr(dest) + addr(call, last=not hops) + for i, h in enumerate(hops): + f += addr(h, last=i == len(hops) - 1) + f += b"\x03\xF0" + info + c = crc16(f) + return f + bytes([c & 0xFF, c >> 8]) + + +# ------------------------------------------- decoder (mirror of aprsrx_app.c) -- + +def sub(v, d): + q = 0 + while v >= d: + v -= d; q += 1 + return q, v + +def put2(v): + q, v = sub(v, 10) + return chr(48 + q) + chr(48 + v) + +def put3(v): + q, v = sub(v, 100) + return chr(48 + q) + put2(v) + +def mic_digit(c): + if 0x30 <= c <= 0x39: return c - 0x30 + if 0x41 <= c <= 0x4A: return c - 0x41 + if 0x50 <= c <= 0x59: return c - 0x50 + return 0 # K, L, Z: position ambiguity + +def mic_bit(c): + return c >= 0x50 or (0x41 <= c <= 0x4B) + +def text_row(o, f, i, end): + while len(o) < 32 and i < end: + ch = f[i] + o += "." if ch < 0x20 or ch > 0x7E else chr(ch) + i += 1 + return o, i + +def is_mice(f, i, end): + return end - i >= 9 and f[i] in (0x60, 0x27, 0x1C, 0x1D) + +def draw_mice(f, i, end): + c = [f[k] >> 1 for k in range(6)] + # row 1: position + o = "" + for k in range(6): + o += chr(48 + mic_digit(c[k])) + if k == 1: o += " " + if k == 3: o += "." + o += "N" if mic_bit(c[3]) else "S" + deg = (f[i + 1] - 28) & 0xFF + if mic_bit(c[4]): deg += 100 + if 180 <= deg <= 189: deg -= 80 + elif 190 <= deg <= 199: deg -= 190 + mn = (f[i + 2] - 28) & 0xFF + if mn >= 60: mn -= 60 + o += " " + put3(deg) + " " + put2(mn) + "." + put2((f[i + 3] - 28) & 0xFF) + o += "W" if mic_bit(c[5]) else "E" + r1 = o + # row 2: speed, course, symbol, message + dc = (f[i + 5] - 28) & 0xFF + q, dc = sub(dc, 10) + sp = ((f[i + 4] - 28) & 0xFF) * 10 + q + crs = dc * 100 + ((f[i + 6] - 28) & 0xFF) + if sp >= 800: sp -= 800 + if crs >= 400: crs -= 400 + kmh = (sp * 1897) >> 10 + o = "%dkm/h %d " % (kmh, crs) + chr(f[i + 7]) + chr(f[i + 8]) + " " + idx = mic_bit(c[0]) * 4 + mic_bit(c[1]) * 2 + mic_bit(c[2]) + custom = any(0x41 <= x <= 0x4B for x in c[:3]) + o += ("Custom-" + chr(48 + 7 - idx)) if custom and idx else MSG[idx] + r2 = o + # row 3: comment without the device markers, altitude first + j = i + 9 + while end > j and f[end - 1] in (0x0D, 0x0A): end -= 1 + if j < end: + m = f[j] + if m in (0x60, 0x27): + j += 1 + if end - j >= 2: end -= 2 + elif m in (0x3E, 0x5D): + j += 1 + if end > j and f[end - 1] in (0x3D, 0x5E): end -= 1 + o = "" + if end - j >= 4 and f[j + 3] == 0x7D: + alt = (((f[j] - 33) * 91 + (f[j + 1] - 33)) * 91 + (f[j + 2] - 33)) - 10000 + o = "%dm " % alt + j += 4 + r3, _ = text_row(o, f, j, end) + return r1, r2, r3 + + +def safe(ch): + return "." if ch < 0x20 or ch > 0x7E else chr(ch) + +def draw_pos(f, i, end): + """Uncompressed position ('!' '=' or, after a 7-char timestamp, '/' '@'): + None when the info field is anything else (shown as raw text).""" + t = f[i] + p = i + 1 + if t in (0x2F, 0x40): p += 7 + elif t not in (0x21, 0x3D): return None + if end < p + 19 or f[p + 4] != 0x2E or f[p + 14] != 0x2E: return None + q = f[p:] + o = "" + for k in range(18): + if k == 8: continue + if k in (2, 9, 12): o += " " + o += safe(q[k]) + r1 = o + o = "" + if p > i + 1: + o += "".join(safe(f[k]) for k in range(i + 1, p)) + " " + o += safe(q[8]) + safe(q[18]) + " " + r2, j = text_row(o, f, p + 19, end) + r3, _ = text_row("", f, j, end) + return r1, r2, r3 + + +def info_start(f): + end = len(f) - 2 + e = 13 + while not (f[e] & 1) and e + 7 < end: + e += 7 + return e + 3, end + + +# ------------------------------------------------------------------- tests -- + +def main(): + ok = True + # the FT3D frame received on 2026-09-30 (screen: >TXUPX9, `x,5l .[/`_0.) + ft3d = _frame("F4HWN", "TXUPX9", (), b"`x,5l \x1c[/`_0\r") + i, end = info_start(ft3d) + rows = draw_mice(ft3d, i, end) + print("FT3D ", rows) + ok &= rows == ("48 50.89N 002 16.25E", "0km/h 0 [/ Off Duty", "") + + cases = [ + # lat, lon, speed kn, course, msg, custom, alt, comment -> expected rows + (48.848306, 2.270844, 0, 0, 7, False, None, "", "48 50.90N 002 16.25E"), + (-33.8688, 151.2093, 25, 271, 6, False, 58, "Hello", "33 52.13S 151 12.56E"), + (40.7128, -74.0060, 5, 90, 0, False, None, "", "40 42.77N 074 00.36W"), + (51.5, -0.1276, 799, 359, 3, True, -12, "x", "51 30.00N 000 07.66W"), + (10.0, 105.5, 12, 0, 5, False, 1234, "", "10 00.00N 105 30.00E"), + (1.0, -179.99, 100, 180, 1, False, None, "", "01 00.00N 179 59.40W"), + ] + for lat, lon, sp, crs, msg, cu, alt, com, want in cases: + f = encode("F4HWN-7", lat, lon, sp, crs, "[/", msg, cu, com, alt_m=alt) + i, end = info_start(f) + r1, r2, r3 = draw_mice(f, i, end) + kmh = (sp * 1897) >> 10 + m = ("Custom-%d" % (7 - msg)) if cu and msg else MSG[msg] + w2 = "%dkm/h %d [/ %s" % (kmh, crs, m) + w3 = ("%dm " % alt if alt is not None else "") + com + good = (r1, r2, r3) == (want, w2, w3) and all(len(r) <= 32 for r in (r1, r2, r3)) + ok &= good + print("OK " if good else "FAIL", (r1, r2, r3), "" if good else (want, w2, w3)) + # standard positions (aprsrx_app.c drawPos) + pos_cases = [ + ("F1PRY-14", "@300158z4921.07N/00150.50EN APRS OISE V=12.5V", + ("49 21.07N 001 50.50E", "300158z /N APRS OISE V=12.5V", "")), + ("F4HWN", "!4850.90N/00216.25E-UV-K5 APRS RX test", + ("48 50.90N 002 16.25E", "/- UV-K5 APRS RX test", "")), + ("F4HWN", "=4850.90S\\12345.67W>" + "x" * 60, + ("48 50.90S 123 45.67W", "\\> " + "x" * 29, "x" * 31)), + ("F4HWN", "!/5L!!<*e7>7P[", None), # compressed: raw text + ("F4HWN", "!4850.90N/00216.25", None), # too short + ("F4HWN", ">status text", None), + ] + for call, info, want in pos_cases: + f = _frame(call, "APZK5", (), info.encode("latin-1")) + i, end = info_start(f) + got = draw_pos(f, i, end) + good = got == want and (got is None or all(len(r) <= 32 for r in got)) + ok &= good + print("OK " if good else "FAIL", got, "" if good else want) + print("ALL OK" if ok else "FAILURES") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/App/apps/aprsrx/test/model_rx.py b/App/apps/aprsrx/test/model_rx.py new file mode 100644 index 00000000..6e6cfd20 --- /dev/null +++ b/App/apps/aprsrx/test/model_rx.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""Pure-Python model of the APRS receive chain, and the integer demodulator the +radio app will run (written to be transliterated to C: ints, shifts, no division). + +Chain: source (Flipper square-wave FSK from the .sub durations, or a real +station's sine AFSK with a given twist) -> discriminator output in Hz + offset + +white noise -> radio audio path (RAW: 5 kHz low-pass as in EPIRB 406; STD: 300 Hz +high-pass, 750 us de-emphasis, 3 kHz low-pass) -> 12-bit ADC on PA4 around 2048 at +9.6 kHz (optional clock error) -> Demod -> frames. + +Demod (per ADC sample): + DC removal -> band-pass -> 4 sliding correlators over one bit (8 samples): + I/Q at 1200 and 2200 Hz -> magnitudes (max + 3/8 min) -> per-tone peak AGC + -> 3 slicers, decision sign(wa*Mm*Ps - wb*Ms*Pm) with wa:wb = 2:3, 1:1, 3:2, + each with its own DPLL (16-bit phase per bit, nudged 1/4 toward mid-bit on + each transition), NRZI, HDLC (flag, destuff, abort), FCS and UI check. + + model_rx.py runs the test sweep, prints a table +""" +import math, os, random, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from ax25 import test_frames, hdlc_bits, crc16, decode +from flipper_aprs import nrzi_tones, edges + +FS_SIM = 96000 +FS_ADC = 9600 +BIAS = 2048 +LSB_PER_HZ = 0.065 # measured on the K1 (EPIRB 406 lab): ~2.3 kHz -> ~150 LSB +FLIPPER_DEV = 2380.0 + + +# ---------------------------------------------------------------- channel ---- + +def flipper_wave(frame): + """Discriminator output (Hz) of the Flipper square-wave FSK, box-averaged + per simulation sample from the exact (rounded to us, as in the .sub) edges.""" + ed, end = edges(nrzi_tones(hdlc_bits(frame))) + ed = [float(round(t)) for t in ed] + n = int(end * 1e-6 * FS_SIM) + T = 1e6 / FS_SIM + out, lv, k = [], 1, 0 + for i in range(n): + t0, t1 = i * T, (i + 1) * T + acc, t = 0.0, t0 + while k < len(ed) and ed[k] < t1: + acc += (ed[k] - t) * (1 if lv else -1) + t = ed[k] + lv ^= 1 + k += 1 + acc += (t1 - t) * (1 if lv else -1) + out.append(FLIPPER_DEV * acc / T) + return out + + +def sine_wave(frame, dev=3000.0, twist_db=0.0): + """A real station: phase-continuous sine AFSK, space tone twist_db louder.""" + tones = nrzi_tones(hdlc_bits(frame)) + spb = FS_SIM / 1200.0 + out, ph = [], 0.0 + lead = [0.0] * int(0.1 * FS_SIM) + a_space = dev * 10 ** (twist_db / 20) + for i in range(int(len(tones) * spb)): + f = tones[int(i / spb)] + ph += f / FS_SIM + out.append((dev if f == 1200.0 else a_space) * math.sin(2 * math.pi * ph)) + return lead + out + + +def biquad_lp(x, fc): + w = math.tan(math.pi * fc / FS_SIM) + q = 1 / math.sqrt(2) + n = 1 / (1 + w / q + w * w) + b0 = w * w * n; b1 = 2 * b0; a1 = 2 * (w * w - 1) * n; a2 = (1 - w / q + w * w) * n + y, x1, x2, y1, y2 = [], 0.0, 0.0, 0.0, 0.0 + for v in x: + o = b0 * v + b1 * x1 + b0 * x2 - a1 * y1 - a2 * y2 + x2, x1, y2, y1 = x1, v, y1, o + y.append(o) + return y + + +def rc_hp(x, fc): + a = math.exp(-2 * math.pi * fc / FS_SIM) + y, acc, prev = [], 0.0, 0.0 + for v in x: + acc = a * (acc + v - prev) + prev = v + y.append(acc) + return y + + +def deemph(x, tau_us=750.0, ref_hz=1200.0): + """1-pole de-emphasis, unity gain at ref_hz.""" + k = 1 - math.exp(-1 / (FS_SIM * tau_us * 1e-6)) + g = math.sqrt(1 + (2 * math.pi * ref_hz * tau_us * 1e-6) ** 2) + y, acc = [], 0.0 + for v in x: + acc += k * (v - acc) + y.append(acc * g) + return y + + +def channel(sig, mode="raw", noise_hz=0.0, off_hz=0.0, ppm=0.0, seed=1, + gap_ms=150): + rnd = random.Random(seed) + gap = int(gap_ms * 1e-3 * FS_SIM) + # no carrier around the burst: the discriminator outputs full-scale noise + x = [rnd.gauss(0, 6000) for _ in range(gap)] + x += [v + off_hz + rnd.gauss(0, noise_hz) for v in sig] + x += [rnd.gauss(0, 6000) for _ in range(gap)] + if mode == "raw": + x = rc_hp(biquad_lp(x, 5000), 30) + else: + x = biquad_lp(deemph(rc_hp(x, 300)), 3000) + step = FS_SIM / FS_ADC * (1 + ppm * 1e-6) + out, p = [], 0.0 + while p < len(x) - 1: + i = int(p); f = p - i + v = x[i] * (1 - f) + x[i + 1] * f + out.append(min(4095, max(0, int(round(BIAS + LSB_PER_HZ * v + rnd.gauss(0, 1)))))) + p += step + return out + + +# ------------------------------------------------------ integer demodulator -- + +def _tab(f, fs): + per = fs // math.gcd(fs, int(f)) + c = [int(round(127 * math.cos(2 * math.pi * f * n / fs))) for n in range(per)] + s = [int(round(127 * math.sin(2 * math.pi * f * n / fs))) for n in range(per)] + return c, s + + +def is_ui(f): + """An APRS frame is a UI frame: the address field ends (bit 0 of an SSID + byte) and is followed by control 0x03 and PID 0xF0. Rejects the rare noise + burst that passes the 16-bit FCS in continuous decoding.""" + end = len(f) - 2 + e = 13 + while not (f[e] & 1) and e + 7 < end: + e += 7 + return bool(f[e] & 1) and e + 2 < end and f[e + 1] == 0x03 and f[e + 2] == 0xF0 + + +class Slicer: + """One decision threshold + its own DPLL and HDLC. The decision is + sign(wa*Mm*Ps - wb*Ms*Pm): wa/wb != 1 favours one tone, which recovers + frames whose twist the per-tone AGC does not fully cancel (Direwolf's + multi-slicer idea).""" + def __init__(self, dm, wa, wb): + self.dm, self.wa, self.wb = dm, wa, wb + self.dprev = 0; self.phase = 0; self.last = 0 + self.sr = 0; self.ones = 0; self.byte = 0; self.nb = 0 + self.buf = bytearray(); self.inframe = False + self.good = 0 + + def step(self, a, b): + dm = self.dm + d = a * self.wa - b * self.wb + # DPLL: sample at phase wrap (mid-bit), transitions pulled to mid-phase + self.phase += dm.step + if (d > 0) != (self.dprev > 0): + self.phase += (dm.ctr - self.phase) >> dm.pll + self.dprev = d + if self.phase >= 65536: + self.phase -= 65536 + lv = 1 if d > 0 else 0 + self.bit(1 if lv == self.last else 0) + self.last = lv + + def bit(self, b): + self.sr = (self.sr >> 1) | (b << 7) + if self.sr == 0x7E: + if self.inframe and len(self.buf) >= 18 and \ + crc16(self.buf[:-2]) == self.buf[-2] | (self.buf[-1] << 8) and \ + is_ui(self.buf): + self.good += 1 + self.dm.frame(bytes(self.buf)) + self.buf = bytearray(); self.inframe = True + self.ones = self.byte = self.nb = 0 + return + if b: + self.ones += 1 + if self.ones > 6: # abort / noise: wait for a flag + self.inframe = False + return + else: + if self.ones == 5: # stuffed zero + self.ones = 0 + return + self.ones = 0 + if not self.inframe: + return + self.byte |= b << self.nb + self.nb += 1 + if self.nb == 8: + if len(self.buf) < 330: + self.buf.append(self.byte) + else: + self.inframe = False + self.byte = self.nb = 0 + + +class Demod: + def __init__(self, fs=FS_ADC, agc=True, bp=True, att=5, dec=10, pll=2, ctr=36864, + slicers=((2, 3), (1, 1), (3, 2))): + self.ctr, self.bp, self.att, self.dec, self.pll = ctr, bp, att, dec, pll + # 2nd-order band-pass (RBJ, constant 0 dB peak) centred on + # sqrt(1200*2200) = 1625 Hz, Q = 0.9: coefficients in Q14 + w0 = 2 * math.pi * 1625 / fs + al = math.sin(w0) / (2 * 0.9) + a0 = 1 + al + self.b0 = int(round(al / a0 * 16384)) + self.a1 = int(round(-2 * math.cos(w0) / a0 * 16384)) + self.a2 = int(round((1 - al) / a0 * 16384)) + self.x1 = self.x2 = self.y1 = self.y2 = 0 + self.N = fs // 1200 # correlation window = one bit + self.cm, _ = _tab(1200, fs) # 8 entries at 9.6 kHz + self.cs, _ = _tab(2200, fs) # 48 entries at 9.6 kHz + self.im = self.qm = self.is_ = self.qs = 0 + self.ring = [[0, 0, 0, 0] for _ in range(self.N)] + self.r = 0; self.km = 0; self.ks = 0 + self.dc = BIAS << 4 + self.pm = self.ps = 64 + self.agc = agc + self.step = (65536 * 1200 + fs // 2) // fs + self.sl = [Slicer(self, wa, wb) for wa, wb in slicers] + self.frames = [] + self.maxab = 0 + + def frame(self, f): + if f not in self.frames: # the same frame from several slicers + self.frames.append(f) + + @staticmethod + def mag(i, q): + a, b = abs(i), abs(q) + if a < b: a, b = b, a + return a + (b >> 2) + (b >> 3) + + def sample(self, adc): + self.dc += ((adc << 4) - self.dc) >> 6 + x = adc - (self.dc >> 4) + if self.bp: + y = (self.b0 * (x - self.x2) - self.a1 * self.y1 - self.a2 * self.y2) >> 14 + self.x2, self.x1, self.y2, self.y1 = self.x1, x, self.y1, y + x = y + # sin = cos read 3/4 period ahead: +6 of 8 (1200 Hz), +12 of 48 (2200 Hz: + # 11 cycles per 48 samples, 12 samples = 2.75 cycles) + cm, cs = self.cm, self.cs + p = ((x * cm[self.km]) >> 8, (x * cm[(self.km + 6) & 7]) >> 8, + (x * cs[self.ks]) >> 8, (x * cs[(self.ks + 12) % 48]) >> 8) + o = self.ring[self.r] + self.im += p[0] - o[0]; self.qm += p[1] - o[1] + self.is_ += p[2] - o[2]; self.qs += p[3] - o[3] + self.ring[self.r] = p + self.r = (self.r + 1) % self.N + self.km = (self.km + 1) % len(self.cm) + self.ks = (self.ks + 1) % len(self.cs) + mm, ms = self.mag(self.im, self.qm), self.mag(self.is_, self.qs) + if self.agc: + self.pm += (mm - self.pm) >> self.att if mm > self.pm else -(self.pm >> self.dec) + self.ps += (ms - self.ps) >> self.att if ms > self.ps else -(self.ps >> self.dec) + if self.pm < 16: self.pm = 16 + if self.ps < 16: self.ps = 16 + a, b = mm * self.ps, ms * self.pm + else: + a, b = mm, ms + if a > self.maxab: self.maxab = a + if b > self.maxab: self.maxab = b + for s in self.sl: + s.step(a, b) + + +def run(adc, **kw): + d = Demod(**kw) + for s in adc: + d.sample(s) + return d.frames + + +# ------------------------------------------------------------------ sweep ---- + +def main(): + fr = test_frames() + waves = {} + def wave(name, src, twist=0.0): + key = (name, src, twist) + if key not in waves: + waves[key] = flipper_wave(fr[name]) if src == "flipper" \ + else sine_wave(fr[name], twist_db=twist) + return waves[key] + + cases = [] + for mode in ("raw", "std"): + for name in ("pos", "long", "badfcs"): + cases.append(("flipper", 0.0, mode, name, 0, 0, 0)) + for noise in (1500, 3000, 4500, 6000): + cases.append(("flipper", 0.0, mode, "pos", noise, 0, 0)) + for off in (-4000, 4000): + cases.append(("flipper", 0.0, mode, "pos", 1500, off, 0)) + for ppm in (-10000, 10000): + cases.append(("flipper", 0.0, mode, "long", 1500, 0, ppm)) + for tw in (-6.0, 0.0, 6.0): + cases.append(("sine", tw, mode, "pos", 1500, 0, 0)) + + print("%-7s %5s %-4s %-6s %6s %6s %7s | %-7s %-7s" % + ("source", "twist", "mode", "frame", "noise", "off", "ppm", "agc", "no-agc")) + for src, tw, mode, name, noise, off, ppm in cases: + oks = [] + for seed in (1, 2, 3): + adc = channel(wave(name, src, tw), mode, noise, off, ppm, seed) + for agc in (True, False): + got = run(adc, agc=agc) + want = [] if name == "badfcs" else [fr[name]] + oks.append((agc, got == want)) + res = ["%d/3" % sum(ok for a, ok in oks if a == agc) for agc in (True, False)] + print("%-7s %5.0f %-4s %-6s %6d %6d %7d | %-7s %-7s" % + (src, tw, mode, name, noise, off, ppm, res[0], res[1])) + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--one": + f = test_frames()["pos"] + got = run(channel(flipper_wave(f))) + print([decode(g) for g in got]) + else: + main() diff --git a/App/apps/aprsrx/test/variants.py b/App/apps/aprsrx/test/variants.py new file mode 100644 index 00000000..5c6d9ed0 --- /dev/null +++ b/App/apps/aprsrx/test/variants.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Compare demodulator variants on the hard cases of the sweep (dev tool). +Run from a file (multiprocessing re-imports __main__).""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from multiprocessing import Pool +from model_rx import * + +FR = test_frames() +CASES = [] +for mode in ("raw", "std"): + for noise in (3000, 4000): + CASES.append(("flipper", 0.0, mode, "pos", noise, 0, 0)) + CASES.append(("flipper", 0.0, mode, "long", 2000, 0, 10000)) + CASES.append(("flipper", 0.0, mode, "long", 2000, 0, -10000)) + for tw in (-9.0, -6.0, -3.0, 0.0, 3.0, 6.0, 9.0): + CASES.append(("sine", tw, mode, "pos", 2500, 0, 0)) + CASES.append(("flipper", 0.0, mode, "badfcs", 1000, 0, 0)) + +VARIANTS = { + "s1": dict(slicers=((1, 1),)), + "s3x2": dict(slicers=((1, 2), (1, 1), (2, 1))), + "s3x1.5": dict(slicers=((2, 3), (1, 1), (3, 2))), + "s5": dict(slicers=((1, 2), (2, 3), (1, 1), (3, 2), (2, 1))), + "s3x3": dict(slicers=((1, 3), (1, 1), (3, 1))), +} +SEEDS = (1, 2, 3, 4) + + +def job(c): + src, tw, mode, name, noise, off, ppm = c + w = flipper_wave(FR[name]) if src == "flipper" else sine_wave(FR[name], twist_db=tw) + row, mx = [], 0 + for seed in SEEDS: + adc = channel(w, mode, noise, off, ppm, seed) + want = [] if name == "badfcs" else [FR[name]] + r = [] + for kw in VARIANTS.values(): + d = Demod(**kw) + for s in adc: + d.sample(s) + r.append(d.frames == want) + mx = max(mx, d.maxab) + row.append(r) + return c, [sum(r[i] for r in row) for i in range(len(VARIANTS))], mx + + +if __name__ == "__main__": + with Pool() as p: + res = p.map(job, CASES) + names = list(VARIANTS) + print("%-40s " % "case" + " ".join("%6s" % n for n in names)) + tot = [0] * len(names) + mx = 0 + for c, r, m in res: + print("%-40s " % (" ".join(str(x) for x in c)) + " ".join("%6d" % x for x in r)) + tot = [a + b for a, b in zip(tot, r)] + mx = max(mx, m) + print("%-40s " % ("TOTAL /%d" % (len(CASES) * len(SEEDS))) + " ".join("%6d" % x for x in tot)) + print("max(Mm*Ps, Ms*Pm) = %d (x3 = %d, int32 max 2147483647)" % (mx, 3 * mx)) diff --git a/App/apps/aprstx/README.md b/App/apps/aprstx/README.md new file mode 100644 index 00000000..82d5cfd2 --- /dev/null +++ b/App/apps/aprstx/README.md @@ -0,0 +1,100 @@ +# APRS TX: on-radio APRS position beacon (work in progress) + +Sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK 1200 bauds) on the +TX VFO at each press of PTT or MENU. Companion of [APRS RX](../aprsrx/README.md), +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) | +| 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) + +| Setting | Default | +|---|---| +| Source | the boot-message callsign (API `boot_callsign`, 1-6 letters/digits) + `SSID` = 7 | +| `DEST` | `APZK5` (APZ = experimental software) | +| `PATH` | `WIDE1-1` (0 to 2 entries) | +| `LAT`, `LON` | `4850.90N`, `00216.25E`: the **default** position, until one is edited on the radio (key 5) | +| `SYMBOL` | `/[` (person) | +| `COMMENT` | `UV-K5 APRS TX` (43 characters at most) | + +Frame sent: `F4HWN-7>APZK5,WIDE1-1:!4850.90N/00216.25E[UV-K5 APRS TX` (58 bytes, +~0.73 s on the air: 50 ms of tone settle, 40 flags = 267 ms, the frame, 3 flags). +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. + +## Keys (UV-K5 and UV-K1) + +| Key | Action | +|---|---| +| PTT or MENU | Send one frame | +| UP / DOWN | Tone level (deviation): REG_70 gain 10-127, step 4, default 66 (the firmware's tone gain) | +| 1 / 3 | Twist `tw` -4..+8: 2200 Hz gain = level × (8 + tw) / 8 (-6..+6 dB) | +| 5 | Edit the position | +| EXIT | Quit (level, twist and position are saved) | + +### Position editor (v0.2) + +``` + +LAT 48 50.90 N + ^ +LON 002 16.25 E +0-9 digit * N/S E/W +UP/DN move MENU ok EXIT +``` + +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 goes on from +the latitude to the longitude. + +| Key | Action | +|---|---| +| 0-9 | Digit at the cursor, then the next one | +| * | N/S (on the latitude) or E/W (on the longitude) | +| UP / DOWN | Move the cursor (to fix one digit) | +| MENU | Check (degrees ≤ 90 / 180, minutes < 60) and keep: the frame is rebuilt; `Invalid position` otherwise | +| EXIT | Cancel | + +The position is saved on exit with the level and twist (`cfg_save`, 10 bytes: +magic 0xA8, level, twist, 13 digits and the hemispheres in nibbles). A v0.1 +config (magic 0xA7) is ignored: defaults. + +## How it transmits + +As the Beacon app keys MCW: `tx_set_params` (carrier + PA on the TX VFO), +REG_51 = 0 (no CTCSS/DCS under the AFSK), `tx_tone(1200)` (tone path on, mic ADC +off, 50 ms settle). Then each bit is timed from SysTick, 40000 cycles at 48 MHz +(exact), and at each NRZI transition the tone frequency REG_71 (1200 Hz = 12389, +2200 Hz = 22714, the driver's `scale_freq`) and gain REG_70 are rewritten: two +SPI writes, a few tens of µs out of 833. `tx_mute` + `tx_end` restore RX. + +## Unknowns for the first on-air test (all fine with the defaults on 2026-09-30) + +1. **Phase continuity** when REG_71 is rewritten (an NCO keeps it, probably). + The RX model decodes even with a phase reset at every transition (3/3 clean, + 2/3 with noise in STD); a hardware TNC such as the FT3D may be stricter. +2. **Twist.** The tone probably enters after the pre-emphasis, so the signal is + flat and a receiver with de-emphasis sees 2200 Hz ~5 dB low. If the FT3D + misses frames that APRS RX decodes, raise `tw` (key 3): +4 is +3.5 dB. +3. **Deviation** at level 66: aim for ~3 kHz; UP/DOWN. + +## Test plan + +1. Simplex test frequency (not 144.800 for the first frames), low power. +2. Receivers: APRS RX on a second radio (its per-slicer counters `s a/b/c` + show the twist: frames only on the space-favouring slicer = 2200 Hz low) and + the FT3D (APRS on band B, same frequency). +3. PTT, check both; adjust level and twist; then 144.800. + +`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`, the config round trip, the editor +rows and caret, the position limits, then +AFSK (continuous phase or reset, tw 0/+4, TX path twist 0/+5 dB) decoded by the +RX model through its RAW and STD audio paths, with and without noise. diff --git a/App/apps/aprstx/app.ld b/App/apps/aprstx/app.ld new file mode 100644 index 00000000..f04a2ec7 --- /dev/null +++ b/App/apps/aprstx/app.ld @@ -0,0 +1,43 @@ +/* Overlay-app link script (POC). + * + * The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with + * the multiboot RAM stub — never both active at once). Code + rodata + data + + * bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+ + * data only; the loader zeroes the overlay before copying, so bss starts clean. + * + * VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM + * layout shifts, update APP_VMA and the loader's compile-time assert catches a + * mismatch. */ + +/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It + * varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x... + * The default is only a fallback for a standalone size check. */ +APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280; +APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */ + +ENTRY(app_main) + +MEMORY { + APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH +} + +SECTIONS { + .app APP_VMA : { + KEEP(*(.text.entry)) /* app_main pinned to offset 0 */ + *(.text .text.*) + *(.rodata .rodata.*) + . = ALIGN(4); + *(.data .data.*) + . = ALIGN(4); + __app_bss_start = .; + *(.bss .bss.* COMMON) + . = ALIGN(4); + __app_bss_end = .; + } > APP + + __app_end = .; + ASSERT(__app_end <= APP_VMA + APP_LENGTH, + "APRS TX overlay app overflows the 4 KiB overlay") + + /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) } +} diff --git a/App/apps/aprstx/aprstx_app.c b/App/apps/aprstx/aprstx_app.c new file mode 100644 index 00000000..c526c664 --- /dev/null +++ b/App/apps/aprstx/aprstx_app.c @@ -0,0 +1,431 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * APRS TX - sends one APRS position frame (AX.25 UI frame, Bell 202 AFSK + * 1200 bauds) on the TX VFO at each press of PTT or MENU. + * + * The source is the boot-message callsign (API boot_callsign) with the SSID of + * gen_assets.py; destination, path, symbol and comment are AX.25-encoded there + * too (assets). The position is edited on the radio (key 5: 13 digits typed in + * a row, as a frequency) and saved; gen_assets.py holds the default. The frame + * is built at launch and after each edit, FCS included. + * + * AFSK comes from the BK4829 tone generator, as the Beacon app keys MCW: the tone + * frequency (REG_71) is rewritten at each NRZI transition, 1200 Hz mark / 2200 Hz + * space, bits timed to the cycle (40000 cycles at 48 MHz) from SysTick. The + * mic ADC is off while a tone is sent; any CTCSS/DCS of the VFO is cleared + * (REG_51) so nothing rides under the AFSK. test/tx_model.py mirrors the frame + * and bit stream and decodes them with the APRS RX model. + * + * Keys (UV-K5 and UV-K1): PTT or MENU send · UP/DOWN tone level (deviation, + * REG_70 gain 10-127) · 1/3 twist: 2200 Hz gain = level x (8 + tw) / 8, + * tw -4..+8 (-6..+6 dB), for a receiver's de-emphasis · 5 edit the position + * · EXIT quit. + * Position editor: 0-9 type a digit (the cursor skips the separators and moves + * on) · * toggle N/S or E/W on the cursor's line · UP/DOWN move the cursor · + * MENU check and keep · EXIT cancel. + * Level, twist and position are saved on exit (cfg_save, 10 bytes). + */ + +#include +#include +#include +#include "../app_api.h" +#include "aprstx_assets.h" /* generated by gen_assets.py */ + +/* ---- SysTick (core at 48 MHz, 10 ms period) ---- */ +#define SYST_LOAD (*(volatile uint32_t *)0xE000E014u) +#define SYST_VAL (*(volatile uint32_t *)0xE000E018u) +#define CYC_PER_BIT (48000000u / 1200u) /* 40000: exact */ + +/* ---- BK4829 ---- */ +#define REG_51 0x51 /* sub-audio: 0 = no CTCSS/DCS (BK4819_ExitSubAu) */ +#define REG_70 0x70 /* tone 1: <15> enable, <14:8> gain */ +#define REG_71 0x71 /* tone 1 frequency, Hz x 10.32444 */ +#define F_MARK 12389u /* 1200 Hz: (f * 1353245 + 2^16) >> 17, scale_freq */ +#define F_SPACE 22714u /* 2200 Hz */ + +#define LVL_DEF 66 /* BK4819_TransmitTone's tone gain */ +#define LVL_MIN 10 +#define LVL_MAX 127 +#define LVL_STEP 4 +#define TW_MIN (-4) /* 2200 Hz gain x 0.5 (-6 dB) */ +#define TW_MAX 8 /* 2200 Hz gain x 2 (+6 dB) */ + +/* ---- AX.25 ---- */ +#define TXDELAY_FLAGS 40u /* 267 ms of flags before the frame */ +#define TAIL_FLAGS 3u +#define CALL_MAX 6u +#define INFO_LEN (1u + 8u + 1u + 9u + 1u + F_COMMENT_LEN) /* !lat/lon[comment */ +#define FRAME_MAX (7u + 7u + F_PATH_LEN + 2u + INFO_LEN + 2u) + +/* position: 13 digits DDMMhh DDDMMhh, hemispheres bit 0 south, bit 1 west */ +#define POS_DIGITS 13u +#define LAT_DIGITS 6u +#define TX_CAPS_X 40u /* "TRANSMIT" capsule in the status bar, after the title */ +#define CFG_MAGIC 0xA8u /* 0xA7 (v0.1: level and twist only) is ignored */ +#define CFG_LEN 10u /* magic, level, twist, 13 digits + hemispheres in nibbles */ + +enum { ST_IDLE = 0, ST_TX, ST_DENIED, ST_NOCALL, ST_BADPOS }; + +/* The app state in one struct: a function loads one base address instead of one + * literal-pool word per global. Words, then bytes. The loader zeroes the overlay + * (.bss) at every launch. */ +static struct { + const app_api_t *A; + uint8_t *frm; /* the frame (app_main's stack) */ + uint32_t tPrev, tCyc, next; /* SysTick cycle counter, next bit edge */ + uint16_t sent; /* frames sent */ + uint8_t flen, prevKey, space, ones, lvl, status; + int8_t tw; + bool running, edit; + uint8_t hemi, ehemi, cur; /* hemispheres, edited ones, cursor digit */ + uint8_t pos[POS_DIGITS]; /* the position sent */ + uint8_t ed[POS_DIGITS]; /* the position being edited */ +} g; +static char str[34]; + +/* ---- tiny freestanding helpers: GCC may turn small copies into calls ---- */ +void *memset(void *d, int c, size_t n) { + uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d; +} +void *memcpy(void *d, const void *s, size_t n) { + uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d; +} + +/* ---- formatting ---- */ +static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; } +/* Formatting by repeated subtraction: no division. Values printed stay below 100000. */ +static uint8_t sub(uint32_t *v,uint32_t d){ uint8_t q=0; while(*v>=d){ *v-=d; q++; } return q; } +static const uint16_t P10[]={10000u,1000u,100u,10u,1u}; +static char *puti(char *o,int32_t v){ + uint32_t u; + if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v; + bool lead=false; + for(uint8_t i=0;i<5;i++){ + uint8_t c=sub(&u,P10[i]); + if(c||lead||i==4u){ *o++=(char)('0'+c); lead=true; } + } + return o; +} +/* A tiny row holds 32 characters (4 px each): print_tiny does not clip, so + * every row built below stays within that. */ +static void tiny(uint8_t y,char *end){ *end='\0'; g.A->print_tiny(str,0,y,false,true); } +/* The 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. */ +static void drawFreq(uint32_t f){ + const app_api_t *A=g.A; + uint16_t m=0; + while(f>=100000u){ f-=100000u; m++; } + char *o=puti(str,m); + *o++='.'; + for(uint8_t i=0;i<5u;i++) *o++=(char)('0'+sub(&f,P10[i])); + *o='\0'; + uint8_t n=(uint8_t)(o-str), big=(uint8_t)(n-3u); /* digits before "00" */ + if(m>=1000u){ A->print_string(str,(uint8_t)((128u-n*8u)>>1),0,4,8); return; } + uint8_t x=(uint8_t)(((128u-(big*13u+14u))>>1)-2u); + A->print_normal(str+n-2u,(uint8_t)(x+big*13u+3u),0,5); /* "00" */ + str[n-2u]='\0'; + A->display_freq(str,x,4,false); /* "144.800" */ +} +/* AX.25 address field -> "F4HWN-7" */ +static char *putCall(char *o,const uint8_t *a){ + for(uint8_t i=0;i<6u;i++){ char c=(char)(a[i]>>1); if(c!=' ') *o++=c; } + uint8_t ssid=(uint8_t)((a[6]>>1)&15u); + if(ssid){ *o++='-'; o=puti(o,ssid); } + return o; +} + +/* ---- position ---- */ +/* "4850.90N" (6 digits) or "00216.25E" (7 digits), as APRS sends it */ +static uint8_t *putCoord(uint8_t *o,const uint8_t *d,uint8_t n,char h){ + for(uint8_t i=0;i9u) return false; + if(la>90u || lam>59u || lo>180u || lom>59u) return false; + if(la==90u && (lam|d[4]|d[5])) return false; + if(lo==180u && (lom|d[11]|d[12])) return false; + return true; +} + +/* ---- the frame: dest, source (boot callsign + SSID), path, UI, info, FCS ---- */ +static void build(void){ + const app_api_t *A=g.A; + uint8_t *f=g.frm; + char call[12]; + A->boot_callsign(call,sizeof call); + uint8_t n=0; + while(call[n]){ if(call[n]=='/') return; n++; } /* AX.25: letters and digits */ + if(!n || n>CALL_MAX) return; /* flen stays 0: no TX */ + + A->asset_read(F_DEST,f,F_DEST_LEN); + for(uint8_t i=0;i<6u;i++) f[7+i]=(uint8_t)((iasset_read(F_PATH,f+k,F_PATH_LEN); k+=F_PATH_LEN; + f[k++]=0x03; f[k++]=0xF0; /* UI frame, no layer 3 */ + uint8_t sym[2]; + A->asset_read(F_SYM,sym,2); + f[k++]='!'; /* position, no messaging */ + k=(uint8_t)(putCoord(f+k,g.pos,LAT_DIGITS,(g.hemi&1u)?'S':'N')-f); + f[k++]=sym[0]; /* symbol table */ + k=(uint8_t)(putCoord(f+k,g.pos+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.hemi&2u)?'W':'E')-f); + f[k++]=sym[1]; /* symbol code */ + A->asset_read(F_COMMENT,f+k,F_COMMENT_LEN); k+=F_COMMENT_LEN; + uint16_t c=0xFFFFu; /* CRC-16/X.25 */ + for(uint8_t i=0;i>1)^0x8408u):(uint16_t)(c>>1); + } + c^=0xFFFFu; + f[k++]=(uint8_t)c; f[k++]=(uint8_t)(c>>8); + g.flen=k; +} + +/* ---- SysTick cycle counter (call at least every 10 ms; compare with a signed + * difference) ---- */ +static void clkStart(void){ g.tPrev=SYST_VAL; g.tCyc=0; } +static uint32_t clkCyc(void){ + uint32_t v=SYST_VAL; + g.tCyc += (v<=g.tPrev) ? g.tPrev-v : g.tPrev+(SYST_LOAD+1u-v); + g.tPrev=v; + return g.tCyc; +} + +/* ---- AFSK ---- */ +static void setTone(void){ + const app_api_t *A=g.A; + uint32_t v=g.lvl; + if(g.space){ v=(v*(uint32_t)(8+g.tw))>>3; if(v>LVL_MAX) v=LVL_MAX; } + A->bk_write(REG_71,(uint16_t)(g.space?F_SPACE:F_MARK)); + A->bk_write(REG_70,(uint16_t)(0x8000u|(v<<8))); +} +/* one bit on the air: NRZI (a 0 changes the tone), from the next bit edge */ +static void sendBit(uint8_t b){ + while((int32_t)(clkCyc()-g.next)<0){} + if(!b){ g.space^=1u; setTone(); } + g.next+=CYC_PER_BIT; +} +/* one byte LSB first; inside the frame, a 0 is stuffed after five 1s */ +static void sendByte(uint8_t v,bool stuff){ + for(uint8_t i=0;i<8u;i++){ + uint8_t b=(uint8_t)(v&1u); + v>>=1; + sendBit(b); + if(stuff){ + if(!b) g.ones=0; + else if(++g.ones==5u){ sendBit(0); g.ones=0; } + } + } +} + +/* "LAT 48 50.90 N" on a normal-font line (7 px per character), and a '^' under + * the digit at cursor cur (-1: none) */ +static void editRow(const char *label,uint8_t line,const uint8_t *d,uint8_t n,char h,int8_t cur){ + char *o=put(str,label); + uint8_t col=0; + for(uint8_t i=0;iprint_normal(str,0,0,line); + if(cur>=0){ str[0]='^'; str[1]='\0'; g.A->print_tiny(str,(uint8_t)(col*7u+2u),(uint8_t)(line*8u+9u),false,true); } +} + +/* ---- display ---- */ +static void draw(void){ + /* The UI texts live in the assets: one read per frame into this word-aligned + * stack block (gen_assets.py keeps every offset aligned). */ + char s[UI_SIZE] __attribute__((aligned(4))); + const app_api_t *A=g.A; + char *o; + A->asset_read(0,s,UI_SIZE); + A->display_clear(); + A->status_clear(); + A->print_inverse(s+T_TITLE,2,0,true,true,(uint8_t)(2u+T_TITLE_CHARS*4u)); + if(g.status==ST_TX) /* a second capsule while on the air */ + A->print_inverse(s+T_TX,TX_CAPS_X,0,true,true,(uint8_t)(TX_CAPS_X+T_TX_CHARS*4u)); + A->draw_battery(); + + if(g.edit){ + int8_t c=(int8_t)g.cur; + editRow(s+T_LAT,1,g.ed,LAT_DIGITS,(g.ehemi&1u)?'S':'N',c<(int8_t)LAT_DIGITS?c:-1); + editRow(s+T_LON,3,g.ed+LAT_DIGITS,POS_DIGITS-LAT_DIGITS,(g.ehemi&2u)?'W':'E', + c>=(int8_t)LAT_DIGITS?(int8_t)(c-(int8_t)LAT_DIGITS):-1); + tiny(40,put(str,s+T_HELP1)); + tiny(48,put(str,g.status==ST_BADPOS?s+T_BADPOS:s+T_HELP2)); + A->blit_status(); + A->blit_full(); + return; + } + + if(g.flen){ + const uint8_t *f=g.frm; + o=putCall(str,f+7); *o='\0'; + A->print_normal(str,0,0,0); /* source */ + o=str; *o++='>'; o=putCall(o,f); + for(uint8_t e=13; !(f[e]&1u); ){ e+=7; *o++=','; o=putCall(o,f+e-6); } + tiny(9,o); /* ">APZK5,WIDE1-1" (<= 2 hops) */ + uint8_t i=(uint8_t)(14u+F_PATH_LEN+2u), end=(uint8_t)(g.flen-2u); /* info */ + for(uint8_t row=0;row<2u;row++){ /* info (<= 64), 2 rows of 32 */ + o=str; + for(; ofb[3][x]|=0x40u; + drawFreq(A->tx_freq()); + + /* bottom line: "lvl 66 tw +0 sent 3", or why nothing was sent */ + if(g.status==ST_DENIED||g.status==ST_NOCALL) + o=put(str,g.status==ST_DENIED?s+T_DENIED:s+T_NOCALL); + else { + o=put(str,s+T_LVL); o=puti(o,g.lvl); + o=put(o,s+T_TW); if(g.tw>0) *o++='+'; o=puti(o,g.tw); + o=put(o,s+T_SENT); o=puti(o,g.sent); + } + tiny(49,o); + + A->blit_status(); + A->blit_full(); +} + +/* ---- one frame on the air ---- */ +static void send(void){ + const app_api_t *A=g.A; + if(!g.flen){ g.status=ST_NOCALL; return; } + if(A->tx_state()){ g.status=ST_DENIED; return; } + g.status=ST_TX; + draw(); + + A->tx_set_params(); /* carrier + PA on the TX VFO */ + A->bk_write(REG_51,0); /* no CTCSS/DCS under the AFSK */ + A->tx_tone(1200); /* tone path on, mic off, 50 ms settle */ + g.space=0; + setTone(); /* mark at our level */ + clkStart(); + g.next=CYC_PER_BIT; + for(uint8_t i=0;itx_mute(true); + A->tx_end(); /* PA off + RX restored */ + + if(g.sent<9999u) g.sent++; + g.status=ST_IDLE; +} + +/* ---- input ---- */ +static void handleKeys(void){ + const app_api_t *A=g.A; + uint8_t key=A->get_key(); + if(key==APP_KEY_INVALID||key==g.prevKey){ g.prevKey=key; return; } + g.prevKey=key; + A->backlight_on(); + if(g.edit){ /* position editor */ + g.status=ST_IDLE; + if(key<=APP_KEY_9){ g.ed[g.cur]=key; if(g.curnav_dir(key); + if(d<0 && g.cur) g.cur--; + if(d>0 && g.curTW_MIN) g.tw--; } + else if(key==APP_KEY_3){ if(g.twnav_dir(key); + if(d>0 && g.lvl<=LVL_MAX-LVL_STEP) g.lvl=(uint8_t)(g.lvl+LVL_STEP); + if(d<0 && g.lvl>=LVL_MIN+LVL_STEP) g.lvl=(uint8_t)(g.lvl-LVL_STEP); + } +} + +__attribute__((section(".text.entry"),used)) +void app_main(const app_api_t *api){ + uint8_t frm[FRAME_MAX]; /* on the stack: the 4 KiB overlay also holds .bss */ + uint8_t c[CFG_LEN]; + g.A=api; + g.frm=frm; + g.prevKey=APP_KEY_INVALID; /* the rest of g starts at 0 (overlay zeroed by the loader) */ + g.lvl=LVL_DEF; + 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); + if(c[0]==CFG_MAGIC){ + if(c[1]>=LVL_MIN && c[1]<=LVL_MAX) g.lvl=c[1]; + if((int8_t)c[2]>=TW_MIN && (int8_t)c[2]<=TW_MAX) g.tw=(int8_t)c[2]; + for(uint8_t i=0;i>((i&1u)*4u))&15u); + uint8_t h=(uint8_t)(c[9]>>4); + if(posOk(g.ed) && h<=3u){ memcpy(g.pos,g.ed,POS_DIGITS); g.hemi=h; } + } + build(); + if(!g.flen) g.status=ST_NOCALL; + api->backlight_on(); + + g.running=true; + while(g.running){ + handleKeys(); + if(!g.running) break; + draw(); + api->battery_sample(); + api->delay_ms(40); + } + + c[0]=CFG_MAGIC; c[1]=g.lvl; c[2]=(uint8_t)g.tw; + for(uint8_t i=3;icfg_save(c,CFG_LEN); +} diff --git a/App/apps/aprstx/build.sh b/App/apps/aprstx/build.sh new file mode 100755 index 00000000..13c26f2f --- /dev/null +++ b/App/apps/aprstx/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside +# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is +# derived from the directory name; only APP_NAME - the human label baked into +# the 64-byte blob header - is per-app. The .app file is named after it (spaces +# stripped), so "Broadcast FM" -> BroadcastFM.app. +set -euo pipefail + +APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... +APP_NAME="APRS TX" # <-- the only per-app line +APP_VER="0.2" +APP_API_MIN=2 +APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) +OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) + +CC=/opt/toolchain/bin/arm-none-eabi-gcc +OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy +command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; } + +CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -fno-jump-tables -std=gnu11 -ffreestanding -fno-builtin -fno-common \ + -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra" +LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ + -Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments" + +rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous + +step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } +trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR + +step 1 assets ; APP_VER="$APP_VER" python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ + --shortcut none --assets "${APP}_assets.bin" >/dev/null +trap - ERR + +BYTES=$(wc -c < "${APP}.bin") +if [ "$BYTES" -gt 4096 ]; then + printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1 +fi +printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \ + "$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT" diff --git a/App/apps/aprstx/gen_assets.py b/App/apps/aprstx/gen_assets.py new file mode 100755 index 00000000..92e044c7 --- /dev/null +++ b/App/apps/aprstx/gen_assets.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# APRS TX read-only assets: the screen texts and the fixed part of the frame. +# +# The UI texts come first, each padded to a multiple of 4 bytes: draw() reads +# them in one asset_read into a word-aligned stack block, so every text costs a +# 2-byte sp-relative add instead of a literal-pool load and a pool word. +# The frame parts are AX.25-encoded here (addresses shifted left, SSID bytes, +# end-of-address bit on the last path entry): the app adds the source +# (boot-message callsign + SSID), the position (edited on the radio, this one is +# the default) and the FCS. Edit the station settings below, then rebuild; +# test/tx_model.py checks the resulting frame. +# +# ./gen_assets.py aprstx_assets.bin aprstx_assets.h +import os, re, sys +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 + +# ---- station settings ---- +SSID = 7 # F4HWN-7 +DEST = "APZK5" # APZ = experimental software +PATH = ["WIDE1-1"] # [] for none +LAT, LON = "4850.90N", "00216.25E" # default position: DDMM.hhN, DDDMM.hhE +SYMBOL = "/[" # table, code: '[' = person +COMMENT = "UV-K5 APRS TX" +# info field: "!" + LAT + SYMBOL[0] + LON + SYMBOL[1] + COMMENT + +TITLE = "APRS TX" +TX_CAPS = "TRANSMIT" # status-bar capsule while on the air + +UI = [ + ("T_TITLE", TITLE), + ("T_TX", TX_CAPS), + ("T_DENIED", "TX denied"), + ("T_NOCALL", "No boot callsign"), + ("T_LVL", "lvl "), + ("T_TW", " tw "), + ("T_SENT", " sent "), + ("T_LAT", "LAT "), # 2 spaces: digits aligned with LON + ("T_LON", "LON "), + ("T_HELP1", "0-9 digit * N/S E/W"), + ("T_HELP2", "UP/DN move MENU ok EXIT"), + ("T_BADPOS", "Invalid position"), +] + + +def addr(call, last=False): + call, _, ssid = call.upper().partition("-") + if not (1 <= len(call) <= 6 and call.isalnum()): + sys.exit(f"bad AX.25 address {call!r}") + b = bytes(ord(c) << 1 for c in call.ljust(6)) + return b + bytes([0x60 | (int(ssid or 0) << 1) | (1 if last else 0)]) + + +if not 0 <= SSID <= 15: + sys.exit("SSID must be 0-15") +if len(PATH) > 2: + sys.exit("at most 2 path entries (frame buffer)") +if not (re.fullmatch(r"\d{4}\.\d{2}[NS]", LAT) and re.fullmatch(r"\d{5}\.\d{2}[EW]", LON)): + sys.exit("LAT must be DDMM.hhN/S, LON DDDMM.hhE/W") +if len(SYMBOL) != 2 or len(COMMENT) > 43: + sys.exit("SYMBOL is 2 characters, COMMENT at most 43 (frame buffer)") +# 13 digits (DDMMhh DDDMMhh), then the hemispheres: bit 0 south, bit 1 west +POS = [int(c) for c in LAT + LON if c.isdigit()] +HEMI = (LAT[-1] == "S") | (LON[-1] == "W") << 1 + +a = Assets("APRSTX") +ui_size = 0 +for name, s in UI: + pad = -(len(s) + 1) % 4 # keep every offset word-aligned + a.text(name, s + "\0" * pad) + ui_size += len(s) + 1 + pad +a.const("UI_SIZE", ui_size) # the UI block read by draw() +a.const("T_TITLE_CHARS", len(TITLE)) +a.const("T_TX_CHARS", len(TX_CAPS)) +a.const("CFG_SSID", SSID) +a.raw("F_DEST", addr(DEST)) +a.raw("F_PATH", b"".join(addr(h, i == len(PATH) - 1) for i, h in enumerate(PATH))) +a.u8("POS_DEF", POS + [HEMI]) +a.raw("F_SYM", SYMBOL.encode("ascii")) +a.raw("F_COMMENT", COMMENT.encode("ascii")) +a.main() diff --git a/App/apps/aprstx/test/tx_model.py b/App/apps/aprstx/test/tx_model.py new file mode 100644 index 00000000..ec5e84a3 --- /dev/null +++ b/App/apps/aprstx/test/tx_model.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Model of the APRS TX app: the frame and bit stream of aprstx_app.c rebuilt +from the real assets (gen_assets.py), checked against the AX.25 reference of +the RX app (ax25.py), then sent as AFSK and decoded by the APRS RX model +(model_rx.py) through a receiver audio path. + +The tone generator is modelled with a phase-continuous NCO (expected: REG_71 is +rewritten, not the phase) and, as the worst case, with a phase reset at each +tone change. The TX tone path is assumed flat (no pre-emphasis): a receiver with +de-emphasis (STD path, FT3D) then sees 2200 Hz ~5 dB low, which the app's twist +setting (2200 Hz gain x (8 + tw) / 8) compensates. + + tx_model.py runs the checks and the decode sweep +""" +import math, os, re, subprocess, sys, tempfile +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(HERE, "..", "..", "aprsrx", "test")) +from ax25 import build, hdlc_bits, crc16, decode +import model_rx as M + +CALL = "F4HWN" # boot message +TXDELAY_FLAGS, TAIL_FLAGS = 40, 3 +LEAD_S = 0.050 + 1 / 1200 # tx_tone's 50 ms settle, then the first bit edge +DEV_MARK = 3000.0 # Hz of deviation for the mark tone at the chosen level + + +def assets(): + d = tempfile.mkdtemp() + b, h = os.path.join(d, "a.bin"), os.path.join(d, "a.h") + subprocess.run([sys.executable, os.path.join(HERE, "..", "gen_assets.py"), b, h], check=True) + defs = dict(re.findall(r"#define (\w+) (\d+)u", open(h).read())) + return open(b, "rb").read(), {k: int(v) for k, v in defs.items()} + + +def put_coord(d, h): + """aprstx_app.c putCoord(): 4850.90N / 00216.25E""" + n = len(d) + return "".join(("." if i == n - 2 else "") + str(x) for i, x in enumerate(d)) + h + + +def pos_ok(d): + """aprstx_app.c posOk()""" + if any(x > 9 for x in d): return False + la, lam, lo, lom = d[0] * 10 + d[1], d[2] * 10 + d[3], d[6] * 100 + d[7] * 10 + d[8], d[9] * 10 + d[10] + if la > 90 or lam > 59 or lo > 180 or lom > 59: return False + if la == 90 and (lam | d[4] | d[5]): return False + if lo == 180 and (lom | d[11] | d[12]): return False + return True + + +def cfg_pack(lvl, tw, pos, hemi): + c = [0xA8, lvl, tw & 0xFF] + [0] * 7 + for i, x in enumerate(pos): c[3 + i // 2] |= x << ((i & 1) * 4) + c[9] |= hemi << 4 + return bytes(c) + + +def cfg_unpack(c): + pos = [(c[3 + i // 2] >> ((i & 1) * 4)) & 15 for i in range(13)] + return pos, c[9] >> 4 + + +def edit_row(label, d, h, cur): + """aprstx_app.c editRow(): the text and the caret column""" + o, col, n = label, None, len(d) + for i, x in enumerate(d): + if i == n - 4: o += " " + if i == n - 2: o += "." + if i == cur: col = len(o) + o += str(x) + return o + " " + h, col + + +def build_c(blob, D, call=CALL, pos=None, hemi=None): + """aprstx_app.c build()""" + if not call or len(call) > 6 or "/" in call: + return None + rd = lambda name: blob[D[name]:D[name] + D[name + "_LEN"]] + pd = rd("POS_DEF") + pos = list(pd[:13]) if pos is None else pos + hemi = pd[13] if hemi is None else hemi + 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() + 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 | (D["CFG_SSID"] << 1) | (0 if D["F_PATH_LEN"] else 1)) + f += rd("F_PATH") + b"\x03\xF0" + info.encode() + c = 0xFFFF + for x in f: + c ^= x + for _ in range(8): + c = (c >> 1) ^ 0x8408 if c & 1 else c >> 1 + c ^= 0xFFFF + return bytes(f + bytes([c & 0xFF, c >> 8])) + + +def bits_c(frame): + """aprstx_app.c sendByte()/sendBit(): the bits on the air, before NRZI""" + out, ones = [], 0 + def byte(v, stuff): + nonlocal ones + for _ in range(8): + b = v & 1; v >>= 1 + out.append(b) + if stuff: + if not b: ones = 0 + else: + ones += 1 + if ones == 5: out.append(0); ones = 0 + for _ in range(TXDELAY_FLAGS): byte(0x7E, False) + ones = 0 + for x in frame: byte(x, True) + for _ in range(TAIL_FLAGS): byte(0x7E, False) + return out + + +def afsk(bits, tw=0, reset=False, twist_path_db=0.0): + """discriminator output (Hz) of the transmitted AFSK; tw as in the app, + twist_path_db an extra 2200 Hz gain of the TX audio path (unknown)""" + fs = M.FS_SIM + a_sp = DEV_MARK * min(127, 66 * (8 + tw) // 8) / 66 * 10 ** (twist_path_db / 20) + out, ph, space = [], 0.0, False + for i in range(int(LEAD_S * fs)): # plain mark before the flags + ph += 1200 / fs + out.append(DEV_MARK * math.sin(2 * math.pi * ph)) + spb = fs / 1200 + t = 0.0 + for b in bits: + if not b: + space = not space + if reset: ph = 0.0 + f, a = (2200, a_sp) if space else (1200, DEV_MARK) + t += spb + while len(out) < int(LEAD_S * fs + t): + ph += f / fs + out.append(a * math.sin(2 * math.pi * ph)) + return out + + +def main(): + blob, D = assets() + ok = True + f = build_c(blob, D) + ssid = D["CFG_SSID"] + info = "!4850.90N/00216.25E[UV-K5 APRS TX" # gen_assets.py defaults + ref = build("%s-%d" % (CALL, ssid) if ssid else CALL, dst="APZK5", path=["WIDE1-1"], info=info) + print("frame ", decode(f), "(%d bytes)" % len(f)) + ok &= f == ref + print("frame == ax25.build:", f == ref) + bc = bits_c(f) + ok &= bc == hdlc_bits(f, TXDELAY_FLAGS, TAIL_FLAGS) + print("bits == ax25.hdlc_bits:", bc == hdlc_bits(f, TXDELAY_FLAGS, TAIL_FLAGS), + "(%d bits, %.0f ms + %.0f ms lead)" % (len(bc), len(bc) / 1.2, LEAD_S * 1000)) + for bad in ("", "F4HWN/P", "F4HWN73"): + ok &= build_c(blob, D, bad) is None + print("bad boot callsigns refused:", True) + + # position editor: another position, south/west, the frame and the config + pos = [3, 3, 5, 2, 1, 3, 1, 5, 1, 1, 2, 5, 6] # 33 52.13S 151 12.56W + f2 = build_c(blob, D, pos=pos, hemi=3) + want = build("F4HWN-7", dst="APZK5", path=["WIDE1-1"], + info="!3352.13S/15112.56W[UV-K5 APRS TX") + ok &= f2 == want + print("edited position frame:", f2 == want, decode(f2)) + c = cfg_pack(70, -2, pos, 3) + ok &= len(c) == 10 and cfg_unpack(c) == (pos, 3) + print("config round trip:", cfg_unpack(c) == (pos, 3), c.hex()) + rows = (edit_row("LAT ", pos[:6], "S", 2), edit_row("LON ", pos[6:], "W", -1)) + ok &= rows == (("LAT 33 52.13 S", 8), ("LON 151 12.56 W", None)) + print("editor rows:", rows) + 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), + ([9,0,0,1,0,0,0,0,0,0,0,0,0], False), ([4,8,6,0,0,0,0,0,0,0,0,0,0], False), + ([4,8,0,0,0,0,1,8,0,0,0,0,1], False), ([4,8,0,0,0,0,1,8,1,0,0,0,0], False), + ([4,8,0,0,0,0,0,0,0,6,0,0,0], False)] + good = all(pos_ok(d) == w for d, w in checks) + ok &= good + print("posOk limits:", good) + + print("\n%-6s %-5s %4s %6s %5s | frames (3 seeds)" % ("phase", "rx", "tw", "path", "noise")) + for reset in (False, True): + for mode in ("raw", "std"): + for tw, path_db in ((0, 0.0), (4, 0.0), (0, 5.0)): + for noise in (0, 1500): + w = afsk(bc, tw, reset, path_db) + n = 0 + for seed in (1, 2, 3): + adc = M.channel(w, mode, noise, 0, 0, seed) + d = M.Demod() + for s in adc: + d.sample(s) + n += d.frames == [f] + print("%-6s %-5s %4d %+5.0fdB %5d | %d/3" % ( + "reset" if reset else "cont", mode, tw, path_db, noise, n)) + if not reset and noise == 0: + ok &= n == 3 + print("\nALL OK" if ok else "\nFAILURES") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cmake/check_printf_formats.py b/cmake/check_printf_formats.py index d1c510f2..d4eb5267 100644 --- a/cmake/check_printf_formats.py +++ b/cmake/check_printf_formats.py @@ -278,7 +278,9 @@ def main() -> int: *args.source_root.rglob("*.h"), ) for path in sorted(source_files): - if "external" in path.parts: + # external code, and host-side test programs (App/apps/*/test) that use + # the host printf and are never built into the firmware + if "external" in path.parts or "test" in path.parts: continue errors.extend(validate_file(path, debug_defined))