use std::collections::{HashMap, HashSet};
const MAX_MUX_DEPTH: usize = 64;
pub(crate) const ID_MASK: u32 = 0x1FFF_FFFF;
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedSignal<'a> {
pub name: &'a str,
pub value: f64,
pub unit: &'a str,
pub text: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Multiplexing {
None,
Switch,
Selected(u64),
RangeSelected {
multiplexor: String,
ranges: Vec<(u64, u64)>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalDef {
pub name: String,
pub start_bit: u64,
pub size: u64,
pub big_endian: bool,
pub signed: bool,
pub factor: f64,
pub offset: f64,
pub unit: String,
pub multiplexing: Multiplexing,
pub value_table: Vec<(i64, String)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MessageDef {
pub name: String,
pub id: u32,
pub extended: bool,
pub length: u64,
pub signals: Vec<SignalDef>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IdMatching {
#[default]
Exact,
J1939Pgn,
J1939PgnAndSource,
}
fn j1939_pgn(id: u32) -> u32 {
let pages = (id >> 24) & 0b11;
let pdu_format = (id >> 16) & 0xFF;
let pdu_specific = (id >> 8) & 0xFF;
if pdu_format < 240 {
(pages << 16) | (pdu_format << 8)
} else {
(pages << 16) | (pdu_format << 8) | pdu_specific
}
}
fn j1939_source(id: u32) -> u8 {
(id & 0xFF) as u8
}
#[derive(Debug, Clone, Default)]
pub struct CanDatabase {
messages: Vec<MessageDef>,
by_id: HashMap<u32, usize>,
matching: IdMatching,
by_pgn: HashMap<u32, usize>,
by_pgn_and_source: HashMap<(u32, u8), usize>,
}
impl CanDatabase {
pub fn new(messages: Vec<MessageDef>) -> Self {
let by_id = messages
.iter()
.enumerate()
.map(|(index, message)| (message.id & ID_MASK, index))
.collect();
CanDatabase {
messages,
by_id,
matching: IdMatching::Exact,
by_pgn: HashMap::new(),
by_pgn_and_source: HashMap::new(),
}
}
pub fn with_matching(mut self, matching: IdMatching) -> Self {
let (by_pgn, by_pgn_and_source) = match matching {
IdMatching::Exact => (HashMap::new(), HashMap::new()),
IdMatching::J1939Pgn | IdMatching::J1939PgnAndSource => {
let by_pgn = self
.messages
.iter()
.enumerate()
.rev()
.map(|(index, message)| (j1939_pgn(message.id & ID_MASK), index))
.collect();
let by_pgn_and_source = if matches!(matching, IdMatching::J1939PgnAndSource) {
self.messages
.iter()
.enumerate()
.rev()
.map(|(index, message)| {
(
(
j1939_pgn(message.id & ID_MASK),
j1939_source(message.id & ID_MASK),
),
index,
)
})
.collect()
} else {
HashMap::new()
};
(by_pgn, by_pgn_and_source)
}
};
self.by_pgn = by_pgn;
self.by_pgn_and_source = by_pgn_and_source;
self.matching = matching;
self
}
pub fn matching(&self) -> IdMatching {
self.matching
}
pub fn messages(&self) -> &[MessageDef] {
&self.messages
}
pub fn message(&self, id: u32) -> Option<&MessageDef> {
Some(&self.messages[self.message_index(id)?])
}
pub fn message_name(&self, id: u32) -> Option<&str> {
Some(self.message(id)?.name.as_str())
}
pub fn decode(&self, id: u32, payload: &[u8]) -> Vec<DecodedSignal<'_>> {
let mut out = match self.message(id) {
Some(message) => Vec::with_capacity(message.signals.len()),
None => return Vec::new(),
};
self.decode_each(id, payload, &mut |_, _, signal| out.push(signal));
out
}
pub(crate) fn decode_each<'a>(
&'a self,
id: u32,
payload: &[u8],
sink: &mut dyn FnMut(usize, usize, DecodedSignal<'a>),
) {
let Some(message_index) = self.message_index(id) else {
return;
};
let message = &self.messages[message_index];
let mut raw_values: HashMap<&str, u64> = HashMap::new();
let mut visiting: HashSet<&str> = HashSet::new();
for (signal_index, signal) in message.signals.iter().enumerate() {
if !is_selected(message, signal, payload, &mut raw_values, &mut visiting, 0) {
continue;
}
let Some(raw) = raw_value_cached(signal, payload, &mut raw_values) else {
continue;
};
sink(
message_index,
signal_index,
DecodedSignal {
name: &signal.name,
value: scale(signal, raw),
unit: &signal.unit,
text: label(signal, raw),
},
);
}
}
fn message_index(&self, id: u32) -> Option<usize> {
let id = id & ID_MASK;
if let Some(&index) = self.by_id.get(&id) {
return Some(index);
}
match self.matching {
IdMatching::Exact => None,
IdMatching::J1939Pgn => self.by_pgn.get(&j1939_pgn(id)).copied(),
IdMatching::J1939PgnAndSource => self
.by_pgn_and_source
.get(&(j1939_pgn(id), j1939_source(id)))
.copied()
.or_else(|| self.by_pgn.get(&j1939_pgn(id)).copied()),
}
}
}
fn is_selected<'a>(
message: &'a MessageDef,
signal: &'a SignalDef,
payload: &[u8],
raw_values: &mut HashMap<&'a str, u64>,
visiting: &mut HashSet<&'a str>,
depth: usize,
) -> bool {
if depth > MAX_MUX_DEPTH {
return false;
}
if !visiting.insert(signal.name.as_str()) {
return false;
}
let result = match &signal.multiplexing {
Multiplexing::None | Multiplexing::Switch => true,
Multiplexing::Selected(want) => match message
.signals
.iter()
.find(|s| s.multiplexing == Multiplexing::Switch)
{
Some(switch) => {
is_selected(message, switch, payload, raw_values, visiting, depth + 1)
&& raw_value_cached(switch, payload, raw_values).is_some_and(|raw| raw == *want)
}
None => false,
},
Multiplexing::RangeSelected {
multiplexor,
ranges,
} => match message.signals.iter().find(|s| s.name == *multiplexor) {
Some(mux) => {
is_selected(message, mux, payload, raw_values, visiting, depth + 1)
&& raw_value_cached(mux, payload, raw_values).is_some_and(|raw| {
ranges.iter().any(|(min, max)| raw >= *min && raw <= *max)
})
}
None => false,
},
};
visiting.remove(signal.name.as_str());
result
}
fn raw_value_cached<'a>(
signal: &'a SignalDef,
payload: &[u8],
raw_values: &mut HashMap<&'a str, u64>,
) -> Option<u64> {
if let Some(&raw) = raw_values.get(signal.name.as_str()) {
return Some(raw);
}
let raw = raw_value(signal, payload)?;
raw_values.insert(signal.name.as_str(), raw);
Some(raw)
}
fn label(signal: &SignalDef, raw: u64) -> Option<&str> {
if signal.value_table.is_empty() {
return None;
}
let key = if signal.signed {
sign_extend(raw, signal.size)
} else {
raw as i64
};
signal
.value_table
.iter()
.find(|(value, _)| *value == key)
.map(|(_, text)| text.as_str())
}
fn scale(signal: &SignalDef, raw: u64) -> f64 {
let value = if signal.signed {
sign_extend(raw, signal.size) as f64
} else {
raw as f64
};
value * signal.factor + signal.offset
}
fn sign_extend(raw: u64, bits: u64) -> i64 {
if bits == 0 || bits >= 64 {
return raw as i64;
}
let sign_bit = 1u64 << (bits - 1);
if raw & sign_bit == 0 {
raw as i64
} else {
(raw | !((1u64 << bits) - 1)) as i64
}
}
fn raw_value(signal: &SignalDef, payload: &[u8]) -> Option<u64> {
let size = signal.size;
if size == 0 || size > 64 {
return None;
}
let available = (payload.len() as u64).checked_mul(8)?;
if signal.big_endian {
let msb = (signal.start_bit / 8) * 8 + (7 - signal.start_bit % 8);
if msb.checked_add(size)? > available {
return None;
}
let mut value = 0u64;
for i in 0..size {
let bit = msb + i;
let byte = payload[(bit / 8) as usize];
let set = (byte >> (7 - bit % 8)) & 1;
value = (value << 1) | u64::from(set);
}
Some(value)
} else {
if signal.start_bit.checked_add(size)? > available {
return None;
}
let mut value = 0u64;
for i in 0..size {
let bit = signal.start_bit + i;
let byte = payload[(bit / 8) as usize];
let set = (byte >> (bit % 8)) & 1;
value |= u64::from(set) << i;
}
Some(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn probe(start_bit: u64, size: u64, big_endian: bool) -> SignalDef {
SignalDef {
name: "Probe".into(),
start_bit,
size,
big_endian,
signed: false,
factor: 1.0,
offset: 0.0,
unit: String::new(),
multiplexing: Multiplexing::None,
value_table: Vec::new(),
}
}
#[test]
fn the_byte_orders_agree_on_a_byte_aligned_signal() {
let payload = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
for byte in 0..8u64 {
let little = raw_value(&probe(byte * 8, 8, false), &payload).unwrap();
let big = raw_value(&probe(byte * 8 + 7, 8, true), &payload).unwrap();
assert_eq!(little, big, "byte {byte}: the orders disagree");
assert_eq!(
little,
u64::from(payload[byte as usize]),
"byte {byte}: neither order read the byte itself"
);
}
}
#[test]
fn the_byte_orders_agree_on_a_single_bit() {
let payload = [0b1010_0101, 0x00, 0xFF, 0x0F];
for bit in 0..payload.len() as u64 * 8 {
let expected = u64::from(payload[(bit / 8) as usize] >> (bit % 8) & 1);
assert_eq!(raw_value(&probe(bit, 1, false), &payload), Some(expected));
assert_eq!(raw_value(&probe(bit, 1, true), &payload), Some(expected));
}
}
#[test]
fn a_multi_byte_signal_distinguishes_the_orders() {
let payload = [0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert_eq!(raw_value(&probe(7, 16, true), &payload), Some(0x0102));
assert_eq!(raw_value(&probe(0, 16, false), &payload), Some(0x0201));
}
#[test]
fn a_signal_beyond_the_payload_is_not_invented() {
let payload = [0xFF, 0xFF, 0xFF];
assert_eq!(raw_value(&probe(16, 16, false), &payload), None);
assert_eq!(raw_value(&probe(8, 16, false), &payload), Some(0xFFFF));
}
#[test]
fn signed_signals_are_sign_extended_at_their_own_width() {
assert_eq!(sign_extend(0b0111, 4), 7);
assert_eq!(sign_extend(0b1111, 4), -1);
assert_eq!(sign_extend(0b1000, 4), -8);
assert_eq!(sign_extend(0x7F, 8), 127);
assert_eq!(sign_extend(0x80, 8), -128);
assert_eq!(sign_extend(u64::MAX, 64), -1);
}
#[test]
fn multiplexing_selects_by_the_switch_value() {
let mut switch = probe(0, 8, false);
switch.name = "Switch".into();
switch.multiplexing = Multiplexing::Switch;
let mut first = probe(8, 8, false);
first.name = "WhenOne".into();
first.multiplexing = Multiplexing::Selected(1);
let mut second = probe(8, 8, false);
second.name = "WhenTwo".into();
second.multiplexing = Multiplexing::Selected(2);
let db = CanDatabase::new(vec![MessageDef {
name: "Muxed".into(),
id: 0x100,
extended: false,
length: 2,
signals: vec![switch, first, second],
}]);
let names = |payload: &[u8]| -> Vec<String> {
db.decode(0x100, payload)
.iter()
.map(|s| s.name.to_string())
.collect()
};
assert_eq!(names(&[1, 42]), ["Switch", "WhenOne"]);
assert_eq!(names(&[2, 42]), ["Switch", "WhenTwo"]);
assert_eq!(names(&[3, 42]), ["Switch"], "no branch matches switch 3");
}
#[test]
fn parameter_groups_match_the_published_numbers() {
let cases = [
(0x0CF0_0400u32, 0xF004u32, "EEC1, PDU2"),
(0x18FE_E500, 0xFEE5, "engine hours, PDU2"),
(0x18FE_CA00, 0xFECA, "DM1, PDU2"),
(
0x18EA_004A,
0xEA00,
"request, PDU1 — destination 0x00 dropped",
),
(0x18EE_FF00, 0xEE00, "address claimed, PDU1"),
(0x0CEF_0B0F, 0xEF00, "proprietary A, PDU1"),
(0x0C00_0304, 0x0000, "TSC1, PDU1 — the group really is zero"),
];
for (id, pgn, what) in cases {
assert_eq!(j1939_pgn(id), pgn, "{what}: {id:#X}");
}
}
#[test]
fn only_the_group_defining_bits_reach_the_parameter_group() {
for priority in 0..8u32 {
for source in [0x00u32, 0x21, 0xFE] {
let id = (priority << 26) | 0x00F0_0400 | source;
assert_eq!(j1939_pgn(id), 0xF004, "{id:#X}");
}
}
assert_eq!(j1939_pgn(0x18EA_0000), j1939_pgn(0x18EA_FF00));
assert_ne!(j1939_pgn(0x18FE_E500), j1939_pgn(0x18FE_E600));
assert_eq!(j1939_pgn(0x0DF0_0400), 0x1F004);
}
#[test]
fn j1939_matching_ignores_the_source_address() {
let messages = vec![MessageDef {
name: "EEC1".into(),
id: 0x0CF0_04FE,
extended: true,
length: 8,
signals: vec![probe(24, 16, false)],
}];
let exact = CanDatabase::new(messages.clone());
assert_eq!(exact.matching(), IdMatching::Exact);
assert_eq!(exact.message_name(0x0CF0_0400), None, "a different ECU");
assert!(exact.decode(0x0CF0_0400, &[0xFF; 8]).is_empty());
let j1939 = CanDatabase::new(messages).with_matching(IdMatching::J1939Pgn);
assert_eq!(j1939.message_name(0x0CF0_0400), Some("EEC1"));
assert_eq!(j1939.message_name(0x0CF0_0421), Some("EEC1"), "and another");
assert_eq!(j1939.decode(0x0CF0_0400, &[0xFF; 8]).len(), 1);
assert_eq!(j1939.message_name(0x18FE_E500), None);
}
#[test]
fn an_exact_identifier_wins_over_its_parameter_group() {
let db = CanDatabase::new(vec![
MessageDef {
name: "Generic".into(),
id: 0x0CF0_04FE,
extended: true,
length: 8,
signals: Vec::new(),
},
MessageDef {
name: "ThisEcuExactly".into(),
id: 0x0CF0_0421,
extended: true,
length: 8,
signals: Vec::new(),
},
])
.with_matching(IdMatching::J1939Pgn);
assert_eq!(db.message_name(0x0CF0_0421), Some("ThisEcuExactly"));
assert_eq!(db.message_name(0x0CF0_0400), Some("Generic"));
}
#[test]
fn a_value_table_labels_the_raw_reading() {
let mut gear = probe(0, 8, false);
gear.name = "Gear".into();
gear.signed = true;
gear.factor = 10.0;
gear.value_table = vec![
(0, "Neutral".into()),
(-1, "Reverse".into()),
(1, "First".into()),
];
let db = CanDatabase::new(vec![MessageDef {
name: "Transmission".into(),
id: 0x200,
extended: false,
length: 1,
signals: vec![gear],
}]);
let decoded = |byte: u8| -> (f64, Option<String>) {
let signal = db.decode(0x200, &[byte]).remove(0);
(signal.value, signal.text.map(str::to_string))
};
assert_eq!(decoded(0), (0.0, Some("Neutral".into())));
assert_eq!(decoded(1), (10.0, Some("First".into())));
assert_eq!(decoded(0xFF), (-10.0, Some("Reverse".into())));
assert_eq!(decoded(2), (20.0, None));
}
#[test]
fn a_signal_without_a_table_has_no_text() {
let db = CanDatabase::new(vec![MessageDef {
name: "Plain".into(),
id: 0x100,
extended: false,
length: 1,
signals: vec![probe(0, 8, false)],
}]);
assert_eq!(db.decode(0x100, &[7])[0].text, None);
}
#[test]
fn an_unknown_identifier_decodes_to_nothing() {
let db = CanDatabase::new(vec![MessageDef {
name: "Only".into(),
id: 0x100,
extended: false,
length: 8,
signals: vec![probe(0, 8, false)],
}]);
assert!(db.decode(0x101, &[0xFF; 8]).is_empty());
assert_eq!(db.decode(0x100, &[0xFF; 8]).len(), 1);
}
}