A new decoder crate, src/decoders/trx-sstv/, following the conventions of the existing ones (process_samples() → events, trx-core + png + base64, shaped like trx-wefax). Nothing is wired into the server or the web UI yet — this is the decoder alone.
What it decodes
Fifteen modes, taken from the VIS header every transmission opens with: Martin M1/M2, Scottie S1/S2/DX, Robot 36/72, PD50/90/120/160/180/240/290, Wraase SC2-180.
Modes are a table, not code. A mode is a list of segments — a sync pulse, gaps, and one scan per colour channel — plus a colour model and a geometry; the decoder reads each scan's offset straight off that list. Fifteen modes therefore cost about as much as one, and a new mode is a table entry.
Signal path
Band-pass over the SSTV band → Hilbert FIR → instantaneous frequency by phase difference → VIS header (parity checked) → a state machine that walks the transmission a line at a time. Each line is looked for where the mode says it should be, then nudged into place by the sync pulse actually found near it: two sound cards never agree exactly, and across the two minutes of a Martin M1 frame an uncorrected error of a few parts per million shears the picture visibly.
Rows are emitted as they decode, so a picture can be watched arriving — which is most of the appeal of the mode. A finished or abandoned reception yields a full RGB canvas that renders to PNG or base64.
Four bugs worth knowing about, because they shaped the code
The per-sample frequency estimate ripples by ±95 Hz at 1200 Hz — the Hilbert approximation is weakest near DC — though its mean is exact. Pixels average over their own window and were always right; the VIS bits and the sync detector classify single samples, and were reading the ripple. Both now read short means. Pixels deliberately still do not, so edges stay sharp.
Broadband noise cost the entire picture, not part of it. A phase-difference detector answers whatever is loudest and there was no input filter. Hence the band-pass, which is what every real decoder does first.
A sync search window shorter than a sync pulse rejected every pulse arriving late in it, for being short.
The first line's sync search locked onto the VIS stop bit — 30 ms at exactly the sync frequency, immediately before the picture. The header already says where the picture starts, so the first line no longer searches.
Testing
33 tests. Nine modes are encoded from a test card and decoded back, compared pixel by pixel, plus: silence around the transmission, a transmission cut off part way (the lines that arrived are kept and checked), two transmissions back to back, 20 dB of noise, and a transmitter clock 0.1% fast. Unit tests cover the mode table, VIS parity and leader rejection, demodulator accuracy at four sample rates, block-boundary continuity, and the PNG path.
Two limits worth stating plainly:
The encoder that generates the test signals reads the same timing table as the decoder, so a round trip tests the decoder and not the numbers. The numbers are held to the published line durations and frame times by a separate test in mode.rs.
Nothing has met a real off-air signal yet, which is where a decoder earns trust. cargo run -p trx-sstv --example round_trip_png -- /tmp/out writes sent-and-decoded pairs to look at.
Full workspace test suite and clippy are clean.
Next
Wiring: a DecodedMessage::Sstv variant, the server decoder task, SetSstvDecodeEnabled, decode history, and a UI panel like WEFAX's.
A new decoder crate, `src/decoders/trx-sstv/`, following the conventions of the existing ones (`process_samples()` → events, `trx-core` + `png` + `base64`, shaped like `trx-wefax`). **Nothing is wired into the server or the web UI yet — this is the decoder alone.**
## What it decodes
Fifteen modes, taken from the VIS header every transmission opens with: Martin M1/M2, Scottie S1/S2/DX, Robot 36/72, PD50/90/120/160/180/240/290, Wraase SC2-180.
Modes are a table, not code. A mode is a list of segments — a sync pulse, gaps, and one scan per colour channel — plus a colour model and a geometry; the decoder reads each scan's offset straight off that list. Fifteen modes therefore cost about as much as one, and a new mode is a table entry.
## Signal path
Band-pass over the SSTV band → Hilbert FIR → instantaneous frequency by phase difference → VIS header (parity checked) → a state machine that walks the transmission a line at a time. Each line is looked for where the mode says it should be, then nudged into place by the sync pulse actually found near it: two sound cards never agree exactly, and across the two minutes of a Martin M1 frame an uncorrected error of a few parts per million shears the picture visibly.
Rows are emitted as they decode, so a picture can be watched arriving — which is most of the appeal of the mode. A finished or abandoned reception yields a full RGB canvas that renders to PNG or base64.
## Four bugs worth knowing about, because they shaped the code
- **The per-sample frequency estimate ripples by ±95 Hz at 1200 Hz** — the Hilbert approximation is weakest near DC — though its mean is exact. Pixels average over their own window and were always right; the VIS bits and the sync detector classify single samples, and were reading the ripple. Both now read short means. Pixels deliberately still do not, so edges stay sharp.
- **Broadband noise cost the entire picture, not part of it.** A phase-difference detector answers whatever is loudest and there was no input filter. Hence the band-pass, which is what every real decoder does first.
- **A sync search window shorter than a sync pulse** rejected every pulse arriving late in it, for being short.
- **The first line's sync search locked onto the VIS stop bit** — 30 ms at exactly the sync frequency, immediately before the picture. The header already says where the picture starts, so the first line no longer searches.
## Testing
33 tests. Nine modes are encoded from a test card and decoded back, compared pixel by pixel, plus: silence around the transmission, a transmission cut off part way (the lines that arrived are kept and checked), two transmissions back to back, 20 dB of noise, and a transmitter clock 0.1% fast. Unit tests cover the mode table, VIS parity and leader rejection, demodulator accuracy at four sample rates, block-boundary continuity, and the PNG path.
Two limits worth stating plainly:
- The encoder that generates the test signals reads the **same** timing table as the decoder, so a round trip tests the decoder and not the numbers. The numbers are held to the published line durations and frame times by a separate test in `mode.rs`.
- Nothing has met a real off-air signal yet, which is where a decoder earns trust. `cargo run -p trx-sstv --example round_trip_png -- /tmp/out` writes sent-and-decoded pairs to look at.
Full workspace test suite and clippy are clean.
## Next
Wiring: a `DecodedMessage::Sstv` variant, the server decoder task, `SetSstvDecodeEnabled`, decode history, and a UI panel like WEFAX's.
A new decoder crate covering the modes SSTV is actually sent in: Martin
M1/M2, Scottie S1/S2/DX, Robot 36/72, PD50 through PD290, and Wraase
SC2-180. The mode comes from the VIS header every transmission opens
with, so nothing has to be told what is arriving.
Modes are a table rather than code: a list of segments -- sync, gaps,
and one scan per colour channel -- plus a colour model and a geometry.
The decoder reads the offset of each scan straight off that list, which
is what makes fifteen modes cost about as much as one, and a new mode a
table entry. The segment lists are checked against the published line
durations in a test, because both are transcribed by hand from the same
specification and a digit wrong in one is unlikely to be wrong
identically in the other.
Signal path: band-pass over the SSTV band, Hilbert FIR, instantaneous
frequency by phase difference, then a state machine that walks the
transmission a line at a time. Each line is looked for where the mode
says it should be and nudged into place by the sync pulse found near
it -- two sound cards never agree exactly, and over the two minutes of a
Martin M1 frame an uncorrected error of a few parts per million shears
the picture visibly. Rows are emitted as they decode, so a picture can
be watched arriving, which is most of the appeal of the mode.
Four things this cost, each now the reason a piece of it is shaped the
way it is:
The per-sample frequency estimate ripples by ±95 Hz at 1200 Hz, where
the Hilbert approximation is weakest, though its mean is exact. Pixels
average over their own window and were always right; the VIS bits and
the sync detector classify individual samples and were reading the
ripple. Both now read short means. Pixels deliberately still do not,
so edges stay where they are.
Broadband noise cost the whole picture, not part of it: a
phase-difference detector answers whatever is loudest, and there was no
input filter. Hence the band-pass, which is what every real decoder
does first.
A sync search window shorter than a sync pulse rejected every pulse
arriving late in it, for being short.
The first line's sync search locked onto the VIS stop bit -- 30 ms at
exactly the sync frequency, immediately before the picture starts. The
header already says where the picture begins, so the first line no
longer searches.
Tests: nine modes are encoded from a test card and decoded back,
compared pixel by pixel, alongside silence around the signal, a
transmission cut off part way, two transmissions back to back, 20 dB of
noise, and a transmitter clock 0.1% fast. The encoder that produces
those signals reads the same table as the decoder, so a round trip
tests the decoder and not the timings; the timings are held to the
published line durations separately.
Nothing is wired into the server or the web UI yet: this is the decoder
alone.
Signed-off-by: Stan Grams <sjg@haxx.space>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A new decoder crate,
src/decoders/trx-sstv/, following the conventions of the existing ones (process_samples()→ events,trx-core+png+base64, shaped liketrx-wefax). Nothing is wired into the server or the web UI yet — this is the decoder alone.What it decodes
Fifteen modes, taken from the VIS header every transmission opens with: Martin M1/M2, Scottie S1/S2/DX, Robot 36/72, PD50/90/120/160/180/240/290, Wraase SC2-180.
Modes are a table, not code. A mode is a list of segments — a sync pulse, gaps, and one scan per colour channel — plus a colour model and a geometry; the decoder reads each scan's offset straight off that list. Fifteen modes therefore cost about as much as one, and a new mode is a table entry.
Signal path
Band-pass over the SSTV band → Hilbert FIR → instantaneous frequency by phase difference → VIS header (parity checked) → a state machine that walks the transmission a line at a time. Each line is looked for where the mode says it should be, then nudged into place by the sync pulse actually found near it: two sound cards never agree exactly, and across the two minutes of a Martin M1 frame an uncorrected error of a few parts per million shears the picture visibly.
Rows are emitted as they decode, so a picture can be watched arriving — which is most of the appeal of the mode. A finished or abandoned reception yields a full RGB canvas that renders to PNG or base64.
Four bugs worth knowing about, because they shaped the code
Testing
33 tests. Nine modes are encoded from a test card and decoded back, compared pixel by pixel, plus: silence around the transmission, a transmission cut off part way (the lines that arrived are kept and checked), two transmissions back to back, 20 dB of noise, and a transmitter clock 0.1% fast. Unit tests cover the mode table, VIS parity and leader rejection, demodulator accuracy at four sample rates, block-boundary continuity, and the PNG path.
Two limits worth stating plainly:
mode.rs.cargo run -p trx-sstv --example round_trip_png -- /tmp/outwrites sent-and-decoded pairs to look at.Full workspace test suite and clippy are clean.
Next
Wiring: a
DecodedMessage::Sstvvariant, the server decoder task,SetSstvDecodeEnabled, decode history, and a UI panel like WEFAX's.0680c3ebedtoa0b0c0ed81