fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let m = month as i64;
let d = day as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CanopenDate {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub ms: u16,
pub day_of_week: u8,
pub summer_time: bool,
}
impl CanopenDate {
pub fn to_unix_nanos(&self) -> i64 {
let days = days_from_civil(self.year as i64, self.month as u32, self.day as u32);
let secs = days * 86_400 + self.hour as i64 * 3_600 + self.minute as i64 * 60;
secs * 1_000_000_000 + self.ms as i64 * 1_000_000
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CanopenTime {
pub ms_since_midnight: u32,
pub days_since_1984: u16,
}
impl CanopenTime {
const EPOCH_DAYS: i64 = 5_113;
pub fn to_unix_nanos(&self) -> i64 {
let days = Self::EPOCH_DAYS + self.days_since_1984 as i64;
days * 86_400 * 1_000_000_000 + self.ms_since_midnight as i64 * 1_000_000
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValueKind {
U8,
U16,
U32,
U64,
I8,
I16,
I32,
I64,
F32,
F64,
Bytes,
Str,
Complex,
CanopenDate,
CanopenTime,
}
impl ValueKind {
pub fn is_numeric(&self) -> bool {
!matches!(
self,
ValueKind::Bytes
| ValueKind::Str
| ValueKind::Complex
| ValueKind::CanopenDate
| ValueKind::CanopenTime
)
}
pub fn name(&self) -> &'static str {
match self {
ValueKind::U8 => "u8",
ValueKind::U16 => "u16",
ValueKind::U32 => "u32",
ValueKind::U64 => "u64",
ValueKind::I8 => "i8",
ValueKind::I16 => "i16",
ValueKind::I32 => "i32",
ValueKind::I64 => "i64",
ValueKind::F32 => "f32",
ValueKind::F64 => "f64",
ValueKind::Bytes => "bytes",
ValueKind::Str => "str",
ValueKind::Complex => "complex",
ValueKind::CanopenDate => "canopen_date",
ValueKind::CanopenTime => "canopen_time",
}
}
}
impl std::fmt::Display for ValueKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum SignalValues {
U8(Vec<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
U64(Vec<u64>),
I8(Vec<i8>),
I16(Vec<i16>),
I32(Vec<i32>),
I64(Vec<i64>),
F32(Vec<f32>),
F64(Vec<f64>),
Bytes {
data: Vec<u8>,
width: usize,
},
VarBytes {
data: Vec<u8>,
starts: Vec<usize>,
},
Str(Vec<String>),
Complex {
re: Vec<f64>,
im: Vec<f64>,
},
CanopenDate(Vec<CanopenDate>),
CanopenTime(Vec<CanopenTime>),
Array {
values: Vec<f64>,
elements_per_sample: usize,
},
ArrayVarLen {
values: Vec<f64>,
starts: Vec<usize>,
},
}
impl SignalValues {
pub fn len(&self) -> usize {
match self {
SignalValues::U8(v) => v.len(),
SignalValues::U16(v) => v.len(),
SignalValues::U32(v) => v.len(),
SignalValues::U64(v) => v.len(),
SignalValues::I8(v) => v.len(),
SignalValues::I16(v) => v.len(),
SignalValues::I32(v) => v.len(),
SignalValues::I64(v) => v.len(),
SignalValues::F32(v) => v.len(),
SignalValues::F64(v) => v.len(),
SignalValues::Bytes { data, width } => {
if *width == 0 {
0
} else {
data.len() / width
}
}
SignalValues::VarBytes { starts, .. } => starts.len().saturating_sub(1),
SignalValues::Str(v) => v.len(),
SignalValues::Complex { re, .. } => re.len(),
SignalValues::CanopenDate(v) => v.len(),
SignalValues::CanopenTime(v) => v.len(),
SignalValues::Array {
values,
elements_per_sample,
} => {
if *elements_per_sample == 0 {
0
} else {
values.len() / elements_per_sample
}
}
SignalValues::ArrayVarLen { starts, .. } => starts.len().saturating_sub(1),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn kind(&self) -> ValueKind {
match self {
SignalValues::U8(_) => ValueKind::U8,
SignalValues::U16(_) => ValueKind::U16,
SignalValues::U32(_) => ValueKind::U32,
SignalValues::U64(_) => ValueKind::U64,
SignalValues::I8(_) => ValueKind::I8,
SignalValues::I16(_) => ValueKind::I16,
SignalValues::I32(_) => ValueKind::I32,
SignalValues::I64(_) => ValueKind::I64,
SignalValues::F32(_) => ValueKind::F32,
SignalValues::F64(_) => ValueKind::F64,
SignalValues::Bytes { .. } | SignalValues::VarBytes { .. } => ValueKind::Bytes,
SignalValues::Str(_) => ValueKind::Str,
SignalValues::Complex { .. } => ValueKind::Complex,
SignalValues::CanopenDate(_) => ValueKind::CanopenDate,
SignalValues::CanopenTime(_) => ValueKind::CanopenTime,
SignalValues::Array { .. } | SignalValues::ArrayVarLen { .. } => ValueKind::F64,
}
}
pub fn bytes_at(&self, index: usize) -> Option<&[u8]> {
match self {
SignalValues::Bytes { data, width } => {
if *width == 0 {
return None;
}
data.get(index * width..(index + 1) * width)
}
SignalValues::VarBytes { data, starts } => {
let from = *starts.get(index)?;
let to = *starts.get(index + 1)?;
data.get(from..to)
}
_ => None,
}
}
pub fn to_f64(&self) -> Vec<f64> {
fn cast<T: Copy + Into<f64>>(v: &[T]) -> Vec<f64> {
v.iter().map(|&x| x.into()).collect()
}
match self {
SignalValues::U8(v) => cast(v),
SignalValues::U16(v) => cast(v),
SignalValues::U32(v) => cast(v),
SignalValues::U64(v) => v.iter().map(|&x| x as f64).collect(),
SignalValues::I8(v) => cast(v),
SignalValues::I16(v) => cast(v),
SignalValues::I32(v) => cast(v),
SignalValues::I64(v) => v.iter().map(|&x| x as f64).collect(),
SignalValues::F32(v) => cast(v),
SignalValues::F64(v) => v.clone(),
SignalValues::Bytes { .. } | SignalValues::VarBytes { .. } | SignalValues::Str(_) => {
vec![f64::NAN; self.len()]
}
SignalValues::Complex { .. }
| SignalValues::CanopenDate(_)
| SignalValues::CanopenTime(_) => vec![f64::NAN; self.len()],
SignalValues::Array { values, .. } => values.clone(),
SignalValues::ArrayVarLen { values, .. } => values.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reports_length_per_variant() {
assert_eq!(SignalValues::U8(vec![1, 2, 3]).len(), 3);
assert_eq!(SignalValues::F64(vec![]).len(), 0);
assert!(SignalValues::F64(vec![]).is_empty());
}
#[test]
fn variable_width_sample_starts_are_wide_enough_for_large_files() {
fn starts_are_usize(_: &[usize]) {}
let v = SignalValues::VarBytes {
data: vec![1, 2, 3],
starts: vec![0, 2, 3],
};
if let SignalValues::VarBytes { starts, .. } = &v {
starts_are_usize(starts);
}
assert_eq!(v.len(), 2);
assert_eq!(v.bytes_at(0), Some(&[1, 2][..]));
assert_eq!(v.bytes_at(1), Some(&[3][..]));
}
#[test]
fn byte_samples_are_addressed_by_width() {
let v = SignalValues::Bytes {
data: vec![1, 2, 3, 4, 5, 6],
width: 3,
};
assert_eq!(v.len(), 2);
assert_eq!(v.bytes_at(0), Some(&[1, 2, 3][..]));
assert_eq!(v.bytes_at(1), Some(&[4, 5, 6][..]));
assert_eq!(v.bytes_at(2), None);
}
#[test]
fn zero_width_bytes_do_not_divide_by_zero() {
let v = SignalValues::Bytes {
data: vec![1, 2, 3],
width: 0,
};
assert_eq!(v.len(), 0);
assert_eq!(v.bytes_at(0), None);
}
#[test]
fn bytes_at_returns_none_for_numeric_variants() {
assert_eq!(SignalValues::U8(vec![1, 2]).bytes_at(0), None);
}
#[test]
fn converts_numeric_variants_to_f64() {
assert_eq!(SignalValues::U16(vec![7, 9]).to_f64(), vec![7.0, 9.0]);
assert_eq!(SignalValues::I8(vec![-3]).to_f64(), vec![-3.0]);
assert_eq!(SignalValues::F32(vec![0.5]).to_f64(), vec![0.5]);
}
#[test]
fn non_numeric_variants_convert_to_nan_not_a_wrong_number() {
let v = SignalValues::Bytes {
data: vec![0xFF; 16],
width: 8,
};
let f = v.to_f64();
assert_eq!(f.len(), 2);
assert!(f.iter().all(|x| x.is_nan()));
}
#[test]
fn large_u64_values_survive_as_integers() {
let big = (1u64 << 63) + 1;
let v = SignalValues::U64(vec![big]);
assert_eq!(v, SignalValues::U64(vec![big]));
assert_ne!(
v.to_f64()[0] as u64,
big,
"f64 round-trip is lossy, as documented"
);
}
#[test]
fn kind_round_trips() {
assert_eq!(SignalValues::U32(vec![]).kind(), ValueKind::U32);
assert_eq!(SignalValues::U32(vec![]).kind().name(), "u32");
assert!(ValueKind::I16.is_numeric());
assert!(!ValueKind::Bytes.is_numeric());
assert!(!ValueKind::Str.is_numeric());
}
}
#[cfg(test)]
mod canopen_tests {
use super::*;
#[test]
fn days_from_civil_matches_known_dates() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(days_from_civil(1969, 12, 31), -1);
assert_eq!(days_from_civil(1984, 1, 1), 5_113);
assert_eq!(days_from_civil(2000, 3, 1), 11_017);
assert_eq!(days_from_civil(2000, 2, 29), 11_016);
assert_eq!(days_from_civil(2026, 8, 3), 20_668);
}
#[test]
fn the_canopen_epoch_is_where_the_time_type_counts_from() {
assert_eq!(CanopenTime::EPOCH_DAYS, days_from_civil(1984, 1, 1));
}
#[test]
fn a_canopen_date_places_itself_on_the_unix_epoch() {
let d = CanopenDate {
year: 1984,
month: 1,
day: 1,
hour: 0,
minute: 0,
ms: 0,
day_of_week: 7,
summer_time: false,
};
assert_eq!(d.to_unix_nanos(), 5_113 * 86_400 * 1_000_000_000);
let d = CanopenDate {
year: 2026,
month: 8,
day: 3,
hour: 12,
minute: 34,
ms: 56_789,
day_of_week: 1,
summer_time: true,
};
let expected =
(20_668i64 * 86_400 + 12 * 3_600 + 34 * 60) * 1_000_000_000 + 56_789 * 1_000_000;
assert_eq!(d.to_unix_nanos(), expected);
}
#[test]
fn a_canopen_time_places_itself_on_the_unix_epoch() {
let t = CanopenTime {
ms_since_midnight: 0,
days_since_1984: 0,
};
assert_eq!(t.to_unix_nanos(), 5_113 * 86_400 * 1_000_000_000);
let days = days_from_civil(2026, 8, 3) - days_from_civil(1984, 1, 1);
let t = CanopenTime {
ms_since_midnight: (12 * 3_600 + 34 * 60) * 1_000 + 56_789,
days_since_1984: days as u16,
};
let d = CanopenDate {
year: 2026,
month: 8,
day: 3,
hour: 12,
minute: 34,
ms: 56_789,
day_of_week: 1,
summer_time: false,
};
assert_eq!(t.to_unix_nanos(), d.to_unix_nanos());
}
#[test]
fn the_new_kinds_are_not_numeric() {
for k in [
ValueKind::Complex,
ValueKind::CanopenDate,
ValueKind::CanopenTime,
] {
assert!(!k.is_numeric(), "{k} must not claim to be numeric");
}
}
}