mod asam_can_group;
mod decoded_frame;
mod overlay_statistics;
mod signal_value;
pub(crate) use asam_can_group::AsamCanGroup;
pub use decoded_frame::DecodedFrame;
pub use overlay_statistics::OverlayStatistics;
pub use signal_value::SignalValue;
use alloc::string::String;
use alloc::vec::Vec;
use crate::index::{ByteRangeReader, IndexedChannelGroup, MdfIndex};
use crate::{DecodedValue, Error, Result};
pub struct DbcOverlayReader<'dbc> {
dbc: &'dbc dbc_rs::Dbc,
index: MdfIndex,
asam_groups: Vec<AsamCanGroup>,
}
impl<'dbc> DbcOverlayReader<'dbc> {
#[cfg(feature = "std")]
pub fn from_file(mdf_path: &str, dbc: &'dbc dbc_rs::Dbc) -> Result<Self> {
let index = MdfIndex::from_file(mdf_path)?;
Self::from_index(index, dbc)
}
pub fn from_index(index: MdfIndex, dbc: &'dbc dbc_rs::Dbc) -> Result<Self> {
let asam_groups = Self::detect_asam_groups(&index)?;
if asam_groups.is_empty() {
return Err(Error::BlockSerializationError(
"No raw CAN channel groups found in MDF file".into(),
));
}
Ok(Self {
dbc,
index,
asam_groups,
})
}
fn detect_asam_groups(index: &MdfIndex) -> Result<Vec<AsamCanGroup>> {
let mut groups = Vec::new();
for (group_idx, group) in index.channel_groups.iter().enumerate() {
if let Some(asam_group) = Self::try_parse_asam_group(group_idx, group) {
groups.push(asam_group);
}
}
Ok(groups)
}
fn try_parse_asam_group(
group_index: usize,
group: &IndexedChannelGroup,
) -> Option<AsamCanGroup> {
let mut timestamp_channel = None;
let mut dataframe_channel = None;
for (ch_idx, channel) in group.channels.iter().enumerate() {
let name = match &channel.name {
Some(n) => n.as_str(),
None => continue,
};
match name {
"Timestamp" => timestamp_channel = Some(ch_idx),
"CAN_DataFrame" => dataframe_channel = Some(ch_idx),
_ => {}
}
}
let timestamp = timestamp_channel?;
let dataframe = dataframe_channel?;
Some(AsamCanGroup {
group_index,
timestamp_channel: timestamp,
dataframe_channel: dataframe,
})
}
pub fn raw_group_count(&self) -> usize {
self.asam_groups.len()
}
pub fn index(&self) -> &MdfIndex {
&self.index
}
pub fn dbc(&self) -> &dbc_rs::Dbc {
self.dbc
}
#[allow(clippy::type_complexity)]
pub fn read_raw_frames<R: ByteRangeReader<Error = Error>>(
&self,
reader: &mut R,
) -> Result<Vec<(u64, u32, bool, Vec<u8>)>> {
let mut frames = Vec::new();
for asam_group in &self.asam_groups {
let timestamps = self.index.read_channel_values(
asam_group.group_index,
asam_group.timestamp_channel,
reader,
)?;
let dataframes = self.index.read_channel_values(
asam_group.group_index,
asam_group.dataframe_channel,
reader,
)?;
for (ts_val, df_val) in timestamps.iter().zip(dataframes.iter()) {
let timestamp_us = match ts_val {
Some(DecodedValue::Float(secs)) => (*secs * 1_000_000.0) as u64,
Some(DecodedValue::UnsignedInteger(us)) => *us,
Some(DecodedValue::SignedInteger(us)) => *us as u64,
_ => 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 data = bytes[5..5 + data_len].to_vec();
frames.push((timestamp_us, can_id, is_extended, data));
}
}
frames.sort_by_key(|(ts, _, _, _)| *ts);
Ok(frames)
}
pub fn frames<R: ByteRangeReader<Error = Error>>(
&self,
message_name: &str,
reader: &mut R,
) -> Result<Vec<DecodedFrame>> {
let message = self
.dbc
.messages()
.iter()
.find(|m| m.name() == message_name)
.ok_or_else(|| {
Error::BlockSerializationError(alloc::format!(
"Message '{}' not found in DBC",
message_name
))
})?;
let msg_id = message.id();
let msg_is_extended = (msg_id & 0x8000_0000) != 0;
let msg_can_id = msg_id & 0x1FFF_FFFF;
let raw_frames = self.read_raw_frames(reader)?;
let mut decoded_frames = Vec::new();
for (timestamp, can_id, is_extended, data) in raw_frames {
if can_id != msg_can_id || is_extended != msg_is_extended {
continue;
}
if let Ok(decoded_signals) = self.dbc.decode(can_id, &data, is_extended) {
let signals: Vec<(String, f64)> = decoded_signals
.iter()
.map(|s| (String::from(s.name), s.value))
.collect();
decoded_frames.push(DecodedFrame {
timestamp_us: timestamp,
can_id,
is_extended,
signals,
});
}
}
Ok(decoded_frames)
}
pub fn signal_values<R: ByteRangeReader<Error = Error>>(
&self,
signal_name: &str,
reader: &mut R,
) -> Result<Vec<SignalValue>> {
let (message, _signal) = self
.dbc
.messages()
.iter()
.find_map(|msg| {
msg.signals()
.iter()
.find(|s| s.name() == signal_name)
.map(|sig| (msg, sig))
})
.ok_or_else(|| {
Error::BlockSerializationError(alloc::format!(
"Signal '{}' not found in DBC",
signal_name
))
})?;
let msg_id = message.id();
let msg_is_extended = (msg_id & 0x8000_0000) != 0;
let msg_can_id = msg_id & 0x1FFF_FFFF;
let raw_frames = self.read_raw_frames(reader)?;
let mut values = Vec::new();
for (timestamp, can_id, is_extended, data) in raw_frames {
if can_id != msg_can_id || is_extended != msg_is_extended {
continue;
}
if let Ok(decoded_signals) = self.dbc.decode(can_id, &data, is_extended) {
if let Some(sig) = decoded_signals.iter().find(|s| s.name == signal_name) {
values.push(SignalValue {
timestamp_us: timestamp,
value: sig.value,
raw_value: sig.raw_value,
});
}
}
}
Ok(values)
}
pub fn can_ids<R: ByteRangeReader<Error = Error>>(&self, reader: &mut R) -> Result<Vec<u32>> {
use alloc::collections::BTreeSet;
let frames = self.read_raw_frames(reader)?;
let ids: BTreeSet<u32> = frames.iter().map(|(_, id, _, _)| *id).collect();
Ok(ids.into_iter().collect())
}
pub fn statistics<R: ByteRangeReader<Error = Error>>(
&self,
reader: &mut R,
) -> Result<OverlayStatistics> {
let frames = self.read_raw_frames(reader)?;
let total_frames = frames.len();
let unique_ids: alloc::collections::BTreeSet<u32> =
frames.iter().map(|(_, id, _, _)| *id).collect();
let (min_timestamp, max_timestamp) = if frames.is_empty() {
(0, 0)
} else {
let min = frames.iter().map(|(ts, _, _, _)| *ts).min().unwrap_or(0);
let max = frames.iter().map(|(ts, _, _, _)| *ts).max().unwrap_or(0);
(min, max)
};
let mut dbc_messages_found = 0;
for msg in self.dbc.messages().iter() {
let msg_id = msg.id() & 0x1FFF_FFFF;
if unique_ids.contains(&msg_id) {
dbc_messages_found += 1;
}
}
Ok(OverlayStatistics {
total_frames,
unique_can_ids: unique_ids.len(),
dbc_messages_found,
dbc_messages_total: self.dbc.messages().len(),
min_timestamp_us: min_timestamp,
max_timestamp_us: max_timestamp,
duration_us: max_timestamp.saturating_sub(min_timestamp),
})
}
pub fn available_messages<R: ByteRangeReader<Error = Error>>(
&self,
reader: &mut R,
) -> Result<Vec<String>> {
let can_ids = self.can_ids(reader)?;
let can_id_set: alloc::collections::BTreeSet<u32> = can_ids.into_iter().collect();
let mut messages = Vec::new();
for msg in self.dbc.messages().iter() {
let msg_id = msg.id() & 0x1FFF_FFFF;
if can_id_set.contains(&msg_id) {
messages.push(String::from(msg.name()));
}
}
Ok(messages)
}
pub fn available_signals<R: ByteRangeReader<Error = Error>>(
&self,
reader: &mut R,
) -> Result<Vec<String>> {
let can_ids = self.can_ids(reader)?;
let can_id_set: alloc::collections::BTreeSet<u32> = can_ids.into_iter().collect();
let mut signals = Vec::new();
for msg in self.dbc.messages().iter() {
let msg_id = msg.id() & 0x1FFF_FFFF;
if can_id_set.contains(&msg_id) {
for sig in msg.signals().iter() {
signals.push(String::from(sig.name()));
}
}
}
Ok(signals)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_dbc() -> dbc_rs::Dbc {
dbc_rs::Dbc::parse(
r#"VERSION "1.0"
BU_: ECM
BO_ 256 Engine : 8 ECM
SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" Vector__XXX
SG_ Temp : 16|8@1+ (1,-40) [-40|215] "C" Vector__XXX
BO_ 512 Transmission : 8 ECM
SG_ Gear : 0|4@1+ (1,0) [0|6] "" Vector__XXX
SG_ Speed : 8|16@1+ (0.01,0) [0|300] "km/h" Vector__XXX
"#,
)
.unwrap()
}
#[test]
fn test_overlay_with_raw_capture() {
use crate::can::RawCanLogger;
let mut logger = RawCanLogger::new().unwrap();
logger.log(256, 1000, &[0x40, 0x1F, 0x5A, 0, 0, 0, 0, 0]);
logger.log(256, 2000, &[0x80, 0x3E, 0x64, 0, 0, 0, 0, 0]);
logger.log(512, 1500, &[0x03, 0x88, 0x13, 0, 0, 0, 0, 0]);
let mdf_bytes = logger.finalize().unwrap();
let temp_path = std::env::temp_dir().join("overlay_test.mf4");
std::fs::write(&temp_path, &mdf_bytes).unwrap();
let dbc = create_test_dbc();
let overlay = DbcOverlayReader::from_file(temp_path.to_str().unwrap(), &dbc).unwrap();
assert!(overlay.raw_group_count() >= 1);
let mut reader = crate::FileRangeReader::new(temp_path.to_str().unwrap()).unwrap();
let stats = overlay.statistics(&mut reader).unwrap();
assert_eq!(stats.total_frames, 3);
assert_eq!(stats.unique_can_ids, 2);
assert_eq!(stats.dbc_messages_found, 2);
let messages = overlay.available_messages(&mut reader).unwrap();
assert!(messages.contains(&String::from("Engine")));
assert!(messages.contains(&String::from("Transmission")));
let engine_frames = overlay.frames("Engine", &mut reader).unwrap();
assert_eq!(engine_frames.len(), 2);
let frame = &engine_frames[0];
assert_eq!(frame.can_id, 256);
let rpm = frame
.signals
.iter()
.find(|(name, _)| name == "RPM")
.unwrap();
assert!((rpm.1 - 2000.0).abs() < 0.1);
let rpm_values = overlay.signal_values("RPM", &mut reader).unwrap();
assert_eq!(rpm_values.len(), 2);
assert!((rpm_values[0].value - 2000.0).abs() < 0.1);
assert!((rpm_values[1].value - 4000.0).abs() < 0.1);
std::fs::remove_file(&temp_path).ok();
}
#[test]
fn test_overlay_signal_not_found() {
use crate::can::RawCanLogger;
let mut logger = RawCanLogger::new().unwrap();
logger.log(256, 1000, &[0x40, 0x1F, 0x5A, 0, 0, 0, 0, 0]);
let mdf_bytes = logger.finalize().unwrap();
let temp_path = std::env::temp_dir().join("overlay_notfound.mf4");
std::fs::write(&temp_path, &mdf_bytes).unwrap();
let dbc = create_test_dbc();
let overlay = DbcOverlayReader::from_file(temp_path.to_str().unwrap(), &dbc).unwrap();
let mut reader = crate::FileRangeReader::new(temp_path.to_str().unwrap()).unwrap();
let result = overlay.signal_values("NonExistent", &mut reader);
assert!(result.is_err());
let result = overlay.frames("NonExistent", &mut reader);
assert!(result.is_err());
std::fs::remove_file(&temp_path).ok();
}
#[test]
fn test_overlay_extended_ids() {
use crate::can::RawCanLogger;
let dbc = dbc_rs::Dbc::parse(
r#"VERSION "1.0"
BU_: ECM
BO_ 2365587201 J1939_EEC1 : 8 ECM
SG_ EngineSpeed : 24|16@1+ (0.125,0) [0|8031.875] "rpm" Vector__XXX
"#,
)
.unwrap();
let mut logger = RawCanLogger::new().unwrap();
let pgn_id = 0x0CF00401; logger.log_extended(pgn_id, 1000, &[0, 0, 0, 0x00, 0x20, 0, 0, 0]);
let mdf_bytes = logger.finalize().unwrap();
let temp_path = std::env::temp_dir().join("overlay_extended.mf4");
std::fs::write(&temp_path, &mdf_bytes).unwrap();
let overlay = DbcOverlayReader::from_file(temp_path.to_str().unwrap(), &dbc).unwrap();
let mut reader = crate::FileRangeReader::new(temp_path.to_str().unwrap()).unwrap();
let raw_frames = overlay.read_raw_frames(&mut reader).unwrap();
assert_eq!(raw_frames.len(), 1);
assert!(raw_frames[0].2);
std::fs::remove_file(&temp_path).ok();
}
}