[docs](trx-wxsat): add README with architecture and API documentation

https://claude.ai/code/session_01Cm1JpWMDZanjwKg3r2S3VR
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 10:21:20 +00:00
committed by Stan Grams
parent 83a6b27130
commit 3ff848a715
+149
View File
@@ -0,0 +1,149 @@
# trx-wxsat
Weather satellite image decoders for NOAA APT and Meteor-M LRPT signals.
## Supported Satellites
| Satellite | Format | Frequency | Modulation |
|----------------|--------|---------------|------------------------|
| NOAA-15 | APT | 137.620 MHz | FM/AM subcarrier |
| NOAA-18 | APT | 137.9125 MHz | FM/AM subcarrier |
| NOAA-19 | APT | 137.100 MHz | FM/AM subcarrier |
| Meteor-M N2-3 | LRPT | 137.900 MHz | QPSK, 72 kbps, CCSDS |
| Meteor-M N2-4 | LRPT | 137.100 MHz | QPSK, 72 kbps, CCSDS |
## Architecture
```
trx-wxsat/src/
├── lib.rs # Module declarations, shared helpers
├── image_enc.rs # Shared PNG encoding (grayscale + RGB)
├── noaa/
│ ├── mod.rs # AptDecoder, AptImage (public API)
│ ├── apt.rs # AM demodulator (Hilbert/FFT), line sync tracker
│ ├── image_enc.rs # APT-specific dual-channel image assembly
│ └── telemetry.rs # Wedge-based calibration, satellite ID, histogram EQ
└── lrpt/
├── mod.rs # LrptDecoder, LrptImage (public API)
├── demod.rs # QPSK demodulator (Costas loop + Gardner TED)
├── cadu.rs # CCSDS CADU frame synchronisation (ASM search)
└── mcu.rs # Per-APID channel assembly, RGB composite
```
## Signal Flow
### NOAA APT
```
FM-demodulated audio (any sample rate)
│ AptDemod: FFT Hilbert transform, bandpass @ 2400 Hz ±1040 Hz
AM envelope resampled to 4160 Hz
│ SyncTracker: 1040 Hz sync-A marker correlation
Aligned 2080-sample lines → [SyncA 39][SpaceA 47][ImageA 909][TelA 45][SyncB 39][SpaceB 47][ImageB 909][TelB 45]
│ Telemetry extraction, wedge-based radiometric calibration, histogram EQ
PNG image (1818 × N pixels, dual-channel side-by-side)
```
**Key DSP details:**
- AM envelope extraction uses an FFT-based Hilbert transform (rustfft) with
bandpass filtering around the 2400 Hz subcarrier
- Sync detection uses cosine correlation against a 7-cycle 1040 Hz reference
pattern, normalised by RMS; threshold 0.15 for acquisition, 0.075 for tracking
- Telemetry frames span 128 lines; wedges 1-8 provide known reference levels
for piecewise-linear radiometric calibration; wedge 9 encodes the sensor
channel ID
- Satellite identification is heuristic, based on the detected channel pairing
(e.g. VIS + TIR4 maps to NOAA-18)
- Per-line normalisation clips to the 2nd-98th percentile before scaling
### Meteor-M LRPT
```
Baseband samples (any sample rate)
│ QpskDemod: Costas loop carrier recovery + Gardner TED symbol timing
Soft symbols (±1.0, I/Q interleaved) @ 72 ksym/s
│ CaduFramer: hard-decision, ASM (0x1ACFFC1D) search, frame lock
1024-byte CADUs (CCSDS transfer frames)
│ ChannelAssembler: VCID → APID routing, MCU extraction
Per-APID pixel rows (1568 px wide)
│ RGB composite (APIDs 64/65/66) or grayscale fallback
PNG image (1568 × N pixels)
```
**LRPT channel mapping:**
| APID | Channel | Band |
|------|---------|-------------------|
| 64 | 1 | Visible (0.5-0.7 µm) |
| 65 | 2 | Visible/NIR (0.7-1.1 µm) |
| 66 | 3 | Near-IR (1.6-1.8 µm) |
| 67 | 4 | Mid-IR (3.5-4.1 µm) |
| 68 | 5 | Thermal IR (10.5-11.5 µm) |
| 69 | 6 | Thermal IR (11.5-12.5 µm) |
**Key DSP details:**
- Costas loop parameters: bandwidth ~0.01 of symbol rate, damping factor 0.707
- Gardner TED operates on interpolated mid-sample points for timing error
estimation
- Frame synchronisation searches for the 4-byte Attached Sync Marker
(`0x1ACFFC1D`) and maintains lock/unlock state tracking
- Spacecraft ID extraction from VCDU header: ID 57 = Meteor-M N2-3,
ID 58 = Meteor-M N2-4
- RGB compositing uses channels 1/2/3 when available; falls back to the
highest-populated single channel as grayscale
## Public API
Both decoders share the same streaming interface:
```rust
// NOAA APT
let mut apt = AptDecoder::new(sample_rate);
apt.process_samples(&audio_batch); // returns new line count
apt.line_count(); // total lines so far
let image: Option<AptImage> = apt.finalize(); // PNG + telemetry
apt.reset(); // prepare for next pass
// Meteor-M LRPT
let mut lrpt = LrptDecoder::new(sample_rate);
lrpt.process_samples(&baseband_batch); // returns new MCU row count
lrpt.mcu_count(); // total MCU rows so far
let image: Option<LrptImage> = lrpt.finalize(); // PNG + metadata
lrpt.reset(); // prepare for next pass
```
### Output types
**`AptImage`**: PNG bytes, line count, first-line timestamp, identified
satellite (`NOAA-15`/`18`/`19`), sensor channels A and B
(`Visible1`, `NearIr2`, `ThermalIr4`, etc.)
**`LrptImage`**: PNG bytes, MCU row count, identified satellite
(`Meteor-M N2-3`/`N2-4`), comma-separated active APID list
## Dependencies
| Crate | Purpose |
|---------------|----------------------------------|
| `trx-core` | Shared core types |
| `rustfft` | FFT for Hilbert AM demodulation |
| `num-complex` | Complex arithmetic |
| `image` | PNG encoding (png feature only) |
## Integration
The crate plugs into `trx-server` as a decoder task. The server feeds PCM
audio from the SDR backend into `process_samples()`, auto-finalises on
timeout (no new lines/MCUs for a configurable period), and publishes
decoded images as `DecodedMessage::WxsatImage` / `DecodedMessage::LrptImage`
for client consumption. Images are saved to `~/.cache/trx-rs/wxsat/` and
`~/.cache/trx-rs/lrpt/`.