pub mod hdds;
#[cfg(feature = "mcap")]
mod mcap_export;
pub use hdds::{
FileHeader, FormatError, HddsFormat, HddsReader, HddsWriter, IndexEntry, SegmentHeader,
FORMAT_VERSION, MAGIC,
};
#[cfg(feature = "mcap")]
pub use mcap_export::{convert_hdds_to_mcap, McapError, McapExporter};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub timestamp_nanos: u64,
pub topic_name: String,
pub type_name: String,
pub writer_guid: String,
pub sequence_number: u64,
pub payload: Vec<u8>,
pub qos_hash: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingMetadata {
pub start_time: String,
pub domain_id: u32,
pub hostname: Option<String>,
pub hdds_version: String,
pub topics: Vec<TopicInfo>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopicInfo {
pub name: String,
pub type_name: String,
pub message_count: u64,
pub reliability: String,
pub durability: String,
}
impl Default for RecordingMetadata {
fn default() -> Self {
Self {
start_time: chrono::Utc::now().to_rfc3339(),
domain_id: 0,
hostname: hostname::get().ok().and_then(|h| h.into_string().ok()),
hdds_version: env!("CARGO_PKG_VERSION").to_string(),
topics: Vec::new(),
description: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Hdds,
Mcap,
}
impl OutputFormat {
pub fn from_extension(path: &std::path::Path) -> Option<Self> {
match path.extension().and_then(|e| e.to_str()) {
Some("hdds") => Some(Self::Hdds),
Some("mcap") => Some(Self::Mcap),
_ => None,
}
}
pub fn extension(&self) -> &'static str {
match self {
Self::Hdds => "hdds",
Self::Mcap => "mcap",
}
}
}
mod hostname {
pub fn get() -> std::io::Result<std::ffi::OsString> {
#[cfg(unix)]
{
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let mut buf = vec![0u8; 256];
let ret = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut i8, buf.len()) };
if ret == 0 {
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf.truncate(len);
Ok(OsString::from_vec(buf))
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(not(unix))]
{
Ok(std::ffi::OsString::from("unknown"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_format_from_extension() {
use std::path::Path;
assert_eq!(
OutputFormat::from_extension(Path::new("test.hdds")),
Some(OutputFormat::Hdds)
);
assert_eq!(
OutputFormat::from_extension(Path::new("test.mcap")),
Some(OutputFormat::Mcap)
);
assert_eq!(OutputFormat::from_extension(Path::new("test.txt")), None);
}
#[test]
fn test_recording_metadata_default() {
let meta = RecordingMetadata::default();
assert_eq!(meta.domain_id, 0);
assert!(meta.topics.is_empty());
}
#[test]
fn test_message_serialization() {
let msg = Message {
timestamp_nanos: 1000,
topic_name: "Temperature".into(),
type_name: "sensor_msgs/Temperature".into(),
writer_guid: "0102030405060708090a0b0c00000302".into(),
sequence_number: 1,
payload: vec![1, 2, 3, 4],
qos_hash: 0x12345678,
};
let json = serde_json::to_string(&msg).expect("serialize");
let decoded: Message = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded.topic_name, "Temperature");
assert_eq!(decoded.sequence_number, 1);
}
}