use serde::Deserialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SequenceFlags {
Continuation = 0,
First = 1,
Last = 2,
Unsegmented = 3,
}
impl SequenceFlags {
fn from_bits(b: u8) -> SequenceFlags {
match b & 0b11 {
0 => SequenceFlags::Continuation,
1 => SequenceFlags::First,
2 => SequenceFlags::Last,
_ => SequenceFlags::Unsegmented,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PrimaryHeader {
pub version: u8,
pub is_telecommand: bool,
pub secondary_header: bool,
pub apid: u16,
pub sequence_flags: SequenceFlags,
pub sequence_count: u16,
pub data_length_field: u16,
}
pub fn encode_packet(
version: u8,
is_telecommand: bool,
secondary_header: bool,
apid: u16,
sequence_flags: SequenceFlags,
sequence_count: u16,
data: &[u8],
) -> Result<Vec<u8>, String> {
if version > 0b111 {
return Err("version exceeds 3 bits".to_string());
}
if apid > 0x07FF {
return Err("APID exceeds 11 bits (0..=2047)".to_string());
}
if sequence_count > 0x3FFF {
return Err("sequence_count exceeds 14 bits (0..=16383)".to_string());
}
if data.is_empty() {
return Err("data field must be at least one octet".to_string());
}
if data.len() > 65536 {
return Err("data field exceeds 65536 octets".to_string());
}
let word1: u16 = ((version as u16) << 13)
| ((is_telecommand as u16) << 12)
| ((secondary_header as u16) << 11)
| apid;
let word2: u16 = ((sequence_flags as u16) << 14) | sequence_count;
let data_length_field: u16 = (data.len() - 1) as u16;
let mut out = Vec::with_capacity(6 + data.len());
out.extend_from_slice(&word1.to_be_bytes());
out.extend_from_slice(&word2.to_be_bytes());
out.extend_from_slice(&data_length_field.to_be_bytes());
out.extend_from_slice(data);
Ok(out)
}
pub fn decode_packet(bytes: &[u8]) -> Result<(PrimaryHeader, Vec<u8>), String> {
if bytes.len() < 6 {
return Err(format!(
"packet shorter than the 6-octet header: {} bytes",
bytes.len()
));
}
let word1 = u16::from_be_bytes([bytes[0], bytes[1]]);
let word2 = u16::from_be_bytes([bytes[2], bytes[3]]);
let data_length_field = u16::from_be_bytes([bytes[4], bytes[5]]);
let header = PrimaryHeader {
version: (word1 >> 13) as u8 & 0b111,
is_telecommand: (word1 >> 12) & 0b1 == 1,
secondary_header: (word1 >> 11) & 0b1 == 1,
apid: word1 & 0x07FF,
sequence_flags: SequenceFlags::from_bits((word2 >> 14) as u8),
sequence_count: word2 & 0x3FFF,
data_length_field,
};
let data_len = data_length_field as usize + 1;
let total = 6 + data_len;
if bytes.len() != total {
return Err(format!(
"declared data length {data_len} (+6 header) = {total} octets, but buffer is {}",
bytes.len()
));
}
Ok((header, bytes[6..].to_vec()))
}
fn sp_default_apid() -> u16 {
100
}
fn sp_default_count() -> u16 {
3
}
fn sp_default_data_len() -> usize {
16
}
#[derive(Deserialize)]
pub struct SpacePacketScenario {
#[serde(default = "sp_default_apid")]
pub apid: u16,
#[serde(default)]
pub telecommand: bool,
#[serde(default = "sp_default_count")]
pub packet_count: u16,
#[serde(default = "sp_default_data_len")]
pub data_len: usize,
}
impl SpacePacketScenario {
pub fn run_json(&self) -> Result<(String, String), String> {
if self.apid > 0x07FF {
return Err("apid must be in 0..=2047".to_string());
}
if self.packet_count == 0 {
return Err("packet_count must be >= 1".to_string());
}
if self.data_len == 0 || self.data_len > 65536 {
return Err("data_len must be in 1..=65536".to_string());
}
let mut packets = Vec::new();
let mut total_bytes = 0usize;
let mut round_trip_ok = true;
for i in 0..self.packet_count {
let data: Vec<u8> = (0..self.data_len)
.map(|k| ((i as usize + k) & 0xFF) as u8)
.collect();
let flags = if self.packet_count == 1 {
SequenceFlags::Unsegmented
} else if i == 0 {
SequenceFlags::First
} else if i == self.packet_count - 1 {
SequenceFlags::Last
} else {
SequenceFlags::Continuation
};
let bytes = encode_packet(0, self.telecommand, false, self.apid, flags, i, &data)?;
total_bytes += bytes.len();
let (hdr, decoded) = decode_packet(&bytes)?;
if decoded != data || hdr.apid != self.apid || hdr.sequence_count != i {
round_trip_ok = false;
}
if i == 0 {
packets.push(serde_json::json!({
"index": i,
"apid": hdr.apid,
"type": if hdr.is_telecommand { "TC" } else { "TM" },
"sequence_flags": format!("{:?}", hdr.sequence_flags),
"sequence_count": hdr.sequence_count,
"data_length_field": hdr.data_length_field,
"total_octets": bytes.len(),
"primary_header_hex": bytes[..6].iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "),
}));
}
}
if !round_trip_ok {
return Err("internal error: encode/decode round trip mismatch".to_string());
}
let json = serde_json::json!({
"kind": "space-packet",
"label": "CCSDS 133.0-B Space Packet primary-header framing — exact, \
deterministic bit layout, encode↔decode round-trip verified; \
NOT a conformance certification (no secondary header / CRC / \
segmentation logic beyond the flags)",
"apid": self.apid,
"type": if self.telecommand { "TC" } else { "TM" },
"packet_count": self.packet_count,
"data_len_octets": self.data_len,
"total_stream_octets": total_bytes,
"round_trip_exact": round_trip_ok,
"first_packet": packets.first(),
});
let summary = format!(
"space-packet: framed {} CCSDS-133.0 {} packet(s) APID {}, {} octets total, \
round-trip exact ({})",
self.packet_count,
if self.telecommand { "TC" } else { "TM" },
self.apid,
total_bytes,
round_trip_ok
);
let json = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
Ok((json, summary))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primary_header_bits_match_the_ccsds_133_layout() {
let pkt = encode_packet(
0,
false,
false,
0x123,
SequenceFlags::Unsegmented,
0,
&[0xDE, 0xAD, 0xBE, 0xEF],
)
.unwrap();
assert_eq!(&pkt[..6], &[0x01, 0x23, 0xC0, 0x00, 0x00, 0x03]);
assert_eq!(&pkt[6..], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn primary_header_matches_published_independent_test_vectors() {
struct Vector {
tc: bool,
shf: bool,
apid: u16,
seqflags: SequenceFlags,
seqcount: u16,
dlen: u16,
want: [u8; 6],
}
let cases = [
Vector {
tc: true,
shf: true,
apid: 0x002,
seqflags: SequenceFlags::First,
seqcount: 0x0034,
dlen: 0x0016,
want: [0x18, 0x02, 0x40, 0x34, 0x00, 0x16],
},
Vector {
tc: true,
shf: true,
apid: 2047,
seqflags: SequenceFlags::Unsegmented,
seqcount: 16383,
dlen: 65535,
want: [0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
},
Vector {
tc: true,
shf: false,
apid: 0x001,
seqflags: SequenceFlags::Unsegmented,
seqcount: 0,
dlen: 0,
want: [0x10, 0x01, 0xC0, 0x00, 0x00, 0x00],
},
Vector {
tc: true,
shf: false,
apid: 0x002,
seqflags: SequenceFlags::Unsegmented,
seqcount: 5,
dlen: 3,
want: [0x10, 0x02, 0xC0, 0x05, 0x00, 0x03],
},
Vector {
tc: false,
shf: false,
apid: 2047,
seqflags: SequenceFlags::Unsegmented,
seqcount: 16383,
dlen: 0,
want: [0x07, 0xFF, 0xFF, 0xFF, 0x00, 0x00],
},
];
for c in &cases {
let data = vec![0u8; c.dlen as usize + 1];
let pkt = encode_packet(0, c.tc, c.shf, c.apid, c.seqflags, c.seqcount, &data).unwrap();
assert_eq!(
&pkt[..6],
&c.want,
"header bytes for apid={} seq={} dlen={}",
c.apid,
c.seqcount,
c.dlen
);
let (h, d) = decode_packet(&pkt).unwrap();
assert_eq!(h.is_telecommand, c.tc);
assert_eq!(h.secondary_header, c.shf);
assert_eq!(h.apid, c.apid);
assert_eq!(h.sequence_flags, c.seqflags);
assert_eq!(h.sequence_count, c.seqcount);
assert_eq!(h.data_length_field, c.dlen);
assert_eq!(d.len(), c.dlen as usize + 1);
}
}
#[test]
fn telecommand_and_secondary_header_flags_set_their_bits() {
let pkt = encode_packet(0, true, true, 0, SequenceFlags::First, 0, &[0]).unwrap();
assert_eq!(pkt[0], 0b0001_1000);
assert_eq!(pkt[2], 0b0100_0000);
}
#[test]
fn encode_decode_round_trips_all_fields() {
let data: Vec<u8> = (0..50).collect();
let pkt = encode_packet(0, false, false, 1234, SequenceFlags::Last, 9001, &data).unwrap();
let (h, d) = decode_packet(&pkt).unwrap();
assert_eq!(h.apid, 1234);
assert_eq!(h.sequence_flags, SequenceFlags::Last);
assert_eq!(h.sequence_count, 9001);
assert_eq!(h.data_length_field, 49);
assert!(!h.is_telecommand);
assert_eq!(d, data);
}
#[test]
fn data_length_field_is_octets_minus_one() {
let pkt = encode_packet(0, false, false, 0, SequenceFlags::Unsegmented, 0, &[7]).unwrap();
assert_eq!(u16::from_be_bytes([pkt[4], pkt[5]]), 0);
assert_eq!(pkt.len(), 7);
}
#[test]
fn out_of_range_fields_and_empty_data_are_rejected() {
assert!(encode_packet(0, false, false, 2048, SequenceFlags::First, 0, &[0]).is_err());
assert!(encode_packet(0, false, false, 0, SequenceFlags::First, 16384, &[0]).is_err());
assert!(encode_packet(0, false, false, 0, SequenceFlags::First, 0, &[]).is_err());
}
#[test]
fn decode_rejects_truncated_and_length_mismatched_buffers() {
assert!(decode_packet(&[0, 1, 2]).is_err()); let mut pkt = encode_packet(
0,
false,
false,
0,
SequenceFlags::Unsegmented,
0,
&[1, 2, 3],
)
.unwrap();
pkt.push(0xFF); assert!(decode_packet(&pkt).is_err());
}
#[test]
fn scenario_runs_reproducibly_and_is_honest() {
let scn = SpacePacketScenario {
apid: 100,
telecommand: false,
packet_count: 3,
data_len: 16,
};
let (j1, _s) = scn.run_json().unwrap();
let (j2, _s) = scn.run_json().unwrap();
assert_eq!(j1, j2, "framing must be deterministic");
let v: serde_json::Value = serde_json::from_str(&j1).unwrap();
assert_eq!(v["kind"], "space-packet");
assert_eq!(v["round_trip_exact"], true);
assert_eq!(v["total_stream_octets"], (6 + 16) * 3);
assert!(v["label"].as_str().unwrap().contains("CCSDS 133.0"));
assert!(!j1.contains("VALIDATED"));
}
}