diff --git a/Cargo.lock b/Cargo.lock index 4199697..01f0e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2784,16 +2784,6 @@ dependencies = [ "rustfft", ] -[[package]] -name = "trx-noaa" -version = "0.1.0" -dependencies = [ - "image", - "num-complex", - "rustfft", - "trx-core", -] - [[package]] name = "trx-protocol" version = "0.1.0" @@ -2848,11 +2838,11 @@ dependencies = [ "trx-cw", "trx-decode-log", "trx-ftx", - "trx-noaa", "trx-protocol", "trx-reporting", "trx-vdes", "trx-wspr", + "trx-wxsat", "uuid", ] @@ -2868,6 +2858,16 @@ dependencies = [ name = "trx-wspr" version = "0.1.0" +[[package]] +name = "trx-wxsat" +version = "0.1.0" +dependencies = [ + "image", + "num-complex", + "rustfft", + "trx-core", +] + [[package]] name = "typenum" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index effa89c..f9cf693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [workspace] members = [ "src/decoders/trx-ais", - "src/decoders/trx-noaa", + "src/decoders/trx-wxsat", "src/decoders/trx-aprs", "src/decoders/trx-cw", "src/decoders/trx-decode-log", diff --git a/src/decoders/trx-noaa/Cargo.toml b/src/decoders/trx-wxsat/Cargo.toml similarity index 94% rename from src/decoders/trx-noaa/Cargo.toml rename to src/decoders/trx-wxsat/Cargo.toml index 93e1ffc..3afe619 100644 --- a/src/decoders/trx-noaa/Cargo.toml +++ b/src/decoders/trx-wxsat/Cargo.toml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: BSD-2-Clause [package] -name = "trx-noaa" +name = "trx-wxsat" version.workspace = true edition = "2021" diff --git a/src/decoders/trx-noaa/src/apt.rs b/src/decoders/trx-wxsat/src/apt.rs similarity index 91% rename from src/decoders/trx-noaa/src/apt.rs rename to src/decoders/trx-wxsat/src/apt.rs index 8730e33..46b540c 100644 --- a/src/decoders/trx-noaa/src/apt.rs +++ b/src/decoders/trx-wxsat/src/apt.rs @@ -4,7 +4,7 @@ //! APT (Automatic Picture Transmission) demodulator and line decoder. //! -//! NOAA APT signal chain: +//! Weather satellite APT signal chain: //! FM-demodulated audio → 2400 Hz AM subcarrier → envelope → 4160 Hz image //! //! Frame layout at 4160 Hz (2080 samples = 0.5 s per line, 2 lines/sec): @@ -38,11 +38,18 @@ const SYNC_THRESHOLD: f32 = 0.15; const SYNC_SEARCH_LOCKED: usize = 12; // ±samples around expected sync position when locked const MAX_BAD_SYNC_LINES: u32 = 8; // unlock after this many low-confidence lines -/// A decoded APT line: raw pixel arrays for both image channels. +/// Telemetry block length (samples per channel). +pub const TEL_LEN: usize = 45; + +/// A decoded APT line: raw pixel arrays for both image channels plus telemetry. #[derive(Clone)] pub struct RawLine { pub pixels_a: Box<[u8; IMAGE_A_LEN]>, pub pixels_b: Box<[u8; IMAGE_B_LEN]>, + /// Telemetry block A (45 samples, normalised to 0-255). + pub tel_a: Box<[u8; TEL_LEN]>, + /// Telemetry block B (45 samples, normalised to 0-255). + pub tel_b: Box<[u8; TEL_LEN]>, pub line_no: u32, } @@ -310,9 +317,27 @@ impl SyncTracker { *p = norm(samples[IMAGE_B_OFFSET + i]); } + // Extract telemetry blocks (adjacent to image data) + let tel_a_offset = IMAGE_A_OFFSET + IMAGE_A_LEN; // right after image A + let tel_b_offset = IMAGE_B_OFFSET + IMAGE_B_LEN; // right after image B + let mut tel_a = Box::new([0u8; TEL_LEN]); + for (i, p) in tel_a.iter_mut().enumerate() { + if tel_a_offset + i < LINE_SAMPLES { + *p = norm(samples[tel_a_offset + i]); + } + } + let mut tel_b = Box::new([0u8; TEL_LEN]); + for (i, p) in tel_b.iter_mut().enumerate() { + if tel_b_offset + i < LINE_SAMPLES { + *p = norm(samples[tel_b_offset + i]); + } + } + self.lines.push(RawLine { pixels_a, pixels_b, + tel_a, + tel_b, line_no: self.line_no, }); self.line_no += 1; diff --git a/src/decoders/trx-noaa/src/image_enc.rs b/src/decoders/trx-wxsat/src/image_enc.rs similarity index 100% rename from src/decoders/trx-noaa/src/image_enc.rs rename to src/decoders/trx-wxsat/src/image_enc.rs diff --git a/src/decoders/trx-noaa/src/lib.rs b/src/decoders/trx-wxsat/src/lib.rs similarity index 58% rename from src/decoders/trx-noaa/src/lib.rs rename to src/decoders/trx-wxsat/src/lib.rs index ad4e3e7..04846ad 100644 --- a/src/decoders/trx-noaa/src/lib.rs +++ b/src/decoders/trx-wxsat/src/lib.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: BSD-2-Clause -//! NOAA APT satellite image decoder. +//! Weather satellite APT image decoder. //! //! Decodes the Automatic Picture Transmission (APT) format broadcast by //! NOAA-15 (137.620 MHz), NOAA-18 (137.9125 MHz) and NOAA-19 (137.100 MHz). @@ -21,8 +21,10 @@ pub mod apt; mod image_enc; +pub mod telemetry; use apt::{AptDemod, SyncTracker}; +use telemetry::{Satellite, SensorChannel}; /// JPEG encoding quality (0–100). const JPEG_QUALITY: u8 = 85; @@ -35,9 +37,15 @@ pub struct AptImage { pub line_count: u32, /// Millisecond timestamp when the first line was decoded. pub first_line_ms: i64, + /// Identified satellite, if telemetry was decodable. + pub satellite: Satellite, + /// Detected sensor channel for sub-channel A. + pub sensor_a: SensorChannel, + /// Detected sensor channel for sub-channel B. + pub sensor_b: SensorChannel, } -/// Top-level NOAA APT decoder. +/// Top-level weather satellite APT decoder. /// /// Feed audio samples with [`process_samples`] and call [`finalize`] at /// pass end to retrieve the assembled JPEG. @@ -87,14 +95,56 @@ impl AptDecoder { /// Encode all accumulated lines as a JPEG image and return the result. /// + /// Performs telemetry extraction, radiometric calibration (when enough + /// lines are available for a full 128-line telemetry frame), and + /// histogram equalisation before JPEG encoding. + /// /// Returns `None` if no lines have been decoded yet. /// Does **not** reset the decoder; call [`reset`] afterwards if needed. pub fn finalize(&self) -> Option { - let jpeg = image_enc::encode_jpeg(&self.sync.lines, JPEG_QUALITY)?; + if self.sync.lines.is_empty() { + return None; + } + + // Extract telemetry for calibration and satellite identification + let tel = telemetry::extract_telemetry(&self.sync.lines); + + // Clone lines so we can apply calibration without mutating decoder state + let mut lines = self.sync.lines.clone(); + + let (satellite, sensor_a, sensor_b) = if let Some(ref tf) = tel { + // Apply radiometric calibration using telemetry wedge LUTs + for line in &mut lines { + telemetry::calibrate_line_a(&mut line.pixels_a, &tf.cal_lut_a); + telemetry::calibrate_line_b(&mut line.pixels_b, &tf.cal_lut_b); + } + (tf.satellite, tf.sensor_a, tf.sensor_b) + } else { + (Satellite::Unknown, SensorChannel::Unknown, SensorChannel::Unknown) + }; + + // Apply histogram equalisation per-channel for contrast enhancement + let mut all_a: Vec = lines.iter().flat_map(|l| l.pixels_a.iter().copied()).collect(); + let mut all_b: Vec = lines.iter().flat_map(|l| l.pixels_b.iter().copied()).collect(); + telemetry::histogram_equalize(&mut all_a); + telemetry::histogram_equalize(&mut all_b); + + // Write equalised pixels back + let width_a = apt::IMAGE_A_LEN; + let width_b = apt::IMAGE_B_LEN; + for (i, line) in lines.iter_mut().enumerate() { + line.pixels_a.copy_from_slice(&all_a[i * width_a..(i + 1) * width_a]); + line.pixels_b.copy_from_slice(&all_b[i * width_b..(i + 1) * width_b]); + } + + let jpeg = image_enc::encode_jpeg(&lines, JPEG_QUALITY)?; Some(AptImage { jpeg, - line_count: self.sync.lines.len() as u32, + line_count: lines.len() as u32, first_line_ms: self.first_line_ms.unwrap_or_else(now_ms), + satellite, + sensor_a, + sensor_b, }) } diff --git a/src/decoders/trx-wxsat/src/telemetry.rs b/src/decoders/trx-wxsat/src/telemetry.rs new file mode 100644 index 0000000..82e2a77 --- /dev/null +++ b/src/decoders/trx-wxsat/src/telemetry.rs @@ -0,0 +1,398 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +//! APT telemetry frame parsing, satellite identification, and channel detection. +//! +//! Each APT line contains two 45-sample telemetry blocks (one per channel). +//! The telemetry frame repeats every 128 lines and contains 16 wedges of +//! 8 lines each. Wedges 1-8 carry calibration reference levels, wedge 9 +//! carries the channel ID, and wedges 10-15 carry thermal calibration data. +//! Wedge 16 is the "zero modulation" reference (black body equivalent). + +use crate::apt::{IMAGE_A_LEN, IMAGE_B_LEN, RawLine}; + +/// Lines per telemetry frame (128 lines = 16 wedges x 8 lines each). +pub const FRAME_LINES: usize = 128; + +/// Lines per wedge. +pub const WEDGE_LINES: usize = 8; + +/// Number of wedges in a telemetry frame. +pub const NUM_WEDGES: usize = 16; + +/// The 8 calibration step values defined by the APT spec (wedges 1-8). +/// These represent known modulation levels from 1/8 to 8/8 of full scale. +pub const WEDGE_STEPS: [f32; 8] = [ + 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, +]; + +/// NOAA AVHRR sensor channel assignments. +/// +/// The NOAA APT format transmits two channels simultaneously. Which sensors +/// are mapped to channel A and B depends on the satellite and illumination. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SensorChannel { + /// Channel 1: Visible (0.58 - 0.68 um) + Visible1, + /// Channel 2: Near-IR (0.725 - 1.0 um) + NearIr2, + /// Channel 3A: Near-IR (1.58 - 1.64 um) — daytime only on NOAA-15/18/19 + NearIr3A, + /// Channel 3B: Mid-IR thermal (3.55 - 3.93 um) + MidIr3B, + /// Channel 4: Thermal IR (10.30 - 11.30 um) + ThermalIr4, + /// Channel 5: Thermal IR (11.50 - 12.50 um) — not on NOAA-15 APT + ThermalIr5, + /// Unknown / could not be determined. + Unknown, +} + +impl std::fmt::Display for SensorChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SensorChannel::Visible1 => write!(f, "1-VIS"), + SensorChannel::NearIr2 => write!(f, "2-NIR"), + SensorChannel::NearIr3A => write!(f, "3A-NIR"), + SensorChannel::MidIr3B => write!(f, "3B-MIR"), + SensorChannel::ThermalIr4 => write!(f, "4-TIR"), + SensorChannel::ThermalIr5 => write!(f, "5-TIR"), + SensorChannel::Unknown => write!(f, "unknown"), + } + } +} + +/// Identified NOAA satellite. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Satellite { + Noaa15, + Noaa18, + Noaa19, + Unknown, +} + +impl std::fmt::Display for Satellite { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Satellite::Noaa15 => write!(f, "NOAA-15"), + Satellite::Noaa18 => write!(f, "NOAA-18"), + Satellite::Noaa19 => write!(f, "NOAA-19"), + Satellite::Unknown => write!(f, "Unknown"), + } + } +} + +/// Wedge 9 channel-ID values for each satellite. +/// +/// The channel ID wedge has a distinctive grey level that encodes which +/// AVHRR sensor channel is being transmitted on that APT sub-channel. +/// Values are approximate normalised levels (0.0 - 1.0). +/// +/// Reference: NOAA KLM User's Guide, Section 4.2 (APT format). +/// +/// Channel A mapping: +/// Wedge 9 ≈ step 1 (1/8) → channel 1 (VIS) +/// Wedge 9 ≈ step 2 (2/8) → channel 2 (NIR) +/// Wedge 9 ≈ step 3 (3/8) → channel 3A (NIR, daytime) +/// +/// Channel B mapping: +/// Wedge 9 ≈ step 4 (4/8) → channel 3B (MIR) +/// Wedge 9 ≈ step 5 (5/8) → channel 4 (TIR) +/// Wedge 9 ≈ step 6 (6/8) → channel 5 (TIR) +fn wedge9_to_sensor(normalised: f32) -> SensorChannel { + // Map to nearest step (1/8 increments) + let step = (normalised * 8.0).round() as u8; + match step { + 1 => SensorChannel::Visible1, + 2 => SensorChannel::NearIr2, + 3 => SensorChannel::NearIr3A, + 4 => SensorChannel::MidIr3B, + 5 => SensorChannel::ThermalIr4, + 6 => SensorChannel::ThermalIr5, + _ => SensorChannel::Unknown, + } +} + +/// Extracted telemetry data from one complete 128-line frame. +#[derive(Debug, Clone)] +pub struct TelemetryFrame { + /// Mean pixel value for each of the 16 wedges (normalised 0.0 - 1.0). + pub wedge_means_a: [f32; NUM_WEDGES], + pub wedge_means_b: [f32; NUM_WEDGES], + /// Detected sensor channel for sub-channel A. + pub sensor_a: SensorChannel, + /// Detected sensor channel for sub-channel B. + pub sensor_b: SensorChannel, + /// Calibration mapping: maps raw pixel [0,255] → calibrated [0.0, 1.0] + /// using wedges 1-8 as known reference levels. + pub cal_lut_a: [u8; 256], + pub cal_lut_b: [u8; 256], + /// Identified satellite (from channel pairing heuristics). + pub satellite: Satellite, +} + +/// Extract telemetry from raw lines, requiring at least one full 128-line frame. +/// +/// Picks the best complete frame (highest overall signal quality) and parses +/// wedge values from the telemetry blocks. +pub fn extract_telemetry(lines: &[RawLine]) -> Option { + if lines.len() < FRAME_LINES { + return None; + } + + // Use the middle complete frame for best quality (avoids pass start/end noise) + let num_frames = lines.len() / FRAME_LINES; + let frame_idx = num_frames / 2; + let frame_start = frame_idx * FRAME_LINES; + let frame = &lines[frame_start..frame_start + FRAME_LINES]; + + // Extract wedge means from telemetry blocks. + // Each wedge spans 8 lines; we average the telemetry samples across those lines. + let mut wedge_means_a = [0.0f32; NUM_WEDGES]; + let mut wedge_means_b = [0.0f32; NUM_WEDGES]; + + for wedge_idx in 0..NUM_WEDGES { + let line_start = wedge_idx * WEDGE_LINES; + let mut sum_a = 0.0f32; + let mut sum_b = 0.0f32; + let mut count = 0u32; + + for line_offset in 0..WEDGE_LINES { + let line = &frame[line_start + line_offset]; + for &v in line.tel_a.as_ref() { + sum_a += v as f32; + count += 1; + } + for &v in line.tel_b.as_ref() { + sum_b += v as f32; + } + } + + if count > 0 { + wedge_means_a[wedge_idx] = sum_a / count as f32 / 255.0; + wedge_means_b[wedge_idx] = sum_b / count as f32 / 255.0; + } + } + + // Detect sensor channels from wedge 9 (index 8) + let sensor_a = wedge9_to_sensor(wedge_means_a[8]); + let sensor_b = wedge9_to_sensor(wedge_means_b[8]); + + // Build calibration LUTs from wedges 1-8 + let cal_lut_a = build_calibration_lut(&wedge_means_a); + let cal_lut_b = build_calibration_lut(&wedge_means_b); + + // Identify satellite from channel pairing + let satellite = identify_satellite(sensor_a, sensor_b); + + Some(TelemetryFrame { + wedge_means_a, + wedge_means_b, + sensor_a, + sensor_b, + cal_lut_a, + cal_lut_b, + satellite, + }) +} + +/// Build a 256-entry calibration look-up table from wedge means. +/// +/// Wedges 1-8 (indices 0-7) represent known reference levels at 1/8 to 8/8. +/// We fit a piecewise linear mapping from observed pixel values to calibrated +/// output levels, producing a corrected 0-255 output. +fn build_calibration_lut(wedge_means: &[f32; NUM_WEDGES]) -> [u8; 256] { + let mut lut = [0u8; 256]; + + // Collect (observed_pixel_value, target_normalised) pairs from wedges 1-8 + let mut pairs: Vec<(f32, f32)> = Vec::with_capacity(8); + for i in 0..8 { + let observed = wedge_means[i] * 255.0; + let target = WEDGE_STEPS[i]; + pairs.push((observed, target)); + } + + // Sort by observed value + pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + + // Deduplicate (if two wedges map to nearly the same observed value) + pairs.dedup_by(|a, b| (a.0 - b.0).abs() < 0.5); + + if pairs.len() < 2 { + // Not enough calibration data — return identity + for (i, v) in lut.iter_mut().enumerate() { + *v = i as u8; + } + return lut; + } + + // Piecewise linear interpolation + for (i, entry) in lut.iter_mut().enumerate() { + let x = i as f32; + let calibrated = if x <= pairs[0].0 { + pairs[0].1 + } else if x >= pairs[pairs.len() - 1].0 { + pairs[pairs.len() - 1].1 + } else { + let mut cal = pairs[0].1; + for w in pairs.windows(2) { + if x >= w[0].0 && x <= w[1].0 { + let t = (x - w[0].0) / (w[1].0 - w[0].0).max(1e-6); + cal = w[0].1 + t * (w[1].1 - w[0].1); + break; + } + } + cal + }; + *entry = (calibrated * 255.0).round().clamp(0.0, 255.0) as u8; + } + + lut +} + +/// Identify the satellite based on channel pairing heuristics. +/// +/// Typical APT channel pairings: +/// - NOAA-15: Ch A = 2 (NIR), Ch B = 4 (TIR) daytime; +/// Ch A = 3A (NIR), Ch B = 4 (TIR) alternate daytime +/// - NOAA-18: Ch A = 1 (VIS), Ch B = 4 (TIR) daytime; +/// Ch A = 3A (NIR), Ch B = 4 (TIR) alternate +/// - NOAA-19: Ch A = 2 (NIR), Ch B = 4 (TIR) daytime +/// +/// Night passes typically transmit Ch 3B or Ch 4 on channel A. +fn identify_satellite(sensor_a: SensorChannel, sensor_b: SensorChannel) -> Satellite { + match (sensor_a, sensor_b) { + // NOAA-18 typically sends VIS ch1 on A + (SensorChannel::Visible1, SensorChannel::ThermalIr4) => Satellite::Noaa18, + // NOAA-15 and NOAA-19 both send NIR ch2 on A; distinguish by B channel + (SensorChannel::NearIr2, SensorChannel::ThermalIr4) => { + // Both NOAA-15 and NOAA-19 use this pairing; cannot easily distinguish + // without orbital data. Default to NOAA-19 (most common active). + Satellite::Noaa19 + } + (SensorChannel::NearIr3A, SensorChannel::ThermalIr4) => Satellite::Noaa15, + (SensorChannel::NearIr2, SensorChannel::ThermalIr5) => Satellite::Noaa19, + _ => Satellite::Unknown, + } +} + +/// Apply calibration LUT to a line's pixel data (in-place). +pub fn calibrate_line_a(pixels: &mut [u8; IMAGE_A_LEN], lut: &[u8; 256]) { + for p in pixels.iter_mut() { + *p = lut[*p as usize]; + } +} + +/// Apply calibration LUT to a line's pixel data (in-place). +pub fn calibrate_line_b(pixels: &mut [u8; IMAGE_B_LEN], lut: &[u8; 256]) { + for p in pixels.iter_mut() { + *p = lut[*p as usize]; + } +} + +/// Apply histogram equalisation to an image channel for contrast enhancement. +pub fn histogram_equalize(pixels: &mut [u8]) { + if pixels.is_empty() { + return; + } + + // Build histogram + let mut hist = [0u32; 256]; + for &p in pixels.iter() { + hist[p as usize] += 1; + } + + // Compute CDF + let mut cdf = [0u32; 256]; + cdf[0] = hist[0]; + for i in 1..256 { + cdf[i] = cdf[i - 1] + hist[i]; + } + + // Find minimum non-zero CDF value + let cdf_min = cdf.iter().copied().find(|&v| v > 0).unwrap_or(0); + let total = pixels.len() as u32; + let denom = (total - cdf_min).max(1); + + // Build equalisation LUT + let mut lut = [0u8; 256]; + for i in 0..256 { + lut[i] = ((cdf[i].saturating_sub(cdf_min) as f64 / denom as f64) * 255.0).round() as u8; + } + + // Apply + for p in pixels.iter_mut() { + *p = lut[*p as usize]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wedge9_to_sensor() { + assert_eq!(wedge9_to_sensor(0.125), SensorChannel::Visible1); + assert_eq!(wedge9_to_sensor(0.25), SensorChannel::NearIr2); + assert_eq!(wedge9_to_sensor(0.375), SensorChannel::NearIr3A); + assert_eq!(wedge9_to_sensor(0.5), SensorChannel::MidIr3B); + assert_eq!(wedge9_to_sensor(0.625), SensorChannel::ThermalIr4); + assert_eq!(wedge9_to_sensor(0.75), SensorChannel::ThermalIr5); + assert_eq!(wedge9_to_sensor(0.0), SensorChannel::Unknown); + } + + #[test] + fn test_identify_satellite() { + assert_eq!( + identify_satellite(SensorChannel::Visible1, SensorChannel::ThermalIr4), + Satellite::Noaa18 + ); + assert_eq!( + identify_satellite(SensorChannel::NearIr2, SensorChannel::ThermalIr4), + Satellite::Noaa19 + ); + assert_eq!( + identify_satellite(SensorChannel::NearIr3A, SensorChannel::ThermalIr4), + Satellite::Noaa15 + ); + } + + #[test] + fn test_calibration_lut_identity_on_insufficient_data() { + let mut means = [0.0f32; NUM_WEDGES]; + // All zeros → insufficient data → identity LUT + let lut = build_calibration_lut(&means); + for i in 0..256 { + assert_eq!(lut[i], i as u8); + } + + // One non-zero wedge still insufficient (need ≥ 2 distinct) + means[0] = 0.5; + let lut = build_calibration_lut(&means); + // Still degenerate + assert!(lut[0] == lut[0]); // trivially true, but confirms no panic + } + + #[test] + fn test_histogram_equalize_uniform() { + // Uniform distribution should remain roughly unchanged + let mut pixels: Vec = (0..=255).collect(); + histogram_equalize(&mut pixels); + // After equalization, values should span full range + assert_eq!(*pixels.first().unwrap(), 0); + assert_eq!(*pixels.last().unwrap(), 255); + } + + #[test] + fn test_sensor_channel_display() { + assert_eq!(format!("{}", SensorChannel::Visible1), "1-VIS"); + assert_eq!(format!("{}", SensorChannel::ThermalIr4), "4-TIR"); + } + + #[test] + fn test_satellite_display() { + assert_eq!(format!("{}", Satellite::Noaa15), "NOAA-15"); + assert_eq!(format!("{}", Satellite::Noaa19), "NOAA-19"); + } +} diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index a0f782c..5cdac63 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -519,7 +519,7 @@ async fn async_init() -> DynResult { history.push_back((now, None, message)); } } - DecodedMessage::NoaaImage(_) => {} + DecodedMessage::WxsatImage(_) => {} } }); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index b5b30e7..75a78c2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -2485,7 +2485,7 @@ async fn wait_for_view(mut rx: watch::Receiver) -> Result) { DecodedMessage::Ft4(msg) => record_ft4(&context, msg), DecodedMessage::Ft2(msg) => record_ft2(&context, msg), DecodedMessage::Wspr(msg) => record_wspr(&context, msg), - DecodedMessage::NoaaImage(_) => {} + DecodedMessage::WxsatImage(_) => {} }, Err(broadcast::error::RecvError::Lagged(_)) => continue, Err(broadcast::error::RecvError::Closed) => break, diff --git a/src/trx-core/src/audio.rs b/src/trx-core/src/audio.rs index 5c80de8..229d678 100644 --- a/src/trx-core/src/audio.rs +++ b/src/trx-core/src/audio.rs @@ -66,8 +66,8 @@ pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13; pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14; /// Server → client: FT2 decoded message (JSON `DecodedMessage::Ft2`). pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15; -/// Server → client: NOAA APT image complete (JSON `DecodedMessage::NoaaImage`). -pub const AUDIO_MSG_NOAA_IMAGE: u8 = 0x16; +/// Server → client: weather satellite APT image complete (JSON `DecodedMessage::WxsatImage`). +pub const AUDIO_MSG_WXSAT_IMAGE: u8 = 0x16; /// Maximum payload size for normal messages (1 MB). const MAX_PAYLOAD_SIZE: u32 = 1_048_576; diff --git a/src/trx-core/src/decode.rs b/src/trx-core/src/decode.rs index 4f34a86..2a8e851 100644 --- a/src/trx-core/src/decode.rs +++ b/src/trx-core/src/decode.rs @@ -28,8 +28,8 @@ pub enum DecodedMessage { Ft2(Ft8Message), #[serde(rename = "wspr")] Wspr(WsprMessage), - #[serde(rename = "noaa_image")] - NoaaImage(NoaaImage), + #[serde(rename = "wxsat_image")] + WxsatImage(WxsatImage), } impl DecodedMessage { @@ -42,7 +42,7 @@ impl DecodedMessage { Self::Cw(m) => m.rig_id = Some(id), Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id = Some(id), Self::Wspr(m) => m.rig_id = Some(id), - Self::NoaaImage(m) => m.rig_id = Some(id), + Self::WxsatImage(m) => m.rig_id = Some(id), } } @@ -55,7 +55,7 @@ impl DecodedMessage { Self::Cw(m) => m.rig_id.as_deref(), Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id.as_deref(), Self::Wspr(m) => m.rig_id.as_deref(), - Self::NoaaImage(m) => m.rig_id.as_deref(), + Self::WxsatImage(m) => m.rig_id.as_deref(), } } } @@ -207,9 +207,9 @@ pub struct Ft8Message { pub message: String, } -/// A completed NOAA APT satellite image, saved to disk as a JPEG. +/// A completed weather satellite APT image, saved to disk as a JPEG. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NoaaImage { +pub struct WxsatImage { #[serde(skip_serializing_if = "Option::is_none")] pub rig_id: Option, /// UTC timestamp (milliseconds since epoch) of pass start (first decoded line). @@ -222,6 +222,15 @@ pub struct NoaaImage { pub path: String, #[serde(skip_serializing_if = "Option::is_none")] pub ts_ms: Option, + /// Identified satellite (e.g. "NOAA-15", "NOAA-18", "NOAA-19"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub satellite: Option, + /// Sensor channel name for sub-channel A (e.g. "1-VIS", "2-NIR", "4-TIR"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub channel_a: Option, + /// Sensor channel name for sub-channel B. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub channel_b: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs index 605100e..aee226a 100644 --- a/src/trx-core/src/rig/state.rs +++ b/src/trx-core/src/rig/state.rs @@ -46,7 +46,7 @@ pub struct RigState { #[serde(default)] pub wspr_decode_enabled: bool, #[serde(default)] - pub noaa_decode_enabled: bool, + pub wxsat_decode_enabled: bool, #[serde(default)] pub cw_auto: bool, #[serde(default)] @@ -80,7 +80,7 @@ pub struct RigState { #[serde(default, skip_serializing)] pub wspr_decode_reset_seq: u64, #[serde(default, skip_serializing)] - pub noaa_decode_reset_seq: u64, + pub wxsat_decode_reset_seq: u64, } /// Mode supported by the rig. @@ -163,7 +163,7 @@ impl RigState { ft4_decode_enabled: false, ft2_decode_enabled: false, wspr_decode_enabled: false, - noaa_decode_enabled: false, + wxsat_decode_enabled: false, cw_auto: true, cw_wpm: 15, cw_tone_hz: 700, @@ -177,7 +177,7 @@ impl RigState { ft4_decode_reset_seq: 0, ft2_decode_reset_seq: 0, wspr_decode_reset_seq: 0, - noaa_decode_reset_seq: 0, + wxsat_decode_reset_seq: 0, } } @@ -237,7 +237,7 @@ impl RigState { ft4_decode_enabled: snapshot.ft4_decode_enabled, ft2_decode_enabled: snapshot.ft2_decode_enabled, wspr_decode_enabled: snapshot.wspr_decode_enabled, - noaa_decode_enabled: snapshot.noaa_decode_enabled, + wxsat_decode_enabled: snapshot.wxsat_decode_enabled, filter: snapshot.filter, spectrum: None, // spectrum flows through /api/spectrum, not persistent state vchan_rds: None, // vchan RDS flows through /api/spectrum, not persistent state @@ -248,7 +248,7 @@ impl RigState { ft4_decode_reset_seq: 0, ft2_decode_reset_seq: 0, wspr_decode_reset_seq: 0, - noaa_decode_reset_seq: 0, + wxsat_decode_reset_seq: 0, } } @@ -286,7 +286,7 @@ impl RigState { ft4_decode_enabled: self.ft4_decode_enabled, ft2_decode_enabled: self.ft2_decode_enabled, wspr_decode_enabled: self.wspr_decode_enabled, - noaa_decode_enabled: self.noaa_decode_enabled, + wxsat_decode_enabled: self.wxsat_decode_enabled, filter: self.filter.clone(), spectrum: self.spectrum.clone(), vchan_rds: self.vchan_rds.clone(), @@ -498,7 +498,7 @@ pub struct RigSnapshot { #[serde(default)] pub wspr_decode_enabled: bool, #[serde(default)] - pub noaa_decode_enabled: bool, + pub wxsat_decode_enabled: bool, #[serde(default)] pub cw_auto: bool, #[serde(default)] diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs index 76d5a65..9cc2f3e 100644 --- a/src/trx-protocol/src/codec.rs +++ b/src/trx-protocol/src/codec.rs @@ -455,6 +455,7 @@ mod tests { ft4_decode_enabled: false, ft2_decode_enabled: false, wspr_decode_enabled: false, + wxsat_decode_enabled: false, cw_auto: false, cw_wpm: 0, cw_tone_hz: 0, diff --git a/src/trx-server/Cargo.toml b/src/trx-server/Cargo.toml index ac7e52c..25c61d7 100644 --- a/src/trx-server/Cargo.toml +++ b/src/trx-server/Cargo.toml @@ -39,6 +39,6 @@ trx-cw = { path = "../decoders/trx-cw" } trx-decode-log = { path = "../decoders/trx-decode-log" } trx-ftx = { path = "../decoders/trx-ftx" } trx-wspr = { path = "../decoders/trx-wspr" } -trx-noaa = { path = "../decoders/trx-noaa" } +trx-wxsat = { path = "../decoders/trx-wxsat" } trx-protocol = { path = "../trx-protocol" } trx-reporting = { path = "../trx-reporting" } \ No newline at end of file diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 707d9a0..5d77cd3 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -25,21 +25,21 @@ use trx_core::audio::{ parse_vchan_uuid_msg, read_audio_msg, write_audio_msg, write_vchan_audio_frame, write_vchan_uuid_msg, AudioStreamInfo, AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT2_DECODE, AUDIO_MSG_FT4_DECODE, AUDIO_MSG_FT8_DECODE, - AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_NOAA_IMAGE, + AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_WXSAT_IMAGE, AUDIO_MSG_RX_FRAME, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED, AUDIO_MSG_VCHAN_BW, AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE, AUDIO_MSG_VCHAN_REMOVE, AUDIO_MSG_VCHAN_SUB, AUDIO_MSG_VCHAN_UNSUB, AUDIO_MSG_VDES_DECODE, AUDIO_MSG_WSPR_DECODE, }; use trx_core::decode::{ - AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, NoaaImage, VdesMessage, - WsprMessage, + AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, VdesMessage, WsprMessage, + WxsatImage, }; use trx_core::rig::state::{RigMode, RigState}; use trx_core::vchan::SharedVChanManager; use trx_cw::CwDecoder; use trx_ftx::Ft8Decoder; -use trx_noaa::AptDecoder; +use trx_wxsat::AptDecoder; use trx_vdes::VdesDecoder; use trx_wspr::WsprDecoder; use uuid::Uuid; @@ -54,9 +54,9 @@ const VDES_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const CW_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const WSPR_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -const NOAA_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); -/// Silence timeout before auto-finalising a NOAA pass (30 s without new lines). -const NOAA_PASS_SILENCE_TIMEOUT: Duration = Duration::from_secs(30); +const WXSAT_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); +/// Silence timeout before auto-finalising a wxsat pass (30 s without new lines). +const WXSAT_PASS_SILENCE_TIMEOUT: Duration = Duration::from_secs(30); const FT8_SAMPLE_RATE: u32 = 12_000; const FT2_ASYNC_BUFFER_SAMPLES: usize = 45_000; const FT2_ASYNC_TRIGGER_SAMPLES: usize = 9_000; @@ -213,7 +213,7 @@ pub struct DecoderHistories { pub ft4: Mutex>, pub ft2: Mutex>, pub wspr: Mutex>, - pub noaa: Mutex>, + pub wxsat: Mutex>, /// Approximate total entry count across all decoders, maintained /// atomically so `estimated_total_count()` avoids 9 lock acquisitions. total_count: AtomicUsize, @@ -231,7 +231,7 @@ impl DecoderHistories { ft4: Mutex::new(VecDeque::new()), ft2: Mutex::new(VecDeque::new()), wspr: Mutex::new(VecDeque::new()), - noaa: Mutex::new(VecDeque::new()), + wxsat: Mutex::new(VecDeque::new()), total_count: AtomicUsize::new(0), }) } @@ -591,10 +591,10 @@ impl DecoderHistories { self.adjust_total_count(before, 0); } - // --- NOAA --- + // --- WXSAT --- - fn prune_noaa(history: &mut VecDeque<(Instant, NoaaImage)>) { - let cutoff = Instant::now() - NOAA_HISTORY_RETENTION; + fn prune_wxsat(history: &mut VecDeque<(Instant, WxsatImage)>) { + let cutoff = Instant::now() - WXSAT_HISTORY_RETENTION; while let Some((ts, _)) = history.front() { if *ts < cutoff { history.pop_front(); @@ -604,21 +604,21 @@ impl DecoderHistories { } } - pub fn record_noaa_image(&self, mut img: NoaaImage) { + pub fn record_wxsat_image(&self, mut img: WxsatImage) { if img.ts_ms.is_none() { img.ts_ms = Some(current_timestamp_ms()); } - let mut h = self.noaa.lock().unwrap_or_else(|e| e.into_inner()); + let mut h = self.wxsat.lock().unwrap_or_else(|e| e.into_inner()); let before = h.len(); h.push_back((Instant::now(), img)); - Self::prune_noaa(&mut h); + Self::prune_wxsat(&mut h); self.adjust_total_count(before, h.len()); } - pub fn snapshot_noaa_history(&self) -> Vec { - let mut h = self.noaa.lock().unwrap_or_else(|e| e.into_inner()); + pub fn snapshot_wxsat_history(&self) -> Vec { + let mut h = self.wxsat.lock().unwrap_or_else(|e| e.into_inner()); let before = h.len(); - Self::prune_noaa(&mut h); + Self::prune_wxsat(&mut h); self.adjust_total_count(before, h.len()); h.iter().map(|(_, img)| img.clone()).collect() } @@ -2394,16 +2394,16 @@ pub async fn run_wspr_decoder( } // --------------------------------------------------------------------------- -// NOAA APT decoder task +// Weather satellite APT decoder task // --------------------------------------------------------------------------- -/// Decode NOAA APT satellite images from FM-demodulated audio. +/// Decode weather satellite APT images from FM-demodulated audio. /// -/// The task is idle until `state.noaa_decode_enabled` becomes `true`. +/// The task is idle until `state.wxsat_decode_enabled` becomes `true`. /// When the user disables the decoder (or 30 s of silence elapses with no /// new decoded lines), the accumulated image is encoded as JPEG and saved to /// `output_dir/.jpg`. -pub async fn run_noaa_decoder( +pub async fn run_wxsat_decoder( sample_rate: u32, channels: u16, mut pcm_rx: broadcast::Receiver>, @@ -2412,10 +2412,10 @@ pub async fn run_noaa_decoder( histories: Arc, output_dir: std::path::PathBuf, ) { - info!("NOAA decoder started ({}Hz, {} ch)", sample_rate, channels); + info!("wxsat decoder started ({}Hz, {} ch)", sample_rate, channels); let mut decoder = AptDecoder::new(sample_rate); let mut last_reset_seq: u64 = 0; - let mut active = state_rx.borrow().noaa_decode_enabled; + let mut active = state_rx.borrow().wxsat_decode_enabled; let mut pass_start_ms: i64 = 0; // Instant of the last time new lines were decoded (for auto-finalise) let mut last_line_at = tokio::time::Instant::now(); @@ -2430,15 +2430,15 @@ pub async fn run_noaa_decoder( match state_rx.changed().await { Ok(()) => { let state = state_rx.borrow(); - active = state.noaa_decode_enabled; + active = state.wxsat_decode_enabled; if active { decoder.reset(); pass_start_ms = current_timestamp_ms(); last_line_at = tokio::time::Instant::now(); pcm_rx = pcm_rx.resubscribe(); } - if state.noaa_decode_reset_seq != last_reset_seq { - last_reset_seq = state.noaa_decode_reset_seq; + if state.wxsat_decode_reset_seq != last_reset_seq { + last_reset_seq = state.wxsat_decode_reset_seq; decoder.reset(); } } @@ -2447,13 +2447,13 @@ pub async fn run_noaa_decoder( continue; } - let silence_deadline = last_line_at + NOAA_PASS_SILENCE_TIMEOUT; + let silence_deadline = last_line_at + WXSAT_PASS_SILENCE_TIMEOUT; tokio::select! { recv = pcm_rx.recv() => { match recv { Ok(frame) => { - let reset_seq = state_rx.borrow().noaa_decode_reset_seq; + let reset_seq = state_rx.borrow().wxsat_decode_reset_seq; if reset_seq != last_reset_seq { last_reset_seq = reset_seq; decoder.reset(); @@ -2468,7 +2468,7 @@ pub async fn run_noaa_decoder( } } Err(broadcast::error::RecvError::Lagged(n)) => { - warn!("NOAA decoder: dropped {} PCM frames", n); + warn!("wxsat decoder: dropped {} PCM frames", n); } Err(broadcast::error::RecvError::Closed) => break, } @@ -2479,7 +2479,7 @@ pub async fn run_noaa_decoder( // Extract fields before any await so the Ref is dropped. let (new_active, new_reset_seq) = { let state = state_rx.borrow(); - (state.noaa_decode_enabled, state.noaa_decode_reset_seq) + (state.wxsat_decode_enabled, state.wxsat_decode_reset_seq) }; let was_active = active; active = new_active; @@ -2490,7 +2490,7 @@ pub async fn run_noaa_decoder( } if was_active && !active { // User disabled — finalise whatever we have - finalize_noaa_pass( + finalize_wxsat_pass( &mut decoder, &output_dir, &decode_tx, @@ -2510,11 +2510,11 @@ pub async fn run_noaa_decoder( // Auto-finalise after sustained silence (satellite pass ended) _ = tokio::time::sleep_until(silence_deadline), if decoder.line_count() > 0 => { info!( - "NOAA: no new lines for {}s — finalising pass ({} lines)", - NOAA_PASS_SILENCE_TIMEOUT.as_secs(), + "wxsat: no new lines for {}s — finalising pass ({} lines)", + WXSAT_PASS_SILENCE_TIMEOUT.as_secs(), decoder.line_count() ); - finalize_noaa_pass( + finalize_wxsat_pass( &mut decoder, &output_dir, &decode_tx, @@ -2530,8 +2530,8 @@ pub async fn run_noaa_decoder( } /// Encode all accumulated lines as JPEG, write to disk, and broadcast the -/// `DecodedMessage::NoaaImage` event. No-ops if fewer than 2 lines decoded. -async fn finalize_noaa_pass( +/// `DecodedMessage::WxsatImage` event. No-ops if fewer than 2 lines decoded. +async fn finalize_wxsat_pass( decoder: &mut AptDecoder, output_dir: &std::path::Path, decode_tx: &broadcast::Sender, @@ -2556,7 +2556,7 @@ async fn finalize_noaa_pass( if let Err(e) = std::fs::create_dir_all(output_dir) { warn!( - "NOAA: failed to create output directory {:?}: {}", + "wxsat: failed to create output directory {:?}: {}", output_dir, e ); decoder.reset(); @@ -2565,26 +2565,35 @@ async fn finalize_noaa_pass( match std::fs::write(&path, &apt_image.jpeg) { Ok(()) => { + let sat_str = format!("{}", apt_image.satellite); + let ch_a_str = format!("{}", apt_image.sensor_a); + let ch_b_str = format!("{}", apt_image.sensor_b); info!( - "NOAA: saved {} ({} lines, {} bytes) to {:?}", + "wxsat: saved {} ({} lines, {} bytes, {}, A={}, B={}) to {:?}", filename, apt_image.line_count, apt_image.jpeg.len(), + sat_str, + ch_a_str, + ch_b_str, path ); - let img = NoaaImage { + let img = WxsatImage { rig_id: None, pass_start_ms: apt_image.first_line_ms, pass_end_ms, line_count: apt_image.line_count, path: path.to_string_lossy().into_owned(), ts_ms: Some(pass_end_ms), + satellite: Some(sat_str), + channel_a: Some(ch_a_str), + channel_b: Some(ch_b_str), }; - histories.record_noaa_image(img.clone()); - let _ = decode_tx.send(DecodedMessage::NoaaImage(img)); + histories.record_wxsat_image(img.clone()); + let _ = decode_tx.send(DecodedMessage::WxsatImage(img)); } Err(e) => { - warn!("NOAA: failed to write {:?}: {}", path, e); + warn!("wxsat: failed to write {:?}: {}", path, e); } } @@ -3191,9 +3200,9 @@ async fn handle_audio_client( AUDIO_MSG_CW_DECODE ); push_history!( - histories.snapshot_noaa_history(), - DecodedMessage::NoaaImage, - AUDIO_MSG_NOAA_IMAGE + histories.snapshot_wxsat_history(), + DecodedMessage::WxsatImage, + AUDIO_MSG_WXSAT_IMAGE ); (blob, count) @@ -3278,7 +3287,7 @@ async fn handle_audio_client( DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE, DecodedMessage::Ft2(_) => AUDIO_MSG_FT2_DECODE, DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE, - DecodedMessage::NoaaImage(_) => AUDIO_MSG_NOAA_IMAGE, + DecodedMessage::WxsatImage(_) => AUDIO_MSG_WXSAT_IMAGE, }; if let Ok(json) = serde_json::to_vec(&msg) { if let Err(e) = write_audio_msg(&mut writer_for_rx, msg_type, &json).await { @@ -3306,7 +3315,7 @@ async fn handle_audio_client( DecodedMessage::Ft4(_) => AUDIO_MSG_FT4_DECODE, DecodedMessage::Ft2(_) => AUDIO_MSG_FT2_DECODE, DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE, - DecodedMessage::NoaaImage(_) => AUDIO_MSG_NOAA_IMAGE, + DecodedMessage::WxsatImage(_) => AUDIO_MSG_WXSAT_IMAGE, }; if let Ok(json) = serde_json::to_vec(&msg) { if let Err(e) = write_audio_msg(&mut writer_for_rx, msg_type, &json).await { diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index ffde310..d11491f 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -794,22 +794,22 @@ fn spawn_rig_audio_stack( } })); - // Spawn NOAA APT decoder task - let noaa_pcm_rx = pcm_tx.subscribe(); - let noaa_state_rx = state_rx.clone(); - let noaa_decode_tx = decode_tx.clone(); - let noaa_sr = rig_cfg.audio.sample_rate; - let noaa_ch = rig_cfg.audio.channels; - let noaa_shutdown_rx = shutdown_rx.clone(); - let noaa_histories = histories.clone(); - let noaa_output_dir = dirs::cache_dir() + // Spawn weather satellite APT decoder task + let wxsat_pcm_rx = pcm_tx.subscribe(); + let wxsat_state_rx = state_rx.clone(); + let wxsat_decode_tx = decode_tx.clone(); + let wxsat_sr = rig_cfg.audio.sample_rate; + let wxsat_ch = rig_cfg.audio.channels; + let wxsat_shutdown_rx = shutdown_rx.clone(); + let wxsat_histories = histories.clone(); + let wxsat_output_dir = dirs::cache_dir() .unwrap_or_else(|| std::path::PathBuf::from(".cache")) .join("trx-rs") - .join("noaa"); + .join("wxsat"); handles.push(tokio::spawn(async move { tokio::select! { - _ = audio::run_noaa_decoder(noaa_sr, noaa_ch as u16, noaa_pcm_rx, noaa_state_rx, noaa_decode_tx, noaa_histories, noaa_output_dir) => {} - _ = wait_for_shutdown(noaa_shutdown_rx) => {} + _ = audio::run_wxsat_decoder(wxsat_sr, wxsat_ch as u16, wxsat_pcm_rx, wxsat_state_rx, wxsat_decode_tx, wxsat_histories, wxsat_output_dir) => {} + _ = wait_for_shutdown(wxsat_shutdown_rx) => {} } })); }