use alloc::string::String;
use alloc::vec::Vec;
#[cfg(feature = "can")]
use super::fd::FdFrame;
use super::fd::{FdFlags, MAX_FD_DATA_LEN};
use crate::bus_logging::timestamp_to_seconds;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum FrameType {
Classic,
ClassicExtended,
FdSmall,
FdSmallExtended,
FdLarge,
FdLargeExtended,
}
impl FrameType {
const ALL: [Self; 6] = [
Self::Classic,
Self::ClassicExtended,
Self::FdSmall,
Self::FdSmallExtended,
Self::FdLarge,
Self::FdLargeExtended,
];
fn group_name(&self, bus_name: &str) -> String {
match self {
FrameType::Classic => alloc::format!("{}_DataFrame", bus_name),
FrameType::ClassicExtended => alloc::format!("{}_DataFrame_IDE", bus_name),
FrameType::FdSmall => alloc::format!("{}_DataFrame_FD", bus_name),
FrameType::FdSmallExtended => alloc::format!("{}_DataFrame_FD_IDE", bus_name),
FrameType::FdLarge => alloc::format!("{}_DataFrame_FD_DLC_over_8", bus_name),
FrameType::FdLargeExtended => {
alloc::format!("{}_DataFrame_FD_IDE_DLC_over_8", bus_name)
}
}
}
fn channel_name(&self) -> &'static str {
"CAN_DataFrame"
}
fn max_data_len(&self) -> usize {
match self {
FrameType::Classic | FrameType::ClassicExtended => 8,
FrameType::FdSmall | FrameType::FdSmallExtended => 8,
FrameType::FdLarge | FrameType::FdLargeExtended => 64,
}
}
fn from_frame(is_extended: bool, is_fd: bool, data_len: usize) -> Self {
match (is_extended, is_fd, data_len > 8) {
(false, false, _) => FrameType::Classic,
(true, false, _) => FrameType::ClassicExtended,
(false, true, false) => FrameType::FdSmall,
(true, true, false) => FrameType::FdSmallExtended,
(false, true, true) => FrameType::FdLarge,
(true, true, true) => FrameType::FdLargeExtended,
}
}
}
#[derive(Clone)]
struct RawFrame {
timestamp_s: f64,
can_id: u32,
dlc: u8,
data: [u8; MAX_FD_DATA_LEN],
data_len: usize,
fd_flags: FdFlags,
is_extended: bool,
is_fd: bool,
}
impl RawFrame {
fn new_classic(
timestamp_us: u64,
can_id: u32,
dlc: u8,
data: &[u8],
is_extended: bool,
) -> Self {
let mut frame_data = [0u8; MAX_FD_DATA_LEN];
let len = data.len().min(8);
frame_data[..len].copy_from_slice(&data[..len]);
Self {
timestamp_s: timestamp_to_seconds(timestamp_us),
can_id,
dlc,
data: frame_data,
data_len: len,
fd_flags: FdFlags::default(),
is_extended,
is_fd: false,
}
}
fn new_fd(
timestamp_us: u64,
can_id: u32,
dlc: u8,
data: &[u8],
flags: FdFlags,
is_extended: bool,
) -> Self {
let mut frame_data = [0u8; MAX_FD_DATA_LEN];
let len = data.len().min(MAX_FD_DATA_LEN);
frame_data[..len].copy_from_slice(&data[..len]);
Self {
timestamp_s: timestamp_to_seconds(timestamp_us),
can_id,
dlc,
data: frame_data,
data_len: len,
fd_flags: flags,
is_extended,
is_fd: true,
}
}
fn frame_type(&self) -> FrameType {
FrameType::from_frame(self.is_extended, self.is_fd, self.data_len)
}
fn to_dataframe_bytes(&self) -> Vec<u8> {
let max_data = self.frame_type().max_data_len();
let has_fd_flags = self.is_fd && self.data_len > 8;
let total_size = if has_fd_flags {
4 + 1 + 1 + max_data
} else {
4 + 1 + max_data
};
let mut bytes = Vec::with_capacity(total_size);
let id_with_flags = if self.is_extended {
self.can_id | 0x8000_0000
} else {
self.can_id
};
bytes.extend_from_slice(&id_with_flags.to_le_bytes());
bytes.push(self.dlc);
if has_fd_flags {
bytes.push(self.fd_flags.to_byte());
}
bytes.extend_from_slice(&self.data[..self.data_len.min(max_data)]);
bytes.resize(total_size, 0);
bytes
}
}
pub struct RawCanLogger<W: crate::writer::MdfWrite> {
writer: crate::MdfWriter<W>,
bus_name: String,
buffers: alloc::collections::BTreeMap<FrameType, Vec<RawFrame>>,
channel_groups: alloc::collections::BTreeMap<FrameType, String>,
initialized: bool,
}
impl RawCanLogger<crate::writer::VecWriter> {
pub fn new() -> crate::Result<Self> {
Self::with_source_name("CAN")
}
pub fn with_source_name(source_name: &str) -> crate::Result<Self> {
let writer = crate::MdfWriter::from_writer(crate::writer::VecWriter::new());
Ok(Self {
writer,
bus_name: String::from(source_name),
buffers: alloc::collections::BTreeMap::new(),
channel_groups: alloc::collections::BTreeMap::new(),
initialized: false,
})
}
pub fn with_bus_name(bus_name: &str) -> crate::Result<Self> {
Self::with_source_name(bus_name)
}
pub fn with_capacity(capacity: usize) -> crate::Result<Self> {
let writer =
crate::MdfWriter::from_writer(crate::writer::VecWriter::with_capacity(capacity));
Ok(Self {
writer,
bus_name: String::from("CAN"),
buffers: alloc::collections::BTreeMap::new(),
channel_groups: alloc::collections::BTreeMap::new(),
initialized: false,
})
}
pub fn finalize(mut self) -> crate::Result<Vec<u8>> {
self.flush_and_finalize()?;
Ok(self.writer.into_inner().into_inner())
}
}
#[cfg(feature = "std")]
impl RawCanLogger<crate::writer::FileWriter> {
pub fn new_file(path: &str) -> crate::Result<Self> {
Self::new_file_with_source_name(path, "CAN")
}
pub fn new_file_with_source_name(path: &str, source_name: &str) -> crate::Result<Self> {
let writer = crate::MdfWriter::new(path)?;
Ok(Self {
writer,
bus_name: String::from(source_name),
buffers: alloc::collections::BTreeMap::new(),
channel_groups: alloc::collections::BTreeMap::new(),
initialized: false,
})
}
pub fn new_file_with_bus_name(path: &str, bus_name: &str) -> crate::Result<Self> {
Self::new_file_with_source_name(path, bus_name)
}
pub fn finalize_file(mut self) -> crate::Result<()> {
self.flush_and_finalize()
}
}
#[cfg(feature = "std")]
impl RawCanLogger<crate::writer::VecWriter> {
pub fn from_file(path: &str) -> crate::Result<Self> {
Self::from_file_with_source_name(path, "CAN")
}
pub fn from_file_with_source_name(path: &str, source_name: &str) -> crate::Result<Self> {
use crate::DecodedValue;
use crate::index::{FileRangeReader, MdfIndex};
let index = MdfIndex::from_file(path)?;
let mut reader = FileRangeReader::new(path)?;
let mut logger = Self::with_source_name(source_name)?;
for (group_idx, group) in index.channel_groups.iter().enumerate() {
let mut timestamp_ch = None;
let mut dataframe_ch = None;
for (ch_idx, channel) in group.channels.iter().enumerate() {
if let Some(name) = &channel.name {
match name.as_str() {
"Timestamp" => timestamp_ch = Some(ch_idx),
"CAN_DataFrame" => dataframe_ch = Some(ch_idx),
_ => {}
}
}
}
let (ts_ch, df_ch) = match (timestamp_ch, dataframe_ch) {
(Some(t), Some(d)) => (t, d),
_ => continue, };
let timestamps = index.read_channel_values(group_idx, ts_ch, &mut reader)?;
let dataframes = index.read_channel_values(group_idx, df_ch, &mut reader)?;
for (ts_val, df_val) in timestamps.iter().zip(dataframes.iter()) {
let timestamp_us = match ts_val {
Some(DecodedValue::Float(s)) => (*s * 1_000_000.0) as u64,
Some(DecodedValue::UnsignedInteger(us)) => *us,
_ => continue,
};
let bytes = match df_val {
Some(DecodedValue::ByteArray(b)) => b,
_ => continue,
};
if bytes.len() < 5 {
continue;
}
let raw_id = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let is_extended = (raw_id & 0x8000_0000) != 0;
let can_id = raw_id & 0x1FFF_FFFF;
let dlc = bytes[4];
let data_len = super::fd::dlc_to_len(dlc).min(bytes.len() - 5);
let (fd_flags, data_start) = if data_len > 8 && bytes.len() > 6 {
(FdFlags::from_byte(bytes[5]), 6)
} else {
(FdFlags::default(), 5)
};
let data = &bytes[data_start..data_start + data_len.min(bytes.len() - data_start)];
let is_fd = data_len > 8 || fd_flags.brs() || fd_flags.esi();
let frame = if is_fd {
RawFrame::new_fd(timestamp_us, can_id, dlc, data, fd_flags, is_extended)
} else {
RawFrame::new_classic(timestamp_us, can_id, dlc, data, is_extended)
};
logger
.buffers
.entry(frame.frame_type())
.or_default()
.push(frame);
}
}
Ok(logger)
}
pub fn from_file_with_bus_name(path: &str, bus_name: &str) -> crate::Result<Self> {
Self::from_file_with_source_name(path, bus_name)
}
pub fn last_timestamp_us(&self) -> u64 {
self.buffers
.values()
.flat_map(|frames| frames.iter())
.map(|f| (f.timestamp_s * 1_000_000.0) as u64)
.max()
.unwrap_or(0)
}
pub fn loaded_frame_count(&self) -> usize {
self.buffers.values().map(|b| b.len()).sum()
}
}
impl<W: crate::writer::MdfWrite> RawCanLogger<W> {
pub fn set_source_name(&mut self, name: &str) {
self.bus_name = String::from(name);
}
pub fn set_bus_name(&mut self, name: &str) {
self.set_source_name(name);
}
#[inline]
pub fn log(&mut self, can_id: u32, timestamp_us: u64, data: &[u8]) -> bool {
let dlc = data.len().min(8) as u8;
let frame = RawFrame::new_classic(timestamp_us, can_id, dlc, data, false);
self.buffers
.entry(frame.frame_type())
.or_default()
.push(frame);
true
}
#[inline]
pub fn log_extended(&mut self, can_id: u32, timestamp_us: u64, data: &[u8]) -> bool {
let dlc = data.len().min(8) as u8;
let frame = RawFrame::new_classic(timestamp_us, can_id, dlc, data, true);
self.buffers
.entry(frame.frame_type())
.or_default()
.push(frame);
true
}
#[inline]
pub fn log_fd(&mut self, can_id: u32, timestamp_us: u64, data: &[u8], flags: FdFlags) -> bool {
let dlc = super::fd::len_to_dlc(data.len());
let frame = RawFrame::new_fd(timestamp_us, can_id, dlc, data, flags, false);
self.buffers
.entry(frame.frame_type())
.or_default()
.push(frame);
true
}
#[inline]
pub fn log_fd_extended(
&mut self,
can_id: u32,
timestamp_us: u64,
data: &[u8],
flags: FdFlags,
) -> bool {
let dlc = super::fd::len_to_dlc(data.len());
let frame = RawFrame::new_fd(timestamp_us, can_id, dlc, data, flags, true);
self.buffers
.entry(frame.frame_type())
.or_default()
.push(frame);
true
}
#[cfg(feature = "can")]
#[inline]
pub fn log_frame<F: embedded_can::Frame>(&mut self, timestamp_us: u64, frame: &F) -> bool {
match frame.id() {
embedded_can::Id::Standard(id) => {
self.log(id.as_raw() as u32, timestamp_us, frame.data())
}
embedded_can::Id::Extended(id) => {
self.log_extended(id.as_raw(), timestamp_us, frame.data())
}
}
}
#[cfg(feature = "can")]
#[inline]
pub fn log_fd_frame<F: FdFrame>(&mut self, timestamp_us: u64, frame: &F) -> bool {
if frame.is_fd() {
match frame.id() {
embedded_can::Id::Standard(id) => self.log_fd(
id.as_raw() as u32,
timestamp_us,
frame.data(),
frame.fd_flags(),
),
embedded_can::Id::Extended(id) => {
self.log_fd_extended(id.as_raw(), timestamp_us, frame.data(), frame.fd_flags())
}
}
} else {
match frame.id() {
embedded_can::Id::Standard(id) => {
self.log(id.as_raw() as u32, timestamp_us, frame.data())
}
embedded_can::Id::Extended(id) => {
self.log_extended(id.as_raw(), timestamp_us, frame.data())
}
}
}
}
pub fn flush(&mut self) -> crate::Result<()> {
if !self.initialized {
self.initialize_mdf()?;
}
for frame_type in FrameType::ALL {
if self.buffers.contains_key(&frame_type) {
self.write_frames(frame_type)?;
}
}
for buffer in self.buffers.values_mut() {
buffer.clear();
}
Ok(())
}
fn initialize_mdf(&mut self) -> crate::Result<()> {
use crate::DataType;
self.writer.init_mdf_file()?;
for &frame_type in self.buffers.keys() {
let group_name = frame_type.group_name(&self.bus_name);
let max_data_len = frame_type.max_data_len();
let dataframe_size =
if matches!(frame_type, FrameType::FdLarge | FrameType::FdLargeExtended) {
4 + 1 + 1 + max_data_len } else {
4 + 1 + max_data_len };
let cg = self.writer.add_channel_group(None, |_| {})?;
self.writer.set_channel_group_name(&cg, &group_name)?;
let source = crate::blocks::SourceBlock::can_bus();
self.writer
.set_channel_group_source(&cg, &source, Some(&self.bus_name))?;
let time_ch = self.writer.add_channel(&cg, None, |ch| {
ch.data_type = DataType::FloatLE;
ch.name = Some(alloc::string::String::from("Timestamp"));
ch.bit_count = 64;
})?;
self.writer.set_time_channel(&time_ch)?;
self.writer.set_channel_unit(&time_ch, "s")?;
let _df_ch = self.writer.add_channel(&cg, Some(&time_ch), |ch| {
ch.data_type = DataType::ByteArray;
ch.name = Some(alloc::string::String::from(frame_type.channel_name()));
ch.bit_count = (dataframe_size * 8) as u32;
})?;
self.channel_groups.insert(frame_type, cg);
}
self.initialized = true;
Ok(())
}
fn write_frames(&mut self, frame_type: FrameType) -> crate::Result<()> {
use crate::DecodedValue;
let cg = match self.channel_groups.get(&frame_type) {
Some(cg) => cg.clone(),
None => return Ok(()),
};
let frames = match self.buffers.get(&frame_type) {
Some(f) if !f.is_empty() => f,
_ => return Ok(()),
};
self.writer.start_data_block_for_cg(&cg, 0)?;
for frame in frames {
let values = [
DecodedValue::Float(frame.timestamp_s),
DecodedValue::ByteArray(frame.to_dataframe_bytes()),
];
self.writer.write_record(&cg, &values)?;
}
self.writer.finish_data_block(&cg)?;
Ok(())
}
fn flush_and_finalize(&mut self) -> crate::Result<()> {
self.flush()?;
self.writer.finalize()
}
pub fn frame_count_for_id(&self, can_id: u32) -> usize {
self.buffers
.values()
.flat_map(|frames| frames.iter())
.filter(|f| f.can_id == can_id)
.count()
}
pub fn total_frame_count(&self) -> usize {
self.buffers.values().map(|b| b.len()).sum()
}
pub fn unique_id_count(&self) -> usize {
let mut ids = alloc::collections::BTreeSet::new();
for frames in self.buffers.values() {
for frame in frames {
ids.insert(frame.can_id);
}
}
ids.len()
}
pub fn has_fd_frames(&self) -> bool {
self.buffers.keys().any(|ft| {
matches!(
ft,
FrameType::FdSmall
| FrameType::FdSmallExtended
| FrameType::FdLarge
| FrameType::FdLargeExtended
)
})
}
pub fn has_extended_frames(&self) -> bool {
self.buffers.keys().any(|ft| {
matches!(
ft,
FrameType::ClassicExtended
| FrameType::FdSmallExtended
| FrameType::FdLargeExtended
)
})
}
pub fn standard_frame_count(&self) -> usize {
self.buffers
.iter()
.filter(|(ft, _)| {
matches!(
ft,
FrameType::Classic | FrameType::FdSmall | FrameType::FdLarge
)
})
.map(|(_, frames)| frames.len())
.sum()
}
pub fn extended_frame_count(&self) -> usize {
self.buffers
.iter()
.filter(|(ft, _)| {
matches!(
ft,
FrameType::ClassicExtended
| FrameType::FdSmallExtended
| FrameType::FdLargeExtended
)
})
.map(|(_, frames)| frames.len())
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_can_logger_basic() {
let mut logger = RawCanLogger::new().unwrap();
assert!(logger.log(0x100, 1000, &[0x01, 0x02, 0x03, 0x04]));
assert!(logger.log(0x100, 2000, &[0x05, 0x06, 0x07, 0x08]));
assert!(logger.log(0x200, 1500, &[0xAA, 0xBB]));
assert_eq!(logger.frame_count_for_id(0x100), 2);
assert_eq!(logger.frame_count_for_id(0x200), 1);
assert_eq!(logger.total_frame_count(), 3);
assert_eq!(logger.unique_id_count(), 2);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
assert_eq!(&mdf_bytes[0..3], b"MDF");
}
#[test]
fn test_raw_can_logger_extended_id() {
let mut logger = RawCanLogger::new().unwrap();
let extended_id = 0x1234_5678;
assert!(logger.log_extended(
extended_id,
1000,
&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
));
assert_eq!(logger.frame_count_for_id(extended_id), 1);
assert!(logger.has_extended_frames());
assert_eq!(logger.extended_frame_count(), 1);
assert_eq!(logger.standard_frame_count(), 0);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_raw_can_logger_mixed_standard_and_extended() {
let mut logger = RawCanLogger::new().unwrap();
assert!(logger.log(0x100, 1000, &[0x01, 0x02, 0x03, 0x04]));
assert!(!logger.has_extended_frames());
assert!(logger.log_extended(0x18FEF100, 2000, &[0xAA, 0xBB, 0xCC, 0xDD]));
assert!(logger.has_extended_frames());
assert_eq!(logger.standard_frame_count(), 1);
assert_eq!(logger.extended_frame_count(), 1);
assert_eq!(logger.unique_id_count(), 2);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_raw_can_logger_extended_fd() {
let mut logger = RawCanLogger::new().unwrap();
let fd_data: [u8; 32] = [0x55; 32];
let flags = FdFlags::new(true, false);
assert!(logger.log_fd_extended(0x18DA00F1, 1000, &fd_data, flags));
assert!(logger.has_extended_frames());
assert!(logger.has_fd_frames());
assert_eq!(logger.extended_frame_count(), 1);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_raw_can_logger_empty() {
let logger = RawCanLogger::new().unwrap();
assert_eq!(logger.total_frame_count(), 0);
assert_eq!(logger.unique_id_count(), 0);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_raw_can_logger_fd_basic() {
let mut logger = RawCanLogger::new().unwrap();
let fd_data: [u8; 32] = [0xAA; 32];
let flags = FdFlags::new(true, false);
assert!(logger.log_fd(0x100, 1000, &fd_data, flags));
assert_eq!(logger.frame_count_for_id(0x100), 1);
assert!(logger.has_fd_frames());
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
assert_eq!(&mdf_bytes[0..3], b"MDF");
}
#[test]
fn test_raw_can_logger_fd_64_bytes() {
let mut logger = RawCanLogger::new().unwrap();
let fd_data: [u8; 64] = core::array::from_fn(|i| i as u8);
let flags = FdFlags::new(true, true); assert!(logger.log_fd(0x200, 2000, &fd_data, flags));
assert_eq!(logger.frame_count_for_id(0x200), 1);
assert!(logger.has_fd_frames());
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_raw_can_logger_mixed_classic_and_fd() {
let mut logger = RawCanLogger::new().unwrap();
assert!(logger.log(0x100, 1000, &[1, 2, 3, 4, 5, 6, 7, 8]));
assert!(!logger.has_fd_frames());
let fd_data: [u8; 24] = [0xBB; 24];
assert!(logger.log_fd(0x200, 2000, &fd_data, FdFlags::default()));
assert!(logger.has_fd_frames());
assert!(logger.log(0x100, 3000, &[9, 10, 11, 12]));
assert_eq!(logger.frame_count_for_id(0x100), 2);
assert_eq!(logger.frame_count_for_id(0x200), 1);
assert_eq!(logger.total_frame_count(), 3);
let mdf_bytes = logger.finalize().unwrap();
assert!(!mdf_bytes.is_empty());
}
#[test]
fn test_dataframe_format() {
let frame = RawFrame::new_classic(1_000_000, 0x123, 4, &[0xAA, 0xBB, 0xCC, 0xDD], false);
let bytes = frame.to_dataframe_bytes();
assert_eq!(bytes[0], 0x23);
assert_eq!(bytes[1], 0x01);
assert_eq!(bytes[2], 0x00);
assert_eq!(bytes[3], 0x00);
assert_eq!(bytes[4], 4);
assert_eq!(bytes[5], 0xAA);
assert_eq!(bytes[6], 0xBB);
assert_eq!(bytes[7], 0xCC);
assert_eq!(bytes[8], 0xDD);
assert_eq!(bytes[9], 0x00); }
#[test]
fn test_dataframe_format_extended() {
let frame =
RawFrame::new_classic(1_000_000, 0x18FEF100, 8, &[1, 2, 3, 4, 5, 6, 7, 8], true);
let bytes = frame.to_dataframe_bytes();
let id = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
assert_eq!(id, 0x98FEF100);
assert!(id & 0x8000_0000 != 0); }
#[test]
fn test_fd_flags() {
let flags = FdFlags::new(true, false);
assert!(flags.brs());
assert!(!flags.esi());
let flags = FdFlags::new(false, true);
assert!(!flags.brs());
assert!(flags.esi());
let flags = FdFlags::from_byte(0x03);
assert!(flags.brs());
assert!(flags.esi());
assert_eq!(flags.to_byte(), 0x03);
}
#[test]
fn test_bus_name() {
let logger = RawCanLogger::with_bus_name("Vehicle_CAN").unwrap();
assert_eq!(logger.bus_name, "Vehicle_CAN");
}
#[test]
fn test_source_name_alias() {
let logger = RawCanLogger::with_source_name("Vehicle_CAN").unwrap();
assert_eq!(logger.bus_name, "Vehicle_CAN");
}
#[cfg(feature = "std")]
#[test]
fn test_from_file_append() {
use std::io::Write;
let mut logger = RawCanLogger::new().unwrap();
logger.log(0x100, 1_000_000, &[0x01, 0x02, 0x03, 0x04]);
logger.log(0x100, 2_000_000, &[0x05, 0x06, 0x07, 0x08]);
logger.log(0x200, 1_500_000, &[0xAA, 0xBB]);
let initial_bytes = logger.finalize().unwrap();
assert!(!initial_bytes.is_empty());
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("test_append.mf4");
let mut file = std::fs::File::create(&temp_path).unwrap();
file.write_all(&initial_bytes).unwrap();
drop(file);
let mut logger = RawCanLogger::from_file(temp_path.to_str().unwrap()).unwrap();
assert_eq!(logger.loaded_frame_count(), 3);
assert_eq!(logger.last_timestamp_us(), 2_000_000);
let next_ts = logger.last_timestamp_us() + 1_000_000;
logger.log(0x100, next_ts, &[0x11, 0x22, 0x33, 0x44]);
logger.log(0x300, next_ts + 500_000, &[0xFF]);
assert_eq!(logger.total_frame_count(), 5);
let appended_bytes = logger.finalize().unwrap();
assert!(appended_bytes.len() > initial_bytes.len());
let _ = std::fs::remove_file(&temp_path);
}
}