use std::fmt;
use smallvec::SmallVec;
use crate::engine::format::common::{iso11783_compose, iso11783_decompose};
use crate::engine::format::plain::ParseError;
use crate::engine::format::timestamp::{days_to_ymd, time_of_day};
use crate::engine::frame::RawFrame;
pub fn write_line<W: fmt::Write>(w: &mut W, frame: &RawFrame) -> fmt::Result {
let canid = iso11783_compose(frame.prio, frame.pgn, frame.src, frame.dst);
write!(
w,
"{} R {canid:08X}",
time_of_day(frame.timestamp.as_deref())
)?;
for b in &frame.data {
write!(w, " {b:02X}")?;
}
Ok(())
}
pub fn parse_line(line: &str) -> Result<RawFrame, ParseError> {
let line = line.trim_end_matches(['\r', '\n']);
if line.is_empty() {
return Err(ParseError::Empty);
}
let mut toks = line.split_whitespace();
let time_tok = toks.next().ok_or(ParseError::MissingTimestamp)?;
let timestamp = synth_iso_timestamp(time_tok);
let _dir = toks.next().ok_or(ParseError::BadHeader {
expected: 3,
found: 1,
})?;
let canid_tok = toks.next().ok_or(ParseError::BadHeader {
expected: 3,
found: 2,
})?;
let canid = u32::from_str_radix(canid_tok, 16).map_err(|_| ParseError::BadInteger {
field: "canid",
value: canid_tok.to_string(),
offset: None,
})?;
let (prio, pgn, src, dst) = iso11783_decompose(canid);
let mut data: SmallVec<[u8; 8]> = SmallVec::new();
for (i, t) in toks.enumerate() {
let b = u8::from_str_radix(t, 16).map_err(|_| ParseError::BadHexByte {
index: i,
value: t.to_string(),
offset: None,
})?;
data.push(b);
}
Ok(RawFrame {
timestamp: Some(timestamp),
prio,
pgn,
src,
dst,
data,
})
}
#[cfg(not(target_arch = "wasm32"))]
fn synth_iso_timestamp(time: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let days = secs.div_euclid(86_400);
let (y, m, d) = days_to_ymd(days);
format!("{y:04}-{m:02}-{d:02}T{time}")
}
#[cfg(target_arch = "wasm32")]
fn synth_iso_timestamp(time: &str) -> String {
format!("1970-01-01T{time}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_ydwg02_pdu2_frame() {
let line = "00:17:55.475 R 0DF50B23 FF FF FF FF FF 00 00 FF";
let f = parse_line(line).unwrap();
assert!(
f.timestamp
.as_deref()
.is_some_and(|t| t.ends_with("T00:17:55.475")),
"timestamp: {:?}",
f.timestamp
);
assert_eq!(f.prio, 3);
assert_eq!(f.pgn, 0x1f50b);
assert_eq!(f.src, 0x23);
assert_eq!(f.dst, 0xff);
assert_eq!(f.data.len(), 8);
}
#[test]
fn pdu1_keeps_destination() {
let line = "00:00:00.000 R 18EEFF05 01 02 03 04 05 06 07 08";
let f = parse_line(line).unwrap();
assert_eq!(f.prio, 6);
assert_eq!(f.pgn, 0xee00);
assert_eq!(f.dst, 0xff);
assert_eq!(f.src, 0x05);
}
#[test]
fn write_line_round_trips_through_parse() {
let frame = RawFrame {
timestamp: Some("2026-01-01T00:17:55.475Z".into()),
prio: 3,
pgn: 0x1f50b,
src: 0x23,
dst: 0xff,
data: [0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff]
.into_iter()
.collect(),
};
let mut line = String::new();
write_line(&mut line, &frame).unwrap();
assert_eq!(line, "00:17:55.475 R 0DF50B23 FF FF FF FF FF 00 00 FF");
let back = parse_line(&line).unwrap();
assert_eq!(
(back.prio, back.pgn, back.src, back.dst),
(3, 0x1f50b, 0x23, 0xff)
);
assert_eq!(back.data.as_slice(), frame.data.as_slice());
assert!(back.timestamp.as_deref().unwrap().contains("00:17:55.475"));
}
#[test]
fn write_line_empty_data() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 0xee00,
src: 0x05,
dst: 0xff,
data: SmallVec::new(),
};
let mut line = String::new();
write_line(&mut line, &frame).unwrap();
assert_eq!(line, "00:00:00.000 R 18EEFF05");
}
}