Skip to main content

zerodds_recorder/
format.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Format-Konstanten + Wire-Strukturen fuer `.zddsrec`.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9/// File-Magic "ZDDS".
10pub const ZDDSREC_MAGIC: [u8; 4] = *b"ZDDS";
11/// Frame-Marker innerhalb des Streams.
12pub const FRAME_MAGIC: u8 = b'F';
13/// Format-Version. Inkompatible Aenderungen heben den Wert.
14pub const ZDDSREC_VERSION: u32 = 1;
15
16/// Sample-Kind nach DDS-Spec §2.2.4.4.5.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18#[repr(u8)]
19pub enum SampleKind {
20    /// `ALIVE` — Reader sieht ein lebendes Sample.
21    Alive = 0,
22    /// `NOT_ALIVE_DISPOSED` — Writer hat dispose()'d.
23    NotAliveDisposed = 1,
24    /// `NOT_ALIVE_UNREGISTERED` — Writer hat unregister()'d.
25    NotAliveUnregistered = 2,
26}
27
28impl SampleKind {
29    /// Wire-Wert.
30    #[must_use]
31    pub fn to_u8(self) -> u8 {
32        self as u8
33    }
34
35    /// Parsed das u8-Wire-Byte. Returns None bei Unknown.
36    #[must_use]
37    pub fn from_u8(v: u8) -> Option<Self> {
38        match v {
39            0 => Some(Self::Alive),
40            1 => Some(Self::NotAliveDisposed),
41            2 => Some(Self::NotAliveUnregistered),
42            _ => None,
43        }
44    }
45}
46
47/// Participant-Entry im Header.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ParticipantEntry {
50    /// 16-Byte RTPS-GUID-Prefix + EntityId (full GUID).
51    pub guid: [u8; 16],
52    /// Logischer Name (z.B. ROS-2 Node-Name).
53    pub name: String,
54}
55
56/// Topic-Entry im Header.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct TopicEntry {
59    /// DDS-Topic-Name.
60    pub name: String,
61    /// Type-Name (z.B. "std_msgs::msg::String").
62    pub type_name: String,
63}
64
65/// Header eines `.zddsrec`-Files.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct Header {
68    /// UNIX-Epoch-Timestamp in Nanosekunden — Frame-Timestamps sind
69    /// Deltas relativ dazu.
70    pub time_base_unix_ns: i64,
71    /// Bekannte Participants (Reihenfolge ist Frame-Index).
72    pub participants: Vec<ParticipantEntry>,
73    /// Bekannte Topics (Reihenfolge ist Frame-Index).
74    pub topics: Vec<TopicEntry>,
75}
76
77impl Header {
78    /// Serialisiert den Header inkl. Magic + Version.
79    pub fn write(&self, out: &mut Vec<u8>) {
80        out.extend_from_slice(&ZDDSREC_MAGIC);
81        out.extend_from_slice(&ZDDSREC_VERSION.to_le_bytes());
82        out.extend_from_slice(&self.time_base_unix_ns.to_le_bytes());
83        out.extend_from_slice(
84            &u32::try_from(self.participants.len())
85                .unwrap_or(u32::MAX)
86                .to_le_bytes(),
87        );
88        out.extend_from_slice(
89            &u32::try_from(self.topics.len())
90                .unwrap_or(u32::MAX)
91                .to_le_bytes(),
92        );
93        for p in &self.participants {
94            out.extend_from_slice(&p.guid);
95            write_string(out, &p.name);
96        }
97        for t in &self.topics {
98            write_string(out, &t.type_name);
99            write_string(out, &t.name);
100        }
101    }
102}
103
104fn write_string(out: &mut Vec<u8>, s: &str) {
105    let bytes = s.as_bytes();
106    out.extend_from_slice(&u32::try_from(bytes.len()).unwrap_or(u32::MAX).to_le_bytes());
107    out.extend_from_slice(bytes);
108}
109
110/// Frame im Sample-Stream.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct Frame {
113    /// Nanosekunden-Delta zum Header-`time_base_unix_ns`.
114    pub timestamp_delta_ns: i64,
115    /// Index in [`Header::participants`].
116    pub participant_idx: u32,
117    /// Index in [`Header::topics`].
118    pub topic_idx: u32,
119    /// DDS-Sample-Kind (Alive/Disposed/Unregistered).
120    pub sample_kind: SampleKind,
121    /// CDR-encoded Payload-Bytes.
122    pub payload: Vec<u8>,
123}
124
125impl Frame {
126    /// Serialisiert den Frame.
127    pub fn write(&self, out: &mut Vec<u8>) {
128        out.push(FRAME_MAGIC);
129        out.extend_from_slice(&self.timestamp_delta_ns.to_le_bytes());
130        out.extend_from_slice(&self.participant_idx.to_le_bytes());
131        out.extend_from_slice(&self.topic_idx.to_le_bytes());
132        out.push(self.sample_kind.to_u8());
133        let len = u32::try_from(self.payload.len()).unwrap_or(u32::MAX);
134        out.extend_from_slice(&len.to_le_bytes());
135        out.extend_from_slice(&self.payload);
136    }
137}
138
139/// Borrowed-Variante des Frames — nuetzlich fuer Replay ohne Copy.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct FrameView<'a> {
142    /// Nanosekunden-Delta zum Header-`time_base_unix_ns`.
143    pub timestamp_delta_ns: i64,
144    /// Index in [`Header::participants`].
145    pub participant_idx: u32,
146    /// Index in [`Header::topics`].
147    pub topic_idx: u32,
148    /// DDS-Sample-Kind.
149    pub sample_kind: SampleKind,
150    /// Borrowed Payload.
151    pub payload: &'a [u8],
152}
153
154impl FrameView<'_> {
155    /// Konvertiert in eine owned [`Frame`].
156    #[must_use]
157    pub fn to_owned(&self) -> Frame {
158        Frame {
159            timestamp_delta_ns: self.timestamp_delta_ns,
160            participant_idx: self.participant_idx,
161            topic_idx: self.topic_idx,
162            sample_kind: self.sample_kind,
163            payload: self.payload.to_vec(),
164        }
165    }
166}
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used)] // tests duerfen unwrap nutzen.
170mod tests {
171    use super::*;
172
173    #[test]
174    fn sample_kind_roundtrip() {
175        for k in [
176            SampleKind::Alive,
177            SampleKind::NotAliveDisposed,
178            SampleKind::NotAliveUnregistered,
179        ] {
180            assert_eq!(SampleKind::from_u8(k.to_u8()), Some(k));
181        }
182        assert_eq!(SampleKind::from_u8(99), None);
183    }
184
185    #[test]
186    fn header_writes_magic_and_version() {
187        let h = Header {
188            time_base_unix_ns: 1_700_000_000_000_000_000,
189            participants: Vec::new(),
190            topics: Vec::new(),
191        };
192        let mut out = Vec::new();
193        h.write(&mut out);
194        assert_eq!(&out[0..4], &ZDDSREC_MAGIC);
195        assert_eq!(
196            u32::from_le_bytes(out[4..8].try_into().unwrap()),
197            ZDDSREC_VERSION
198        );
199    }
200}