Files
trx-rs/src/decoders/trx-wxsat/src/lrpt/cadu.rs
T
sjg ba48de2d30
Sync docs to Wiki / wiki (push) Has been cancelled
Initial commit
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-05-17 23:25:14 +02:00

249 lines
7.5 KiB
Rust

// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
//! CCSDS CADU (Channel Access Data Unit) frame synchronisation and extraction.
//!
//! Meteor-M LRPT uses CCSDS-compatible framing:
//! - Attached Sync Marker (ASM): `0x1ACFFC1D` (32 bits)
//! - CADU length: 1024 bytes (8192 bits) including ASM
//! - Rate 1/2 convolutional coding (Viterbi decoded upstream)
//! - Reed-Solomon (255, 223) error correction
//!
//! The framer correlates against the ASM pattern to find frame boundaries,
//! then extracts fixed-length CADUs.
/// CCSDS Attached Sync Marker for Meteor-M LRPT.
const ASM: [u8; 4] = [0x1A, 0xCF, 0xFC, 0x1D];
/// Total CADU length in bytes (including 4-byte ASM).
pub const CADU_LEN: usize = 1024;
/// CADU payload length (excluding ASM).
pub const CADU_PAYLOAD_LEN: usize = CADU_LEN - 4;
/// Generate the CCSDS pseudo-random derandomization sequence.
///
/// Polynomial: x^8 + x^7 + x^5 + x^3 + 1, initial state 0xFF.
/// The sequence is XOR'd with CADU bytes after the ASM to undo the
/// on-board randomization applied before transmission.
fn ccsds_derandomize(data: &mut [u8]) {
let mut sr: u8 = 0xFF;
for byte in data.iter_mut() {
*byte ^= sr;
for _ in 0..8 {
let feedback = ((sr >> 7) ^ (sr >> 5) ^ (sr >> 3) ^ sr) & 1;
sr = (sr << 1) | feedback;
}
}
}
/// A complete CADU frame (1024 bytes including ASM).
#[derive(Clone)]
pub struct Cadu {
pub data: Vec<u8>,
}
impl Cadu {
/// VCDU header: spacecraft ID (10 bits starting at byte 4).
pub fn spacecraft_id(&self) -> u16 {
if self.data.len() < 6 {
return 0;
}
((self.data[4] as u16) << 2) | ((self.data[5] as u16) >> 6)
}
/// VCDU header: virtual channel ID (6 bits).
pub fn vcid(&self) -> u8 {
if self.data.len() < 6 {
return 0;
}
self.data[5] & 0x3F
}
/// VCDU counter (24 bits, bytes 6-8).
pub fn vcdu_counter(&self) -> u32 {
if self.data.len() < 9 {
return 0;
}
((self.data[6] as u32) << 16) | ((self.data[7] as u32) << 8) | (self.data[8] as u32)
}
/// MPDU payload region (after VCDU primary header).
pub fn mpdu_payload(&self) -> &[u8] {
if self.data.len() < 16 {
return &[];
}
// VCDU primary header = 6 bytes, MPDU header pointer = 2 bytes
// Payload starts at offset 4 (ASM) + 6 (VCDU hdr) + 2 (MPDU ptr) = 12
&self.data[12..]
}
}
/// Accumulates soft symbols, performs Viterbi-like hard decisions, and
/// searches for ASM to extract complete CADUs.
pub struct CaduFramer {
/// Bit accumulation buffer.
bit_buf: Vec<u8>,
/// Byte accumulation buffer for frame extraction.
byte_buf: Vec<u8>,
/// Whether we are locked to a frame boundary.
locked: bool,
/// Bytes remaining in the current frame.
remaining: usize,
}
impl Default for CaduFramer {
fn default() -> Self {
Self::new()
}
}
impl CaduFramer {
pub fn new() -> Self {
Self {
bit_buf: Vec::new(),
byte_buf: Vec::new(),
locked: false,
remaining: 0,
}
}
/// Push soft symbols (interleaved I/Q) and extract any complete CADUs.
///
/// Soft symbols are hard-decided (threshold at 0.0) and packed into bytes.
pub fn push(&mut self, symbols: &[f32]) -> Vec<Cadu> {
// Hard-decide symbols to bits
for &sym in symbols {
self.bit_buf.push(if sym >= 0.0 { 1 } else { 0 });
}
// Pack bits into bytes
while self.bit_buf.len() >= 8 {
let byte = (self.bit_buf[0] << 7)
| (self.bit_buf[1] << 6)
| (self.bit_buf[2] << 5)
| (self.bit_buf[3] << 4)
| (self.bit_buf[4] << 3)
| (self.bit_buf[5] << 2)
| (self.bit_buf[6] << 1)
| self.bit_buf[7];
self.byte_buf.push(byte);
self.bit_buf.drain(..8);
}
let mut cadus = Vec::new();
self.extract_frames(&mut cadus);
cadus
}
fn extract_frames(&mut self, cadus: &mut Vec<Cadu>) {
loop {
if self.locked {
if self.byte_buf.len() >= self.remaining {
// Collect the rest of the frame
let frame_bytes: Vec<u8> = self.byte_buf.drain(..self.remaining).collect();
// Prepend ASM to make a complete CADU
let mut data = ASM.to_vec();
data.extend_from_slice(&frame_bytes);
if data.len() == CADU_LEN {
// Derandomize payload (everything after 4-byte ASM)
ccsds_derandomize(&mut data[4..]);
cadus.push(Cadu { data });
}
self.locked = false;
continue;
}
break;
}
// Search for ASM in the byte buffer
if let Some(pos) = find_asm(&self.byte_buf) {
// Discard bytes before ASM
self.byte_buf.drain(..pos);
// Skip the 4 ASM bytes
if self.byte_buf.len() >= 4 {
self.byte_buf.drain(..4);
self.locked = true;
self.remaining = CADU_LEN - 4; // payload bytes needed
continue;
}
break;
}
// No ASM found; keep last 3 bytes (partial ASM might straddle boundary)
if self.byte_buf.len() > 3 {
let keep = self.byte_buf.len().saturating_sub(3);
self.byte_buf.drain(..keep);
}
break;
}
}
pub fn reset(&mut self) {
self.bit_buf.clear();
self.byte_buf.clear();
self.locked = false;
self.remaining = 0;
}
}
/// Find the ASM pattern in a byte buffer; returns the offset if found.
fn find_asm(buf: &[u8]) -> Option<usize> {
if buf.len() < 4 {
return None;
}
(0..=(buf.len() - 4)).find(|&i| {
buf[i] == ASM[0] && buf[i + 1] == ASM[1] && buf[i + 2] == ASM[2] && buf[i + 3] == ASM[3]
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_asm() {
let buf = [0x00, 0x1A, 0xCF, 0xFC, 0x1D, 0x00];
assert_eq!(find_asm(&buf), Some(1));
}
#[test]
fn test_find_asm_at_start() {
let buf = [0x1A, 0xCF, 0xFC, 0x1D, 0x00];
assert_eq!(find_asm(&buf), Some(0));
}
#[test]
fn test_find_asm_not_found() {
let buf = [0x00, 0x01, 0x02, 0x03, 0x04];
assert_eq!(find_asm(&buf), None);
}
#[test]
fn test_derandomize_roundtrip() {
let original = vec![0xAB; CADU_PAYLOAD_LEN];
let mut data = original.clone();
// Randomize
ccsds_derandomize(&mut data);
// Should differ from original
assert_ne!(data, original);
// Derandomize again (same sequence) should restore
ccsds_derandomize(&mut data);
assert_eq!(data, original);
}
#[test]
fn test_cadu_spacecraft_id() {
let mut data = vec![0u8; CADU_LEN];
// ASM
data[0..4].copy_from_slice(&ASM);
// Spacecraft ID = 0x0C3 (195) in bits [4*8..4*8+10]
// byte 4 = 0x30 (top 8 bits: 00110000), byte 5 bits 7-6 = 11
data[4] = 0x30;
data[5] = 0xC0;
let cadu = Cadu { data };
assert_eq!(cadu.spacecraft_id(), 0xC3);
}
}