hdds_recording/format/
mod.rs1pub mod hdds;
11
12#[cfg(feature = "mcap")]
13mod mcap_export;
14
15pub use hdds::{
16 FileHeader, FormatError, HddsFormat, HddsReader, HddsWriter, IndexEntry, SegmentHeader,
17 FORMAT_VERSION, MAGIC,
18};
19
20#[cfg(feature = "mcap")]
21pub use mcap_export::{convert_hdds_to_mcap, McapError, McapExporter};
22
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Message {
28 pub timestamp_nanos: u64,
30
31 pub topic_name: String,
33
34 pub type_name: String,
36
37 pub writer_guid: String,
39
40 pub sequence_number: u64,
42
43 pub payload: Vec<u8>,
45
46 pub qos_hash: u32,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RecordingMetadata {
53 pub start_time: String,
55
56 pub domain_id: u32,
58
59 pub hostname: Option<String>,
61
62 pub hdds_version: String,
64
65 pub topics: Vec<TopicInfo>,
67
68 pub description: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct TopicInfo {
75 pub name: String,
77
78 pub type_name: String,
80
81 pub message_count: u64,
83
84 pub reliability: String,
86 pub durability: String,
87}
88
89impl Default for RecordingMetadata {
90 fn default() -> Self {
91 Self {
92 start_time: chrono::Utc::now().to_rfc3339(),
93 domain_id: 0,
94 hostname: hostname::get().ok().and_then(|h| h.into_string().ok()),
95 hdds_version: env!("CARGO_PKG_VERSION").to_string(),
96 topics: Vec::new(),
97 description: None,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum OutputFormat {
105 Hdds,
107 Mcap,
109}
110
111impl OutputFormat {
112 pub fn from_extension(path: &std::path::Path) -> Option<Self> {
114 match path.extension().and_then(|e| e.to_str()) {
115 Some("hdds") => Some(Self::Hdds),
116 Some("mcap") => Some(Self::Mcap),
117 _ => None,
118 }
119 }
120
121 pub fn extension(&self) -> &'static str {
123 match self {
124 Self::Hdds => "hdds",
125 Self::Mcap => "mcap",
126 }
127 }
128}
129
130mod hostname {
132 pub fn get() -> std::io::Result<std::ffi::OsString> {
133 #[cfg(unix)]
134 {
135 use std::ffi::OsString;
136 use std::os::unix::ffi::OsStringExt;
137
138 let mut buf = vec![0u8; 256];
139 let ret = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut i8, buf.len()) };
140 if ret == 0 {
141 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
142 buf.truncate(len);
143 Ok(OsString::from_vec(buf))
144 } else {
145 Err(std::io::Error::last_os_error())
146 }
147 }
148 #[cfg(not(unix))]
149 {
150 Ok(std::ffi::OsString::from("unknown"))
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_output_format_from_extension() {
161 use std::path::Path;
162
163 assert_eq!(
164 OutputFormat::from_extension(Path::new("test.hdds")),
165 Some(OutputFormat::Hdds)
166 );
167 assert_eq!(
168 OutputFormat::from_extension(Path::new("test.mcap")),
169 Some(OutputFormat::Mcap)
170 );
171 assert_eq!(OutputFormat::from_extension(Path::new("test.txt")), None);
172 }
173
174 #[test]
175 fn test_recording_metadata_default() {
176 let meta = RecordingMetadata::default();
177 assert_eq!(meta.domain_id, 0);
178 assert!(meta.topics.is_empty());
179 }
180
181 #[test]
182 fn test_message_serialization() {
183 let msg = Message {
184 timestamp_nanos: 1000,
185 topic_name: "Temperature".into(),
186 type_name: "sensor_msgs/Temperature".into(),
187 writer_guid: "0102030405060708090a0b0c00000302".into(),
188 sequence_number: 1,
189 payload: vec![1, 2, 3, 4],
190 qos_hash: 0x12345678,
191 };
192
193 let json = serde_json::to_string(&msg).expect("serialize");
194 let decoded: Message = serde_json::from_str(&json).expect("deserialize");
195
196 assert_eq!(decoded.topic_name, "Temperature");
197 assert_eq!(decoded.sequence_number, 1);
198 }
199}