use crate::engine::bits::{Extracted, Sentinel, classify_sentinel, extract_bits};
use crate::engine::db::PgnDatabase;
use crate::engine::frame::RawFrame;
use crate::engine::types::{FieldInfo, FieldType, PgnInfo};
fn sentinel_field_value(f: &FieldInfo, ex: Extracted) -> Option<FieldValue> {
match classify_sentinel(ex, f.unknown_value, f.out_of_range_value, f.reserved_value) {
Sentinel::Unknown => Some(FieldValue::NotAvailable),
Sentinel::OutOfRange => Some(FieldValue::OutOfRange { value: ex.raw }),
Sentinel::Reserved => Some(FieldValue::ReservedValue { value: ex.raw }),
Sentinel::None => None,
}
}
fn unnamed_lookup_value(ex: Extracted, bits: u32) -> FieldValue {
if bits > 1 {
let band = if bits > 2 { 2 } else { 1 };
if ex.value >= ex.max - band {
return FieldValue::NotAvailable;
}
}
FieldValue::Lookup {
value: ex.raw,
name: None,
}
}
fn range_max_sentinel(f: &FieldInfo, ex: Extracted, bits: u32) -> Option<FieldValue> {
let range_max = f.range_max?;
let resolution = f.resolution.unwrap_or(1.0);
if bits == 0 || bits >= 64 || resolution <= 0.0 || !range_max.is_finite() {
return None;
}
let raw_max = (1u64 << bits) - 1;
let display_offset = f.offset.map(|o| o as f64).unwrap_or(0.0);
let raw_range_max_f = (range_max - display_offset - f.unit_offset) / resolution + 0.5;
if raw_range_max_f < 0.0 {
return None;
}
let raw_range_max = raw_range_max_f as u64;
if raw_range_max >= raw_max {
return None;
}
let by_size: u64 = match bits {
8.. => 3,
4..=7 => 2,
2..=3 => 1,
_ => 0,
};
let reserved = (raw_max - raw_range_max).min(by_size);
if reserved == 0 || ex.raw <= raw_max - reserved {
return None;
}
Some(match raw_max - ex.raw {
0 => FieldValue::NotAvailable,
1 => FieldValue::OutOfRange { value: ex.raw },
_ => FieldValue::ReservedValue { value: ex.raw },
})
}
fn range_min_out_of_range(f: &FieldInfo, ex: Extracted) -> Option<FieldValue> {
let range_min = f.range_min?;
let resolution = f.resolution.unwrap_or(1.0);
if resolution <= 0.0 || !range_min.is_finite() {
return None;
}
let display_offset = f.offset.map(|o| o as f64).unwrap_or(0.0);
let raw_min = ((range_min - display_offset - f.unit_offset) / resolution).round();
(raw_min > i64::MIN as f64 && ex.value < raw_min as i64)
.then_some(FieldValue::OutOfRange { value: ex.raw })
}
#[derive(Debug, Clone)]
pub struct DecodedField {
pub info: &'static FieldInfo,
pub value: FieldValue,
pub bit_offset: Option<u32>,
pub bit_length: Option<u32>,
pub repeat_index: Option<u32>,
pub repeat_set: u8,
pub(crate) overrides: Option<Box<FieldOverrides>>,
}
#[derive(Debug, Clone)]
pub struct FieldOverrides {
pub unit: Option<&'static str>,
pub resolution: Option<f64>,
pub precision: u8,
}
impl DecodedField {
#[inline]
pub fn order(&self) -> u8 {
self.info.order
}
#[inline]
pub fn id(&self) -> &'static str {
self.info.id
}
#[inline]
pub fn name(&self) -> &'static str {
self.info.name
}
#[inline]
pub fn unit(&self) -> Option<&'static str> {
if let Some(o) = self.overrides.as_deref()
&& o.unit.is_some()
{
return o.unit;
}
self.info.unit
}
pub fn as_f64_in(&self, target: &str) -> Option<f64> {
let v = self.value.as_f64()?;
match self.unit() {
Some(from) => crate::engine::units::convert_unit(v, from, target),
None => Some(v),
}
}
#[inline]
pub fn resolution(&self) -> Option<f64> {
if let Some(o) = self.overrides.as_deref()
&& o.resolution.is_some()
{
return o.resolution;
}
self.info.resolution
}
#[inline]
pub fn precision(&self) -> u8 {
if let Some(o) = self.overrides.as_deref()
&& o.precision > 0
{
return o.precision;
}
self.info.precision
}
#[inline]
pub fn part_of_primary_key(&self) -> bool {
self.info.part_of_primary_key.unwrap_or(false)
}
}
#[derive(Debug, Clone)]
pub enum FieldValue {
Number(f64),
Integer(i64),
Float(f64),
Binary(Vec<u8>),
Lookup {
value: u64,
name: Option<&'static str>,
},
BitField {
value: u64,
bits: Vec<(u64, Option<&'static str>)>,
},
Decimal(String),
String(String),
Date(u16),
Time { raw: i64, seconds: f64 },
Mmsi(u32),
Pgn {
value: u32,
description: Option<&'static str>,
},
IsoName {
value: u64,
subfields: Vec<DecodedField>,
},
Reserved {
value: u64,
bytes: Vec<u8>,
bit_length: u32,
},
Spare {
value: u64,
bytes: Vec<u8>,
bit_length: u32,
},
NotAvailable,
OutOfRange { value: u64 },
ReservedValue { value: u64 },
Unsupported { field_type: &'static str },
}
impl FieldValue {
#[inline]
pub fn as_f64(&self) -> Option<f64> {
match self {
FieldValue::Number(v) | FieldValue::Float(v) => Some(*v),
FieldValue::Integer(v) => Some(*v as f64),
FieldValue::Time { seconds, .. } => Some(*seconds),
FieldValue::Date(d) => Some(*d as f64),
FieldValue::Mmsi(v) => Some(*v as f64),
_ => None,
}
}
#[inline]
pub fn as_i64(&self) -> Option<i64> {
match self {
FieldValue::Integer(v) => Some(*v),
FieldValue::Number(v) => Some(*v as i64),
FieldValue::Lookup { value, .. } => Some(*value as i64),
FieldValue::BitField { value, .. } => Some(*value as i64),
FieldValue::Time { raw, .. } => Some(*raw),
FieldValue::Date(d) => Some(*d as i64),
FieldValue::Mmsi(v) => Some(*v as i64),
FieldValue::Pgn { value, .. } => Some(*value as i64),
_ => None,
}
}
#[inline]
pub fn lookup_value(&self) -> Option<u64> {
match self {
FieldValue::Lookup { value, .. } => Some(*value),
FieldValue::BitField { value, .. } => Some(*value),
_ => None,
}
}
#[inline]
pub fn as_str(&self) -> Option<&str> {
match self {
FieldValue::String(s) => Some(s.as_str()),
FieldValue::Lookup { name: Some(n), .. } => Some(*n),
_ => None,
}
}
#[inline]
pub fn is_not_available(&self) -> bool {
matches!(self, FieldValue::NotAvailable)
}
#[inline]
pub fn is_sentinel(&self) -> bool {
matches!(
self,
FieldValue::NotAvailable
| FieldValue::OutOfRange { .. }
| FieldValue::ReservedValue { .. }
)
}
}
#[derive(Debug, Clone)]
pub struct DecodedPgn {
pub timestamp: Option<String>,
pub prio: u8,
pub pgn: u32,
pub src: u8,
pub dst: u8,
pub description: &'static str,
pub id: &'static str,
pub id_is_pinned: bool,
pub data: Vec<u8>,
pub fields: Vec<DecodedField>,
pub has_repeating_set: [bool; 2],
pub index_by_order: [i8; 32],
}
impl DecodedPgn {
#[inline]
pub fn field(&self, f: crate::engine::FieldRef) -> Option<&DecodedField> {
debug_assert_eq!(
f.pgn.id, self.id,
"FieldRef/PGN mismatch: a `{}` field ref was used on a `{}` record",
f.pgn.id, self.id,
);
let idx_slot = (f.field.order as usize).checked_sub(1)?;
let idx = *self.index_by_order.get(idx_slot)?;
if idx < 0 {
return None;
}
self.fields.get(idx as usize)
}
#[inline]
pub fn field_by_name(&self, name: &str) -> Option<&DecodedField> {
self.fields
.iter()
.find(|f| f.repeat_set == 0 && f.name() == name)
}
pub fn iso_name(&self) -> Option<u64> {
if self.pgn != 60928 {
return None;
}
let f = |name: &str, bits: u32| -> u64 {
let mask = (1u64 << bits) - 1;
self.field_by_name(name)
.and_then(|df| match df.value {
FieldValue::OutOfRange { value } | FieldValue::ReservedValue { value } => {
Some(value)
}
ref v => v.as_i64().map(|v| v as u64),
})
.map_or(mask, |v| v & mask)
};
Some(
f("Unique Number", 21)
| f("Manufacturer Code", 11) << 21
| f("Device Instance Lower", 3) << 32
| f("Device Instance Upper", 5) << 35
| f("Device Function", 8) << 40
| f("Device Class", 7) << 49
| f("System Instance", 4) << 56
| f("Industry Group", 3) << 60
| f("Arbitrary address capable", 1) << 63,
)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DecodeError {
#[error("PGN {pgn} not found in database")]
#[non_exhaustive]
UnknownPgn { pgn: u32 },
#[error("PGN {pgn} payload too short for field {field:?}")]
#[non_exhaustive]
ShortPayload { pgn: u32, field: String },
}
impl PgnDatabase {
pub fn decode(&self, frame: &RawFrame) -> Result<DecodedPgn, DecodeError> {
let info = self
.pick_variant(frame)
.ok_or(DecodeError::UnknownPgn { pgn: frame.pgn })?;
let (mut fields, has_repeating_set) = decode_fields(info, &frame.data, self)?;
if frame.pgn == 60928 {
crate::engine::quirk::note_address_claim(frame.src, &frame.data);
}
crate::engine::quirk::apply(frame.pgn, frame.src, &mut fields);
let index_by_order = build_index_by_order(&fields);
Ok(DecodedPgn {
timestamp: frame.timestamp.clone(),
prio: frame.prio,
pgn: frame.pgn,
src: frame.src,
dst: frame.dst,
description: info.description,
id: info.id,
id_is_pinned: info.id_is_pinned,
data: frame.data.to_vec(),
fields,
has_repeating_set,
index_by_order,
})
}
pub fn pick_variant(&self, frame: &RawFrame) -> Option<&'static PgnInfo> {
let idx = (self.dispatch)(frame.pgn, &frame.data).or_else(|| (self.catchall)(frame.pgn))?;
Some(&self.pgn_slice()[idx])
}
}
#[derive(Default, Debug, Clone)]
struct DecodeContext {
target_pgn: Option<u32>,
current_param_idx: Option<u32>,
dynamic_field_type: Option<crate::engine::types::LookupFieldTypeValue>,
dynamic_length_bytes: Option<i64>,
previous_field_value: u64,
skip_field: bool,
resolved_target: Option<(&'static PgnInfo, bool)>,
}
fn decode_fields(
info: &'static PgnInfo,
data: &[u8],
db: &PgnDatabase,
) -> Result<(Vec<DecodedField>, [bool; 2]), DecodeError> {
let mut out = Vec::with_capacity(info.fields.len());
let mut ctx = DecodeContext::default();
let start1 = info.repeating_field_set1_start_field;
let size1 = info.repeating_field_set1_size;
let count_field1 = info.repeating_field_set1_count_field;
let start2 = info.repeating_field_set2_start_field;
let size2 = info.repeating_field_set2_size;
let count_field2 = info.repeating_field_set2_count_field;
let mut entered = [false, false];
let mut i = 0usize;
let mut cursor_bits: u32 = 0;
while i < info.fields.len() {
let f = &info.fields[i];
if let (Some(start), Some(size)) = (start1, size1)
&& (f.order as u32) == start
{
let bit_offset = f.bit_offset.unwrap_or(cursor_bits);
let total = data.len() as u32 * 8;
if bit_offset < total && !is_cut_short(f, bit_offset, total) {
entered[0] = true;
}
cursor_bits = decode_repeating(
info,
data,
db,
&mut out,
&mut ctx,
i,
size as usize,
count_field1,
cursor_bits,
1,
);
i += size as usize;
continue;
}
if let (Some(start), Some(size)) = (start2, size2)
&& (f.order as u32) == start
{
let bit_offset = f.bit_offset.unwrap_or(cursor_bits);
let total = data.len() as u32 * 8;
if bit_offset < total && !is_cut_short(f, bit_offset, total) {
entered[1] = true;
}
cursor_bits = decode_repeating(
info,
data,
db,
&mut out,
&mut ctx,
i,
size as usize,
count_field2,
cursor_bits,
2,
);
i += size as usize;
continue;
}
if !field_present(f.condition, ctx.target_pgn) {
i += 1;
continue;
}
let effective_offset = f.bit_offset.unwrap_or(cursor_bits);
if let Some((decoded, bits_consumed)) =
decode_one_field_at(f, info, data, db, effective_offset, &mut ctx)
{
cursor_bits = effective_offset + bits_consumed;
if core::mem::take(&mut ctx.skip_field) {
i += 1;
continue;
}
out.push(decoded);
}
i += 1;
}
Ok((out, entered))
}
pub(crate) fn build_index_by_order(fields: &[DecodedField]) -> [i8; 32] {
let mut idx = [i8::MIN; 32];
for (pos, f) in fields.iter().enumerate() {
if f.repeat_set != 0 {
continue;
}
if let Some(slot) = (f.order() as usize).checked_sub(1)
&& slot < idx.len()
&& pos < i8::MAX as usize
{
idx[slot] = pos as i8;
}
}
idx
}
#[allow(clippy::too_many_arguments)]
fn decode_repeating(
info: &'static PgnInfo,
data: &[u8],
db: &PgnDatabase,
out: &mut Vec<DecodedField>,
ctx: &mut DecodeContext,
start_idx: usize,
set_size: usize,
count_field_order: Option<u32>,
base_cursor: u32,
set_number: u8,
) -> u32 {
let raw_count: Option<u32> = count_field_order.and_then(|cf| {
out.iter()
.find(|d| (d.order() as u32) == cf)
.and_then(|d| match &d.value {
FieldValue::Integer(n) if *n >= 0 => Some(*n as u32),
FieldValue::Number(n) if *n >= 0.0 => Some(*n as u32),
_ => None,
})
});
let count_was_zero = raw_count == Some(0);
let max_iters: u32 = match raw_count {
Some(n) => n.max(1),
None => u32::MAX,
};
let set = &info.fields[start_idx..start_idx + set_size];
if set.is_empty() {
return base_cursor;
}
let payload_bits = (data.len() as u32).saturating_mul(8);
let mut iter_cursor = set[0].bit_offset.unwrap_or(base_cursor);
for iter in 0..max_iters {
if iter_cursor >= payload_bits {
break;
}
let mut sub_cursor = iter_cursor;
let mut produced_any = false;
let mut first_field_of_iter = true;
for sf in set {
if !field_present(sf.condition, ctx.target_pgn) {
continue;
}
let off = if iter == 0 {
sf.bit_offset.unwrap_or(sub_cursor)
} else {
sub_cursor
};
if let Some((mut d, bits)) = decode_one_field_at(sf, info, data, db, off, ctx) {
d.repeat_set = set_number;
d.repeat_index = if count_was_zero && !first_field_of_iter {
None
} else {
Some(iter)
};
first_field_of_iter = false;
sub_cursor = off + bits;
if !core::mem::take(&mut ctx.skip_field) {
out.push(d);
}
produced_any = true;
} else if let Some(bl) = sf.bit_length {
sub_cursor = off + bl;
}
}
if !produced_any {
break;
}
iter_cursor = sub_cursor;
}
iter_cursor
}
fn decode_one_field_at(
f: &'static FieldInfo,
info: &'static PgnInfo,
data: &[u8],
db: &PgnDatabase,
bit_offset: u32,
ctx: &mut DecodeContext,
) -> Option<(DecodedField, u32)> {
let signed = f.signed.unwrap_or(false);
let offset_k = f.offset.unwrap_or(0);
if matches!(f.field_type, Some(FieldType::StringLau)) {
let (value, bits_consumed) = decode_string_lau(data, bit_offset, f.encoding);
return Some((
DecodedField {
info: f,
value,
bit_offset: Some(bit_offset),
bit_length: Some(bits_consumed),
repeat_index: None,
repeat_set: 0,
overrides: None,
},
bits_consumed,
));
}
if matches!(f.field_type, Some(FieldType::Variable)) {
return decode_variable(f, data, db, bit_offset, ctx);
}
if matches!(f.field_type, Some(FieldType::DynamicFieldValue)) {
let overrides = ctx.dynamic_field_type.as_ref().map(|v| {
Box::new(FieldOverrides {
unit: v.unit,
resolution: v.resolution,
precision: v.precision,
})
});
let (val, consumed_bits) = decode_dynamic_field_value(data, bit_offset, db, ctx);
return Some((
DecodedField {
info: f,
value: val,
bit_offset: Some(bit_offset),
bit_length: Some(consumed_bits),
repeat_index: None,
repeat_set: 0,
overrides,
},
consumed_bits,
));
}
if matches!(
f.field_type,
Some(FieldType::StringLz) | Some(FieldType::Binary)
) && f.bit_length.is_none()
{
let payload_bits = (data.len() as u32).saturating_mul(8);
let avail = payload_bits.saturating_sub(bit_offset);
let avail = avail - (avail & 7); let value = match f.field_type {
Some(FieldType::StringLz) => decode_string_lz(data, bit_offset, avail, f.encoding),
_ if ctx.previous_field_value > 0 && (ctx.previous_field_value as u32) <= avail => {
decode_binary(data, bit_offset, ctx.previous_field_value as u32)
}
_ => decode_binary(data, bit_offset, avail),
};
return Some((
DecodedField {
info: f,
value,
bit_offset: Some(bit_offset),
bit_length: Some(avail),
repeat_index: None,
repeat_set: 0,
overrides: None,
},
avail,
));
}
let bit_length = f.bit_length?;
let total = data.len() as u32 * 8;
if is_cut_short(f, bit_offset, total) {
ctx.skip_field = true;
let avail = total.saturating_sub(bit_offset);
return Some((
DecodedField {
info: f,
value: FieldValue::NotAvailable,
bit_offset: Some(bit_offset),
bit_length: Some(avail),
repeat_index: None,
repeat_set: 0,
overrides: None,
},
avail,
));
}
if f.continues.is_some() {
ctx.skip_field = true;
}
let joined;
let (data, at, width) = match join_continuation(f, data, bit_offset, bit_length) {
Some((value, width)) => {
joined = value.to_le_bytes();
(&joined[..], 0, width)
}
None => (data, bit_offset, bit_length),
};
let value = decode_scalar(f, info, data, db, at, width, signed, offset_k, ctx);
finish_scalar(f, value, bit_offset, bit_length, ctx)
}
fn is_cut_short(f: &FieldInfo, bit_offset: u32, total: u32) -> bool {
let Some(bit_length) = f.bit_length else {
return false;
};
let whole = f.continuation.map_or(bit_length, |c| {
bit_length.max(c.bit_offset.saturating_add(c.bit_length))
});
bit_offset.saturating_add(whole) > total
&& !matches!(
f.field_type,
Some(FieldType::Binary | FieldType::StringFix | FieldType::StringLz)
)
}
fn join_continuation(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
) -> Option<(u64, u32)> {
let c = f.continuation?;
let width = bit_length + c.bit_length;
let payload_bits = data.len() as u32 * 8;
if width > 64
|| bit_offset + bit_length > payload_bits
|| bit_offset + c.bit_offset + c.bit_length > payload_bits
{
return None;
}
let low = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0)?;
let high = extract_bits(
data,
(bit_offset + c.bit_offset) as usize,
c.bit_length as usize,
false,
0,
)?;
Some((low.raw | (high.raw << bit_length), width))
}
#[allow(clippy::too_many_arguments)]
fn decode_scalar(
f: &'static FieldInfo,
info: &'static PgnInfo,
data: &[u8],
db: &PgnDatabase,
bit_offset: u32,
bit_length: u32,
signed: bool,
offset_k: i64,
ctx: &mut DecodeContext,
) -> FieldValue {
match f.field_type {
Some(FieldType::Decimal) => decode_decimal(
data,
bit_offset,
bit_length,
trailing_zero_is_digit(f, info, data),
),
Some(FieldType::Number) => decode_number(f, data, bit_offset, bit_length, signed, offset_k),
Some(FieldType::Float) => decode_float(f, data, bit_offset, bit_length),
Some(FieldType::Lookup) => decode_lookup(f, data, bit_offset, bit_length, db),
Some(FieldType::IndirectLookup) => {
decode_indirect_lookup(f, info, data, bit_offset, bit_length, db)
}
Some(FieldType::BitLookup) => decode_bitlookup(f, data, bit_offset, bit_length, db),
Some(FieldType::Reserved) => {
decode_reserved(data, bit_offset, bit_length, signed, offset_k, true)
}
Some(FieldType::Spare) => {
decode_reserved(data, bit_offset, bit_length, signed, offset_k, false)
}
Some(FieldType::Binary) => decode_binary(data, bit_offset, bit_length),
Some(FieldType::Mmsi) => decode_mmsi(f, data, bit_offset, bit_length),
Some(FieldType::Pgn) => decode_pgn_field(data, bit_offset, bit_length, db),
Some(FieldType::Date) => decode_date(f, data, bit_offset, bit_length),
Some(FieldType::Time) | Some(FieldType::Duration) => {
decode_time(f, data, bit_offset, bit_length, signed)
}
Some(FieldType::StringFix) => decode_string_fix(data, bit_offset, bit_length, f.encoding),
Some(FieldType::StringLz) => decode_string_lz(data, bit_offset, bit_length, f.encoding),
Some(FieldType::StringLau) => unreachable!("STRING_LAU handled above"),
Some(FieldType::Variable) => unreachable!("VARIABLE handled above"),
Some(FieldType::IsoName) => decode_iso_name(data, bit_offset, bit_length, db),
Some(FieldType::DynamicFieldKey) => {
decode_dynamic_field_key(f, data, db, bit_offset, bit_length, ctx)
}
Some(FieldType::DynamicFieldLength) => {
decode_dynamic_field_length(f, data, bit_offset, bit_length, ctx)
}
Some(FieldType::DynamicFieldValue) => unreachable!("DYNAMIC_FIELD_VALUE handled above"),
Some(FieldType::FieldIndex) => decode_number(f, data, bit_offset, bit_length, signed, 0),
Some(FieldType::Address) => decode_number(f, data, bit_offset, bit_length, signed, 0),
None => FieldValue::Unsupported {
field_type: "<no field type>",
},
}
}
fn finish_scalar(
f: &'static FieldInfo,
value: FieldValue,
bit_offset: u32,
bit_length: u32,
ctx: &mut DecodeContext,
) -> Option<(DecodedField, u32)> {
match &value {
FieldValue::Pgn { value: p, .. } => ctx.target_pgn = Some(*p),
FieldValue::Integer(n) if matches!(f.field_type, Some(FieldType::FieldIndex)) => {
ctx.current_param_idx = Some(*n as u32);
}
_ => {}
}
if let FieldValue::Integer(n) = &value
&& *n >= 0
{
ctx.previous_field_value = *n as u64;
}
if f.is_dynamic_length_marker {
let raw_len = match &value {
FieldValue::Integer(n) if *n >= 0 => Some(*n),
FieldValue::Number(n) if *n >= 0.0 => Some(*n as i64),
_ => None,
};
if let Some(n) = raw_len {
ctx.dynamic_length_bytes = Some(n - i64::from(f.dynamic_field_length_overhead));
}
}
Some((
DecodedField {
info: f,
value,
bit_offset: Some(bit_offset),
bit_length: Some(bit_length),
repeat_index: None,
repeat_set: 0,
overrides: None,
},
bit_length,
))
}
pub(crate) fn is_pgn_proprietary(n: u32) -> bool {
(0xEF00..=0xEFFF).contains(&n)
|| (0xFF00..=0xFFFF).contains(&n)
|| (0x1_EF00..=0x1_EFFF).contains(&n)
|| (0x1_FF00..=0x1_FFFF).contains(&n)
}
pub(crate) fn field_present(condition: Option<&str>, target_pgn: Option<u32>) -> bool {
match condition {
None => true,
Some("PGNIsProprietary") => target_pgn.is_some_and(is_pgn_proprietary),
Some(_) => false,
}
}
fn match_pgn_by_parameters(
db: &PgnDatabase,
pgn_id: u32,
params: &[u8],
) -> Option<&'static PgnInfo> {
let first = db.specific_variants(pgn_id).next()?;
if is_pgn_proprietary(pgn_id)
&& (params.len() < 6 || params[0] < 2 || params[1] != 0x01 || params[4] != 3)
{
return None;
}
if !first.fields.iter().any(|fi| fi.match_value.is_some()) {
return Some(first);
}
let nparams = *params.first()? as usize;
'variant: for cand in db.specific_variants(pgn_id) {
let mut d = 1usize;
for _ in 0..nparams {
if d >= params.len() {
break; }
let index = params[d] as usize;
d += 1;
if index == 0 || index > cand.fields.len() {
continue 'variant;
}
let field = &cand.fields[index - 1];
let bits = field.bit_length.unwrap_or(0) as usize;
if let Some(want) = field.match_value {
let Some(ex) = extract_bits(params, d * 8, bits, field.signed.unwrap_or(false), 0)
else {
continue 'variant;
};
if ex.value != want {
continue 'variant;
}
}
d += bits.div_ceil(8);
}
return Some(cand);
}
None
}
fn decode_variable(
f: &'static FieldInfo,
data: &[u8],
db: &PgnDatabase,
bit_offset: u32,
ctx: &mut DecodeContext,
) -> Option<(DecodedField, u32)> {
let target_pgn = ctx.target_pgn?;
let target_idx = ctx.current_param_idx?;
let (target_info, used_catch_all) = match ctx.resolved_target {
Some(r) => r,
None => {
let params = (bit_offset as usize / 8)
.checked_sub(2)
.and_then(|s| data.get(s..))
.unwrap_or(&[]);
let r = match match_pgn_by_parameters(db, target_pgn, params) {
Some(p) => (p, false),
None => (
db.fallback_pgn(target_pgn)
.or_else(|| db.first_pgn(target_pgn))?,
true,
),
};
ctx.resolved_target = Some(r);
r
}
};
let target_field = target_info
.fields
.iter()
.find(|tf| (tf.order as u32) == target_idx)?;
if used_catch_all
&& (target_field.bit_length.unwrap_or(0) == 0
|| target_field.bit_length_variable == Some(true))
{
let start = bit_offset as usize;
let total = data.len() * 8;
if start >= total {
return None;
}
let bits = (total - start) as u32;
let value = decode_binary(data, bit_offset, bits);
return Some((
DecodedField {
info: f,
value,
bit_offset: Some(bit_offset),
bit_length: Some(bits),
repeat_index: None,
repeat_set: 0,
overrides: None,
},
bits,
));
}
let mut sub_ctx = DecodeContext::default();
let (sub, bits) = decode_one_field_at(
target_field,
target_info,
data,
db,
bit_offset,
&mut sub_ctx,
)?;
ctx.skip_field |= sub_ctx.skip_field;
let bits_byte_aligned = bits.div_ceil(8) * 8;
let overrides = Some(Box::new(FieldOverrides {
unit: target_field.unit,
resolution: target_field.resolution,
precision: target_field.precision,
}));
Some((
DecodedField {
info: f,
value: sub.value,
bit_offset: Some(bit_offset),
bit_length: target_field.bit_length.or(Some(bits_byte_aligned)),
repeat_index: None,
repeat_set: 0,
overrides,
},
bits_byte_aligned,
))
}
fn decode_number(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
signed: bool,
_offset_k: i64,
) -> FieldValue {
let display_offset = f.offset.map(|o| o as f64).unwrap_or(0.0);
let has_display_offset = f.offset.unwrap_or(0) != 0;
let effective_signed = if has_display_offset { false } else { signed };
let Some(ex) = extract_bits(
data,
bit_offset as usize,
bit_length as usize,
effective_signed,
0,
) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
if !effective_signed && let Some(sent) = range_max_sentinel(f, ex, bit_length) {
return sent;
}
if effective_signed && let Some(sent) = range_min_out_of_range(f, ex) {
return sent;
}
let resolution = f.resolution.unwrap_or(1.0);
let unit = f.unit;
if resolution == 1.0
&& unit.is_none()
&& f.physical_quantity.is_none()
&& f.unit_offset == 0.0
&& display_offset == 0.0
{
FieldValue::Integer(ex.value)
} else {
FieldValue::Number(ex.value as f64 * resolution + display_offset + f.unit_offset)
}
}
fn decode_float(f: &FieldInfo, data: &[u8], bit_offset: u32, bit_length: u32) -> FieldValue {
if bit_length != 32 {
return FieldValue::Unsupported {
field_type: "FLOAT (non-32-bit)",
};
}
let Some(ex) = extract_bits(data, bit_offset as usize, 32, false, 0) else {
return FieldValue::NotAvailable;
};
let bits = ex.value as u32;
let v = f32::from_bits(bits) as f64;
if v.is_nan() {
return FieldValue::NotAvailable;
}
FieldValue::Float(match f.resolution {
Some(r) if r != 0.0 => v * r + f.unit_offset,
_ => v,
})
}
fn decode_lookup(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
db: &PgnDatabase,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0) else {
return FieldValue::NotAvailable;
};
let raw = ex.raw;
let name = f
.lookup_enumeration
.and_then(|n| db.lookup(n))
.and_then(|t| t.get(raw))
.map(|v| v.name);
if name.is_some() {
return FieldValue::Lookup { value: raw, name };
}
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
unnamed_lookup_value(ex, bit_length)
}
fn in_same_repeating_set(info: &PgnInfo, a: u32, b: u32) -> bool {
[
(
info.repeating_field_set1_start_field,
info.repeating_field_set1_size,
),
(
info.repeating_field_set2_start_field,
info.repeating_field_set2_size,
),
]
.iter()
.any(|(start, size)| match (start, size) {
(Some(s), Some(n)) if *n > 0 => (*s..*s + *n).contains(&a) && (*s..*s + *n).contains(&b),
_ => false,
})
}
fn indirect_key_offset(
value_field: &FieldInfo,
key_field: &FieldInfo,
info: &PgnInfo,
self_bit: u32,
) -> Option<usize> {
let key_static = key_field.bit_offset?;
if !in_same_repeating_set(info, value_field.order as u32, key_field.order as u32) {
return Some(key_static as usize);
}
let value_static = value_field.bit_offset?;
let delta = value_static.checked_sub(key_static)?;
Some(self_bit.checked_sub(delta)? as usize)
}
fn decode_indirect_lookup(
f: &FieldInfo,
info: &PgnInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
db: &PgnDatabase,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0) else {
return FieldValue::NotAvailable;
};
let raw = ex.raw;
let name = (|| -> Option<&'static str> {
let table_name = f.lookup_indirect_enumeration?;
let val1_order = f.lookup_indirect_enumeration_field_order?;
let val1_field = info.fields.iter().find(|x| x.order == val1_order)?;
let val1_len = val1_field.bit_length?;
let val1_off = indirect_key_offset(f, val1_field, info, bit_offset)?;
let val1 = extract_bits(data, val1_off, val1_len as usize, false, 0)?;
db.indirect_lookup(table_name, val1.raw, raw)
})();
if name.is_some() {
return FieldValue::Lookup { value: raw, name };
}
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
FieldValue::Lookup {
value: raw,
name: None,
}
}
fn decode_bitlookup(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
db: &PgnDatabase,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0) else {
return FieldValue::NotAvailable;
};
let raw = ex.value as u64;
let mut bits = Vec::new();
let table = f.lookup_bit_enumeration.and_then(|n| db.bit_lookup(n));
for bit in 0..bit_length.min(64) {
if raw & (1u64 << bit) != 0 {
bits.push((
1u64 << bit,
table.and_then(|t| t.get(bit as u8)).map(|v| v.name),
));
}
}
FieldValue::BitField { value: raw, bits }
}
fn decode_reserved(
data: &[u8],
bit_offset: u32,
bit_length: u32,
signed: bool,
offset_k: i64,
is_reserved: bool,
) -> FieldValue {
let Some(ex) = extract_bits(
data,
bit_offset as usize,
bit_length as usize,
signed,
offset_k,
) else {
return FieldValue::NotAvailable;
};
let raw = ex.value as u64;
if is_reserved && ex.value == ex.max {
return FieldValue::Reserved {
value: raw,
bytes: Vec::new(),
bit_length,
};
}
let bytes = match decode_binary(data, bit_offset, bit_length) {
FieldValue::Binary(b) => b,
_ => Vec::new(),
};
if is_reserved {
FieldValue::Reserved {
value: raw,
bytes,
bit_length,
}
} else {
FieldValue::Spare {
value: raw,
bytes,
bit_length,
}
}
}
fn decode_iso_name(data: &[u8], bit_offset: u32, bit_length: u32, db: &PgnDatabase) -> FieldValue {
if bit_length != 64 || (bit_offset & 7) != 0 {
return FieldValue::Unsupported {
field_type: "ISO_NAME (unaligned or non-64-bit)",
};
}
let byte_off = (bit_offset / 8) as usize;
if byte_off + 8 > data.len() {
return FieldValue::NotAvailable;
}
let mut value: u64 = 0;
for i in 0..8 {
value |= (data[byte_off + i] as u64) << (i * 8);
}
match value {
u64::MAX => return FieldValue::NotAvailable,
v if v == u64::MAX - 1 => return FieldValue::OutOfRange { value },
_ => {}
}
let sub = &data[byte_off..byte_off + 8];
let subfields = match db.first_pgn(60928) {
Some(pgn) => decode_fields(pgn, sub, db).map(|x| x.0).unwrap_or_default(),
None => Vec::new(),
};
FieldValue::IsoName { value, subfields }
}
fn decode_decimal(data: &[u8], bit_offset: u32, bit_length: u32, keep_last: bool) -> FieldValue {
let start_byte = (bit_offset / 8) as usize;
if start_byte >= data.len() {
return FieldValue::NotAvailable;
}
let avail = (data.len() as u32 * 8).saturating_sub(bit_offset);
if bit_length > avail {
return FieldValue::NotAvailable;
}
let bits = bit_length;
let nbytes = bits.div_ceil(8) as usize;
let end = (start_byte + nbytes).min(data.len());
if data[start_byte..end].iter().all(|&b| b == 0xff) {
return FieldValue::NotAvailable;
}
let mut out = String::with_capacity(nbytes * 2);
let mut value: u32 = 0;
let mut magnitude: u32 = 1;
let mut mask: u8 = 1 << (bit_offset % 8);
let mut idx = start_byte;
for bit in 0..bits {
let Some(&byte) = data.get(idx) else { break };
if byte & mask != 0 {
value |= magnitude;
}
if mask == 128 {
mask = 1;
idx += 1;
} else {
mask <<= 1;
}
magnitude <<= 1;
if bit % 8 == 7 {
if value >= 100 {
return FieldValue::NotAvailable;
}
out.push((b'0' + (value / 10) as u8) as char);
out.push((b'0' + (value % 10) as u8) as char);
value = 0;
magnitude = 1;
}
}
if out.is_empty() {
return FieldValue::NotAvailable;
}
if out.ends_with('0') && !keep_last {
out.pop();
}
FieldValue::Decimal(out)
}
fn trailing_zero_is_digit(f: &FieldInfo, info: &PgnInfo, data: &[u8]) -> bool {
(|| {
let (order, value) = f.trailing_zero_is_digit?;
let key = info.fields.iter().find(|x| x.order == order)?;
let raw = extract_bits(
data,
key.bit_offset? as usize,
key.bit_length? as usize,
false,
0,
)?
.raw;
Some(raw as i64 == value)
})()
.unwrap_or(false)
}
fn decode_binary(data: &[u8], bit_offset: u32, bit_length: u32) -> FieldValue {
let bit_offset = bit_offset as usize;
let bit_length = bit_length as usize;
if bit_offset & 7 == 0 && bit_length & 7 == 0 {
let start = bit_offset / 8;
if start >= data.len() {
return FieldValue::NotAvailable;
}
let declared_end = start + bit_length / 8;
let end = declared_end.min(data.len());
return FieldValue::Binary(data[start..end].to_vec());
}
let start_byte = bit_offset / 8;
let start_bit = bit_offset % 8;
let avail = data.len().saturating_mul(8).saturating_sub(bit_offset);
let bit_length = bit_length.min(avail);
if bit_length == 0 {
return FieldValue::NotAvailable;
}
let nbytes = bit_length.div_ceil(8);
let mut out = Vec::with_capacity(nbytes);
let mut remaining = bit_length as isize;
for i in 0..nbytes {
let Some(&raw) = data.get(start_byte + i) else {
break;
};
let mut byte = raw;
if i == 0 && start_bit != 0 {
byte >>= start_bit;
if (remaining as usize) + start_bit < 8 {
byte &= (1u8 << remaining) - 1;
}
byte <<= start_bit;
remaining -= (8 - start_bit) as isize;
} else {
if remaining < 8 {
byte &= (1u8 << remaining) - 1;
}
remaining -= 8;
}
out.push(byte);
}
FieldValue::Binary(out)
}
fn decode_mmsi(f: &FieldInfo, data: &[u8], bit_offset: u32, bit_length: u32) -> FieldValue {
if bit_length != 32 {
return FieldValue::Unsupported {
field_type: "MMSI (non-32-bit)",
};
}
let Some(ex) = extract_bits(data, bit_offset as usize, 32, false, 0) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = range_max_sentinel(f, ex, 32) {
return sent;
}
if ex.value == 0 {
return FieldValue::NotAvailable;
}
FieldValue::Mmsi(ex.value as u32)
}
fn decode_pgn_field(data: &[u8], bit_offset: u32, bit_length: u32, db: &PgnDatabase) -> FieldValue {
if bit_length != 24 {
return FieldValue::Unsupported {
field_type: "PGN (non-24-bit)",
};
}
let Some(ex) = extract_bits(data, bit_offset as usize, 24, false, 0) else {
return FieldValue::NotAvailable;
};
let pgn = ex.value as u32;
let variants: Vec<&'static PgnInfo> = db.pgn_variants(pgn).collect();
let description =
if variants.len() == 1 && !variants[0].fields.iter().any(|f| f.match_value.is_some()) {
Some(variants[0].description)
} else {
None
};
FieldValue::Pgn {
value: pgn,
description,
}
}
fn decode_date(f: &FieldInfo, data: &[u8], bit_offset: u32, bit_length: u32) -> FieldValue {
if bit_length != 16 {
return FieldValue::Unsupported {
field_type: "DATE (non-16-bit)",
};
}
let Some(ex) = extract_bits(data, bit_offset as usize, 16, false, 0) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
FieldValue::Date(ex.value as u16)
}
fn decode_time(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
signed: bool,
) -> FieldValue {
let Some(ex) = extract_bits(
data,
bit_offset as usize,
bit_length as usize,
signed,
f.offset.unwrap_or(0),
) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
let resolution = f.resolution.unwrap_or(1.0);
FieldValue::Time {
raw: ex.value,
seconds: ex.value as f64 * resolution,
}
}
fn decode_string_fix(
data: &[u8],
bit_offset: u32,
bit_length: u32,
encoding: Option<&str>,
) -> FieldValue {
let bo = bit_offset as usize;
let bl = bit_length as usize;
if bo & 7 != 0 || bl & 7 != 0 {
return FieldValue::Unsupported {
field_type: "STRING_FIX (unaligned)",
};
}
let start = bo / 8;
if start >= data.len() {
return FieldValue::NotAvailable;
}
let end = (start + bl / 8).min(data.len());
let raw = &data[start..end];
let mut len = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
while len > 0 && is_string_padding(raw[len - 1]) {
len -= 1;
}
if len == 0 {
return FieldValue::NotAvailable;
}
let s = decode_text(&raw[..len], encoding);
FieldValue::String(s)
}
fn decode_string_lau(data: &[u8], bit_offset: u32, charset: Option<&str>) -> (FieldValue, u32) {
let bo = bit_offset as usize;
if bo & 7 != 0 {
return (
FieldValue::Unsupported {
field_type: "STRING_LAU (unaligned)",
},
8,
);
}
let start = bo / 8;
if start + 2 > data.len() {
return (FieldValue::NotAvailable, 16);
}
let total_len = data[start] as usize;
let encoding = data[start + 1];
if total_len < 2 {
return (FieldValue::NotAvailable, (total_len.max(2) * 8) as u32);
}
let body_len = total_len - 2;
let body_end = (start + 2 + body_len).min(data.len());
let body = &data[start + 2..body_end];
let bits_consumed = (total_len * 8) as u32;
let s = match encoding {
0 => {
let mut code_units = Vec::with_capacity(body.len() / 2);
let mut i = 0;
while i + 1 < body.len() {
code_units.push(u16::from_le_bytes([body[i], body[i + 1]]));
i += 2;
}
String::from_utf16_lossy(&code_units)
}
_ => {
let end = body
.iter()
.rposition(|&b| !is_string_padding(b))
.map_or(0, |i| i + 1);
decode_text(&body[..end], charset)
}
};
let trimmed = trim_string_padding(&s);
if trimmed.is_empty() {
return (FieldValue::NotAvailable, bits_consumed);
}
(FieldValue::String(trimmed.to_string()), bits_consumed)
}
fn decode_text(v: &[u8], encoding: Option<&str>) -> String {
match std::str::from_utf8(v) {
Ok(s) => s.to_string(),
Err(_) => match encoding {
Some("RDS_G0") => v.iter().map(|&b| crate::engine::rds_g0_char(b)).collect(),
_ => v.iter().map(|&b| b as char).collect(),
},
}
}
fn is_string_padding(b: u8) -> bool {
matches!(b, 0xff | 0x00 | b'@') || b.is_ascii_whitespace() || b == 0x0b
}
fn trim_string_padding(s: &str) -> &str {
s.trim_end_matches(|c: char| match c {
'\u{ff}' => true,
c if c.is_ascii() => is_string_padding(c as u8),
_ => false,
})
}
fn decode_string_lz(
data: &[u8],
bit_offset: u32,
bit_length: u32,
encoding: Option<&str>,
) -> FieldValue {
let bo = bit_offset as usize;
let bl = bit_length as usize;
if bo & 7 != 0 {
return FieldValue::Unsupported {
field_type: "STRING_LZ (unaligned)",
};
}
let start = bo / 8;
if start >= data.len() {
return FieldValue::NotAvailable;
}
let region_end = if bl == 0 {
data.len()
} else {
(start + bl / 8).min(data.len())
};
let len_byte = data[start] as usize;
let content_start = start + 1;
let max_avail = region_end.saturating_sub(content_start);
let content_end = content_start + len_byte.min(max_avail);
let raw = decode_text(&data[content_start..content_end], encoding);
let trimmed = trim_string_padding(&raw);
if trimmed.is_empty() {
FieldValue::NotAvailable
} else {
FieldValue::String(trimmed.to_string())
}
}
fn decode_dynamic_field_key(
f: &FieldInfo,
data: &[u8],
db: &PgnDatabase,
bit_offset: u32,
bit_length: u32,
ctx: &mut DecodeContext,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0) else {
return FieldValue::NotAvailable;
};
let raw = ex.raw;
let entry = f
.lookup_field_type_enumeration
.and_then(|n| db.field_type_lookup(n, raw));
let name = entry.map(|e| e.name);
ctx.dynamic_field_type = entry.copied();
if name.is_some() {
return FieldValue::Lookup { value: raw, name };
}
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
FieldValue::Lookup {
value: raw,
name: None,
}
}
fn decode_dynamic_field_length(
f: &FieldInfo,
data: &[u8],
bit_offset: u32,
bit_length: u32,
ctx: &mut DecodeContext,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, false, 0) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = sentinel_field_value(f, ex) {
return sent;
}
let len = ex.raw as i64 - i64::from(f.dynamic_field_length_overhead);
ctx.dynamic_length_bytes = Some(len);
FieldValue::Integer(ex.value)
}
fn decode_dynamic_field_value(
data: &[u8],
bit_offset: u32,
db: &PgnDatabase,
ctx: &mut DecodeContext,
) -> (FieldValue, u32) {
let entry = ctx.dynamic_field_type.take();
let explicit_len = ctx.dynamic_length_bytes.take();
let length_bits: Option<i64> = explicit_len
.map(|n| n * 8)
.or_else(|| entry.as_ref().and_then(|e| e.bit_length()).map(i64::from));
if explicit_len == Some(0) {
ctx.skip_field = true;
return (FieldValue::NotAvailable, 0);
}
let bits_signed = length_bits.unwrap_or(0);
let remaining = (data.len() as u32 * 8).saturating_sub(bit_offset);
if bits_signed < 0 {
return (decode_binary(data, bit_offset, remaining), remaining);
}
if (data.len() as i64) < (bit_offset as i64 + bits_signed) >> 3 {
ctx.skip_field = true;
return (FieldValue::NotAvailable, remaining);
}
let bits = bits_signed as u32;
let Some(entry) = entry else {
let bits = if bits == 0 && explicit_len.is_none() {
remaining
} else {
bits
};
return (decode_binary(data, bit_offset, bits), bits);
};
let signed = entry.signed || matches!(entry.field_type, Some(ft) if ft.starts_with("FIX"));
let val = match entry.field_type {
Some(ft) if ft.starts_with("NUMBER") || ft.starts_with("FIX") || ft.starts_with("UFIX") => {
decode_dynamic_number(data, bit_offset, bits, signed, &entry)
}
Some("LOOKUP") => match entry.lookup_enumeration {
Some(name) => {
let Some(ex) = extract_bits(data, bit_offset as usize, bits as usize, false, 0)
else {
return (FieldValue::NotAvailable, bits);
};
match db.lookup(name).and_then(|t| t.get(ex.raw)).map(|v| v.name) {
Some(nm) => FieldValue::Lookup {
value: ex.raw,
name: Some(nm),
},
None => unnamed_lookup_value(ex, bits),
}
}
None => decode_binary(data, bit_offset, bits),
},
Some("DURATION") | Some("TIME") => {
let known_signed_duration = matches!(entry.name, "Race Timer" | "Timezone offset");
let want_signed = signed || known_signed_duration;
let Some(ex_unsigned) =
extract_bits(data, bit_offset as usize, bits as usize, false, 0)
else {
return (FieldValue::NotAvailable, bits);
};
let ex = if want_signed {
match extract_bits(data, bit_offset as usize, bits as usize, true, 0) {
Some(e) => e,
None => return (FieldValue::NotAvailable, bits),
}
} else {
ex_unsigned
};
if let Some(sent) = dynamic_sentinel(ex, bits) {
return (sent, bits);
}
let res = entry.resolution.unwrap_or(1.0);
FieldValue::Time {
raw: ex.value,
seconds: ex.value as f64 * res,
}
}
Some("DATE") => {
let Some(ex) = extract_bits(data, bit_offset as usize, bits as usize, false, 0) else {
return (FieldValue::NotAvailable, bits);
};
FieldValue::Date(ex.value as u16)
}
Some("ISO_NAME") => decode_iso_name(data, bit_offset, bits, db),
_ => decode_binary(data, bit_offset, bits),
};
(val, bits)
}
fn dynamic_sentinel(ex: Extracted, bits: u32) -> Option<FieldValue> {
let reserved: i64 = match bits {
8.. => 3,
4..=7 => 2,
2..=3 => 1,
_ => 0,
};
if ex.value <= ex.max - reserved {
return None;
}
Some(match ex.max - ex.value {
0 => FieldValue::NotAvailable,
1 => FieldValue::OutOfRange { value: ex.raw },
_ => FieldValue::ReservedValue { value: ex.raw },
})
}
fn decode_dynamic_number(
data: &[u8],
bit_offset: u32,
bit_length: u32,
signed: bool,
entry: &crate::engine::types::LookupFieldTypeValue,
) -> FieldValue {
let Some(ex) = extract_bits(data, bit_offset as usize, bit_length as usize, signed, 0) else {
return FieldValue::NotAvailable;
};
if let Some(sent) = dynamic_sentinel(ex, bit_length) {
return sent;
}
let raw = ex.value as f64;
let res = entry.resolution.unwrap_or(1.0);
if res == 1.0 && entry.unit.is_none() {
FieldValue::Integer(ex.value)
} else {
FieldValue::Number(raw * res + entry.unit_offset)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::PgnDatabase;
fn db() -> &'static PgnDatabase {
PgnDatabase::embedded(crate::engine::Units::Metric)
}
#[test]
fn j1939_gears_are_offset_by_125() {
let db = PgnDatabase::embedded_j1939(crate::engine::Units::Metric);
let gears = |selected: u8, current: u8| {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 61445,
src: 3,
dst: 255,
data: smallvec::smallvec![selected, 0xe8, 0x03, current, 0x44, 0x20, 0x52, 0x20],
};
let d = db.decode(&frame).expect("decode");
let gear = |name: &str| d.field_by_name(name).map(|f| f.value.clone());
(
gear("Transmission Selected Gear"),
gear("Transmission Current Gear"),
)
};
let number = |v: &Option<FieldValue>| match v {
Some(FieldValue::Number(x)) => Some(*x),
_ => None,
};
let (selected, current) = gears(125, 124);
assert_eq!(number(&selected), Some(0.0), "neutral");
assert_eq!(number(¤t), Some(-1.0), "reverse");
let (park, unknown) = gears(251, 0xff);
assert_eq!(number(&park), Some(126.0), "Park");
assert!(
matches!(unknown, Some(FieldValue::NotAvailable)),
"{unknown:?}"
);
}
#[test]
fn a_value_below_range_min_is_out_of_range() {
let decode = |pgn: u32, hex: &str| {
let data: smallvec::SmallVec<[u8; 8]> = hex
.split(',')
.map(|h| u8::from_str_radix(h, 16).unwrap())
.collect();
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn,
src: 1,
dst: 255,
data,
};
db().decode(&frame).expect("decode")
};
let value = |d: &DecodedPgn, name: &str| d.field_by_name(name).map(|f| f.value.clone());
let aton = decode(
129041,
"ff,30,86,27,3b,56,55,55,bd,56,55,55,bd,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,fe,ff,ff,02,01",
);
assert!(matches!(
value(&aton, "Latitude"),
Some(FieldValue::OutOfRange { value: 0xbd55_5556 })
));
assert!(matches!(
value(&aton, "Longitude"),
Some(FieldValue::Number(_))
));
let attitude = decode(127257, "01,00,80,47,85,48,85,ff");
assert!(matches!(
value(&attitude, "Yaw"),
Some(FieldValue::OutOfRange { .. })
));
assert!(matches!(
value(&attitude, "Pitch"),
Some(FieldValue::OutOfRange { .. })
));
assert!(matches!(
value(&attitude, "Roll"),
Some(FieldValue::Number(_))
));
let battery = decode(127508, "00,e2,04,00,80,ff,ff,ff");
match value(&battery, "Current") {
Some(FieldValue::Number(a)) => assert!((a + 3276.8).abs() < 1e-9, "{a}"),
other => panic!("expected a current, got {other:?}"),
}
}
#[test]
fn mmsi_zero_is_not_available() {
let data: smallvec::SmallVec<[u8; 8]> = "18,8e,96,03,0e,24,4e,56,43,47,4a,57,37,32,4a,\
49,4b,39,40,40,78,00,28,00,28,00,3c,00,00,00,00,00,00,00,ff"
.split(',')
.map(|h| u8::from_str_radix(h.trim(), 16).unwrap())
.collect();
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 129810,
src: 43,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let value = |id: &str| {
dec.fields
.iter()
.find(|f| f.id() == id)
.map(|f| f.value.clone())
};
assert!(
matches!(value("mothershipUserId"), Some(FieldValue::NotAvailable)),
"{:?}",
value("mothershipUserId")
);
assert!(matches!(value("userId"), Some(FieldValue::Mmsi(_))));
}
#[test]
fn string_lau_ff_encoding_is_unset_field() {
let data = [0x05u8, 0xff, 0xff, 0xff, 0xff]; let (value, bits) = decode_string_lau(&data, 0, None);
assert!(matches!(value, FieldValue::NotAvailable), "{value:?}");
assert_eq!(bits, 40);
}
#[test]
fn string_lau_ascii_still_decodes() {
let data = [0x05u8, 0x01, b'H', b'i', 0xff];
let (value, bits) = decode_string_lau(&data, 0, None);
assert!(
matches!(value, FieldValue::String(ref s) if s == "Hi"),
"{value:?}"
);
assert_eq!(bits, 40);
}
#[test]
fn decodes_iso_address_claim() {
let data = smallvec::smallvec![0xfb, 0x9b, 0x70, 0x22, 0x00, 0x9b, 0x50, 0xc0];
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 60928,
src: 5,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
assert_eq!(dec.id, "isoAddressClaim");
assert_eq!(dec.fields.len(), 10);
match dec.fields[0].value {
FieldValue::Integer(v) => assert_eq!(v, 1_088_507),
ref other => panic!("expected integer, got {other:?}"),
}
match &dec.fields[1].value {
FieldValue::Lookup { value, name } => {
assert_eq!(*value, 275);
assert_eq!(name.as_deref(), Some("Navico"));
}
other => panic!("expected lookup, got {other:?}"),
}
match &dec.fields[4].value {
FieldValue::Lookup { value, .. } => assert_eq!(*value, 155),
other => panic!("expected lookup, got {other:?}"),
}
let wire_name = u64::from_le_bytes([0xfb, 0x9b, 0x70, 0x22, 0x00, 0x9b, 0x50, 0xc0]);
assert_eq!(dec.iso_name(), Some(wire_name));
}
#[test]
fn iso_name_keeps_fields_that_decode_as_not_available() {
let wire = [0xef, 0xfe, 0xe0, 0xe7, 0xff, 0x82, 0xdc, 0xcf];
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 60928,
src: 110,
dst: 255,
data: smallvec::SmallVec::from_slice(&wire),
};
let dec = db().decode(&frame).expect("decode");
assert_eq!(dec.iso_name(), Some(u64::from_le_bytes(wire)));
}
#[test]
fn iso_name_keeps_out_of_range_and_reserved_fields() {
for system_instance in [0x0e, 0x0d] {
let wire = [
0xef,
0xfe,
0xe0,
0xe7,
0xff,
0x82,
0xdc,
0xc0 | system_instance,
];
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 60928,
src: 110,
dst: 255,
data: smallvec::SmallVec::from_slice(&wire),
};
let dec = db().decode(&frame).expect("decode");
assert_eq!(dec.iso_name(), Some(u64::from_le_bytes(wire)));
}
}
#[test]
fn iso_name_is_none_for_non_claim_pgn() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 127251,
src: 5,
dst: 255,
data: smallvec::smallvec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
};
let dec = db().decode(&frame).expect("decode");
assert_eq!(dec.iso_name(), None);
}
#[test]
fn unknown_pgn_below_any_fallback_returns_error() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 1,
src: 1,
dst: 255,
data: smallvec::smallvec![0u8; 8],
};
assert!(matches!(
db().decode(&frame),
Err(DecodeError::UnknownPgn { pgn: 1 })
));
}
#[test]
fn string_fix_is_capped_to_the_message_not_dropped() {
let mut data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![0x13, 0x99, 0x00];
data.extend_from_slice(b"HELLO");
let frame = RawFrame {
timestamp: None,
prio: 7,
pgn: 130821,
src: 19,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let msg = dec
.fields
.iter()
.find(|f| f.info.name == "Message")
.expect("a Message field");
match &msg.value {
FieldValue::String(s) => assert_eq!(s, "HELLO"),
other => panic!("expected the truncated text, got {other:?}"),
}
}
#[test]
fn a_geographical_area_keeps_its_last_zero() {
let address = |format: u8, category: u8| {
let mut data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![format, category];
data.extend_from_slice(&[0x0f, 0x14, 0x04, 0x05, 0x0a, 0x27, 0xff, 0xff]);
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 129808,
src: 1,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
dec.fields
.iter()
.find(|f| f.info.name == "DSC Message Address")
.map(|f| f.value.clone())
.expect("a DSC Message Address")
};
for category in [108, 112] {
match address(102, category) {
FieldValue::Decimal(d) => assert_eq!(d, "1520040510"),
other => panic!("expected ten digits, got {other:?}"),
}
}
match address(120, 108) {
FieldValue::Decimal(d) => assert_eq!(d, "152004051"),
other => panic!("expected nine digits, got {other:?}"),
}
}
#[test]
fn a_pgn_field_is_the_pgn_as_sent() {
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 126208,
src: 29,
dst: 255,
data: smallvec::smallvec![0x00, 0x13, 0x00, 0x01, 0xff, 0x01, 0xff, 0xff],
};
let dec = db().decode(&frame).expect("decode");
let pgn = dec
.fields
.iter()
.find(|f| f.info.name == "PGN")
.expect("a PGN field");
match &pgn.value {
FieldValue::Pgn { value, .. } => assert_eq!(*value, 65555),
other => panic!("expected a PGN, got {other:?}"),
}
}
#[test]
fn a_variable_value_the_payload_ends_inside_is_left_out() {
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 126208,
src: 1,
dst: 2,
data: smallvec::smallvec![0x01, 0x02, 0xf8, 0x01, 0xf8, 0x01, 0x04, 0x10],
};
let dec = db().decode(&frame).expect("decode");
assert!(dec.fields.iter().any(|f| f.info.name == "Parameter"));
assert!(
dec.fields.iter().all(|f| f.info.name != "Value"),
"a cut-off value is left out"
);
}
#[test]
fn a_split_value_without_its_high_bits_is_left_out() {
let db = PgnDatabase::embedded_j1939(crate::engine::Units::Si);
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 65226,
src: 0,
dst: 255,
data: smallvec::smallvec![0x00, 0xff, 0x64, 0x00, 0x03, 0x01, 0xc8, 0x00],
};
let dec = db.decode(&frame).expect("decode");
let spns: Vec<_> = dec.fields.iter().filter(|f| f.info.name == "SPN").collect();
assert_eq!(spns.len(), 1, "only the whole DTC's SPN");
assert_eq!(spns[0].repeat_index, Some(0), "the first DTC's");
let frame = RawFrame {
data: smallvec::smallvec![0x00, 0xff, 0x64, 0x00],
..frame
};
let dec = db.decode(&frame).expect("decode");
assert!(dec.fields.iter().all(|f| f.info.name != "SPN"));
assert_eq!(dec.has_repeating_set, [false, false], "no list");
}
#[test]
fn decimal_is_digit_pairs_and_all_ones_is_absent() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x70, 0x70, 0x33, 0x14, 0x00, 0x5f, 0x1e, 0x6e, 0x64, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x12, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
];
let frame = RawFrame {
timestamp: None,
prio: 4,
pgn: 129808,
src: 3,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let addr = dec
.fields
.iter()
.find(|f| f.info.name == "DSC Message Address")
.expect("a DSC Message Address");
match &addr.value {
FieldValue::Decimal(d) => assert_eq!(d, "512000953"),
other => panic!("expected decimal digits, got {other:?}"),
}
for f in &dec.fields {
if f.info.name == "MMSI of Ship In Distress" {
assert!(
f.value.is_not_available(),
"an all-ones DECIMAL must be absent, got {:?}",
f.value
);
}
}
}
#[test]
fn string_padding_covers_all_of_isspace() {
assert_eq!(trim_string_padding("107473373\n"), "107473373");
assert_eq!(trim_string_padding("abc\r\n"), "abc");
assert_eq!(trim_string_padding("abc\t \u{0}@\u{ff}"), "abc");
assert_eq!(trim_string_padding("a b"), "a b");
}
#[test]
fn dynamic_field_signedness_comes_from_the_field_type() {
let entry = db()
.field_type_lookup("VICTRON_VREG", 8220)
.expect("VICTRON_VREG 8220");
assert!(entry.signed, "DC Current must be signed");
}
#[test]
fn dynamic_field_units_follow_the_database() {
use crate::engine::Units;
let entry = |units, key| {
*PgnDatabase::embedded(units)
.field_type_lookup("VICTRON_VREG", key)
.expect("VICTRON_VREG entry")
};
let energy = entry(Units::Si, 784);
assert_eq!(
(energy.unit, energy.resolution),
(Some("J"), Some(36_000.0))
);
let energy = entry(Units::Metric, 784);
assert_eq!((energy.unit, energy.resolution), (Some("kWh"), Some(0.01)));
let temp = entry(Units::Si, 60908);
assert_eq!((temp.unit, temp.unit_offset), (Some("K"), 0.0));
let temp = entry(Units::Metric, 60908);
assert_eq!((temp.unit, temp.unit_offset), (Some("C"), -273.15));
}
#[test]
fn zero_width_binary_is_sized_by_the_previous_field() {
let data: smallvec::SmallVec<[u8; 8]> =
smallvec::smallvec![0xa3, 0x99, 0x1e, 0x80, 0x00, 0x04, 0x64, 0x00, 0x00, 0x00];
let frame = RawFrame {
timestamp: None,
prio: 7,
pgn: 130820,
src: 10,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
if let Some(v) = dec.fields.iter().find(|f| f.info.name == "Values") {
match &v.value {
FieldValue::Binary(b) => assert_eq!(b.as_slice(), &[0x04]),
other => panic!("expected one masked byte, got {other:?}"),
}
}
}
#[test]
fn bitlookup_keeps_set_bits_the_enumeration_does_not_name() {
let data: smallvec::SmallVec<[u8; 8]> =
smallvec::smallvec![0x41, 0x9f, 0x00, 0x0a, 0x0a, 0x00, 0x80, 0x00];
let frame = RawFrame {
timestamp: None,
prio: 7,
pgn: 65305,
src: 13,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let mode = dec
.fields
.iter()
.find(|f| f.info.name == "Mode")
.expect("a Mode field");
match &mode.value {
FieldValue::BitField { bits, .. } => {
assert_eq!(bits.as_slice(), &[(2, None), (8, Some("Standby"))]);
}
other => panic!("expected a BitField, got {other:?}"),
}
let mut json = String::new();
crate::engine::output::write_json(
&mut json,
&dec,
&crate::engine::output::JsonOptions {
name_value: true,
..Default::default()
},
)
.unwrap();
assert!(
json.contains(r#""mode":[{"value":2,"name":null},{"value":8,"name":"Standby"}]"#),
"got: {json}"
);
}
#[test]
fn reserved_bytes_keep_their_bit_position() {
let data: smallvec::SmallVec<[u8; 8]> =
smallvec::smallvec![0x7d, 0x91, 0x64, 0x20, 0x2d, 0x00];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 130824,
src: 26,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let r = dec
.fields
.iter()
.find(|f| f.info.name == "Reserved")
.expect("a Reserved field");
match &r.value {
FieldValue::Reserved { bytes, .. } => assert_eq!(bytes.as_slice(), &[0x10]),
other => panic!("expected Reserved bytes, got {other:?}"),
}
}
#[test]
fn all_ones_mmsi_is_suppressed() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x41, 0x9f, 0x81, 0xff, 0x18, 0x88, 0x87, 0x94, 0x0e, 0x24, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x50, 0x49, 0x36, 0x34, 0x34, 0x36, 0x00, 0x6e, 0x00, 0x28, 0x00,
0x1e, 0x00, 0x3c, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
];
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 130842,
src: 8,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let mothership = dec
.fields
.iter()
.find(|f| f.info.name == "Mothership User ID")
.expect("a Mothership User ID field");
assert!(
mothership.value.is_sentinel(),
"an all-ones MMSI must read as a sentinel, got {:?}",
mothership.value
);
let user_id = dec
.fields
.iter()
.find(|f| f.info.name == "User ID")
.expect("a User ID field");
assert!(matches!(user_id.value, FieldValue::Mmsi(244_615_048)));
}
#[test]
fn truncated_trailing_reserved_is_omitted() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x12, 0x10, 0xad, 0x8c, 0x0e, 0xb2, 0x33, 0x2c, 0x03, 0xe6, 0x96, 0x6d, 0x1f, 0x3e,
0xb8, 0x65, 0x71, 0x00, 0xc0, 0x00, 0xc1, 0xff, 0x7f, 0x00, 0xe2, 0xff,
];
let frame = RawFrame {
timestamp: None,
prio: 4,
pgn: 129039,
src: 8,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let mut json = String::new();
crate::engine::output::write_json(
&mut json,
&dec,
&crate::engine::output::JsonOptions::default(),
)
.unwrap();
assert!(
!json.to_ascii_lowercase().contains("reserved"),
"got: {json}"
);
}
#[test]
fn unresolved_key_takes_the_rest_of_the_frame_as_its_value() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x41, 0x9f, 0xff, 0xff, 0x01, 0xff, 0x07, 0x45, 0x00, 0x01, 0x2d, 0x7d, 0x10, 0x14,
];
let frame = RawFrame {
timestamp: None,
prio: 2,
pgn: 130845,
src: 13,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let value = dec
.fields
.iter()
.find(|f| f.info.name == "Value")
.expect("a Value field");
match &value.value {
FieldValue::Binary(b) => assert_eq!(b.as_slice(), &[0x2d, 0x7d, 0x10, 0x14]),
other => panic!("expected the trailing bytes, got {other:?}"),
}
}
#[test]
fn all_ones_dynamic_value_is_not_available() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x7d, 0x99, 0x64, 0x20, 0x2d, 0x00, 0x0d, 0x21, 0x43, 0x00, 0x0e, 0x41, 0xc0, 0xa1,
0xb2, 0x1f, 0x0f, 0x41, 0xce, 0x50, 0x3c, 0x03, 0x69, 0x20, 0x43, 0x7d, 0xd3, 0x20,
0x2a, 0x74, 0x81, 0x40, 0x70, 0xd3, 0x02, 0x00, 0x9a, 0x20, 0x35, 0xf0, 0x82, 0x20,
0x00, 0x00, 0x0a, 0x21, 0x98, 0x02, 0x0c, 0x21, 0xb8, 0x02, 0x75, 0x40, 0x20, 0x6c,
0xfb, 0xff, 0x09, 0x41, 0xff, 0xff, 0xff, 0xff, 0x0b, 0x41, 0xff, 0xff, 0xff, 0xff,
0xd0, 0x40, 0xe0, 0xaa, 0x8c, 0x0e, 0x7f, 0x20, 0x00, 0x00, 0x9d, 0x20, 0x54, 0xad,
];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 130824,
src: 26,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let mut expecting_value_for: Option<u64> = None;
let mut seen = 0;
for f in &dec.fields {
match (f.info.name, &f.value) {
("Key", FieldValue::Lookup { value, .. }) => {
assert!(
expecting_value_for.is_none(),
"key {} got no Value at all",
expecting_value_for.unwrap()
);
if matches!(value, 265 | 267) {
expecting_value_for = Some(*value);
}
}
("Value", v) => {
if let Some(key) = expecting_value_for.take() {
match v {
FieldValue::NotAvailable => seen += 1,
other => panic!("key {key}: expected not available, got {other:?}"),
}
}
}
_ => {}
}
}
assert!(expecting_value_for.is_none(), "trailing key had no Value");
assert_eq!(seen, 2, "expected both trip-time records");
}
#[test]
fn dynamic_length_overhead_sizes_130823_records() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x13, 0x99, 0xff, 0x00, 0x05, 0x01, 0x00, 0x0a, 0x00, 0x55, 0x02, 0xbc, 0x2f, 0x00,
0x96, 0x50, 0xc0, 0xff, 0x04, 0x02, 0x26, 0x00, 0x00, 0x04, 0x02, 0x27, 0x00, 0x00,
0x04, 0x02, 0x7d, 0x00, 0x00, 0x04, 0x02, 0xa5, 0x00, 0x00, 0x04, 0x02, 0x99, 0x01,
0x00, 0x04, 0x02, 0xf9, 0x01, 0x00,
];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 130823,
src: 19,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let mut pending: Option<i64> = None;
let mut records = 0;
for f in &dec.fields {
match f.info.name {
"Length" => {
if let FieldValue::Integer(n) = f.value {
if let Some(prev) = pending.take() {
assert_eq!(prev, 3, "record with no Value declared Length {prev}");
}
pending = Some(n);
records += 1;
}
}
"Value" => {
let len = pending.take().expect("Value without a preceding Length");
let bytes = match &f.value {
FieldValue::Binary(b) => b.len() as i64,
other => panic!("expected binary Value, got {other:?}"),
};
assert_eq!(bytes, len - 3, "Length {len} should give {} bytes", len - 3);
}
_ => {}
}
}
if let Some(last) = pending {
assert_eq!(
last, 3,
"trailing record with no Value declared Length {last}"
);
}
assert!(records >= 6, "expected several records, got {records}");
}
#[test]
fn parameters_resolve_a_proprietary_variant() {
let params = [4u8, 0x01, 0x3f, 0x07, 0x03, 0x04, 0x04];
let picked = match_pgn_by_parameters(db(), 130833, ¶ms).expect("a variant");
assert_eq!(picked.id, "furunoShipParametersAndAntennaPosition");
}
#[test]
fn parameters_naming_no_126720_variant_fall_back_to_the_catch_all() {
let params = [4u8, 0x01, 0x3b, 0x07, 0x03, 0x04, 0x04, 0x5c, 0x05, 0x0f];
assert!(match_pgn_by_parameters(db(), 126720, ¶ms).is_none());
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
0x00, 0x00, 0xef, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x04, 0x01, 0x3b, 0x07,
0x03, 0x04, 0x04, 0x5c, 0x05, 0x0f,
];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 126208,
src: 204,
dst: 172,
data,
};
let dec = db().decode(&frame).expect("decode");
let value = dec
.fields
.iter()
.rfind(|f| f.repeat_set == 1)
.expect("parameter 4's value");
match &value.value {
FieldValue::Binary(b) => assert_eq!(b.as_slice(), &[0x5c, 0x05, 0x0f]),
other => panic!("expected the catch-all's raw Data, got {other:?}"),
}
}
#[test]
fn parameters_resolve_a_126720_variant_behind_the_catch_all() {
let params = [3u8, 0x01, 0x87, 0x00, 0x03, 0x04, 0x04, 0x23];
let picked = match_pgn_by_parameters(db(), 126720, ¶ms).expect("a variant");
assert_eq!(picked.id, "airmarSimulateMode");
}
#[test]
fn proprietary_target_without_manufacturer_does_not_resolve() {
let params = [1u8, 0x04, 0x00];
assert!(match_pgn_by_parameters(db(), 126720, ¶ms).is_none());
}
#[test]
fn match_variant_beats_fallback_listed_first() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![
3, 0, 0, 0, 0, 0, 0, 0,
];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 126208,
src: 0,
dst: 0,
data,
};
let picked = db().pick_variant(&frame).expect("variant");
assert_eq!(picked.id, "nmeaReadFieldsGroupFunction");
}
#[test]
fn fallback_when_no_specific_variant_matches() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![99, 0, 0, 0, 0, 0, 0, 0];
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 126208,
src: 0,
dst: 0,
data,
};
let picked = db().pick_variant(&frame).expect("variant");
assert!(
picked.fallback.unwrap_or(false),
"expected Fallback variant, got id={}",
picked.id
);
}
#[test]
fn pgn_59392_picks_iso_acknowledgement_over_range_catchall() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 59392,
src: 17,
dst: 200,
data: smallvec::smallvec![0x01, 0xff, 0xff, 0xff, 0xff, 0x16, 0xf0, 0x01],
};
let picked = db().pick_variant(&frame).expect("variant");
assert_eq!(picked.id, "isoAcknowledgement", "got id={}", picked.id);
assert_eq!(picked.fallback, None);
}
#[test]
fn j1939_flavor_resolves_ecu1_where_main_falls_back() {
let frame = RawFrame {
timestamp: None,
prio: 3,
pgn: 61444,
src: 0,
dst: 255,
data: smallvec::smallvec![0xff, 0xff, 0xff, 0xe0, 0x2e, 0xff, 0xff, 0xff],
};
let j1939 = crate::engine::db::PgnDatabase::embedded_j1939(crate::engine::db::Units::Si);
let picked = j1939.pick_variant(&frame).expect("j1939 variant");
assert_eq!(picked.id, "ecu1", "got id={}", picked.id);
let main = db().pick_variant(&frame).expect("main variant");
assert!(
main.fallback.unwrap_or(false),
"main db must fall back for 61444, got id={}",
main.id
);
}
#[test]
fn j1939_flavor_keeps_shared_iso_pgns() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 60928,
src: 0,
dst: 255,
data: smallvec::smallvec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
};
let j1939 = crate::engine::db::PgnDatabase::embedded_j1939(crate::engine::db::Units::Si);
let picked = j1939.pick_variant(&frame).expect("j1939 variant");
assert_eq!(picked.id, "isoAddressClaim", "got id={}", picked.id);
}
#[test]
fn j1939_flavor_does_not_know_nmea2000_pgns() {
let frame = RawFrame {
timestamp: None,
prio: 2,
pgn: 129025, src: 0,
dst: 255,
data: smallvec::smallvec![0x00; 8],
};
let j1939 = crate::engine::db::PgnDatabase::embedded_j1939(crate::engine::db::Units::Si);
let picked = j1939.pick_variant(&frame);
assert!(
picked.is_none_or(|p| p.fallback.unwrap_or(false)),
"129025 must not resolve specifically in the J1939 flavor, got {:?}",
picked.map(|p| p.id)
);
}
#[test]
fn cross_pgn_catchall_for_unknown_proprietary() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 65500,
src: 0,
dst: 255,
data: smallvec::smallvec![0u8; 8],
};
let picked = db().pick_variant(&frame).expect("catchall");
assert!(picked.fallback.unwrap_or(false));
assert_eq!(picked.pgn, 65280);
}
#[test]
fn not_available_sentinel_on_all_ones() {
let data: smallvec::SmallVec<[u8; 8]> = smallvec::smallvec![0xff; 8];
let frame = RawFrame {
timestamp: None,
prio: 2,
pgn: 127245,
src: 0,
dst: 255,
data,
};
let dec = db().decode(&frame).expect("decode");
let instance = &dec.fields[0];
assert!(matches!(instance.value, FieldValue::NotAvailable));
}
#[test]
fn group_function_header_is_read_only_for_a_proprietary_pgn() {
let frame = RawFrame::new(
None,
3,
126208,
7,
80,
[0x05, 0x1d, 0xff, 0x01, 0x3f, 0x9f, 0x00, 0x00, 0x00],
);
let dec = db().decode(&frame).expect("decode");
let value = |name: &str| dec.field_by_name(name).map(|f| f.value.clone());
assert!(matches!(
value("Manufacturer Code"),
Some(FieldValue::Lookup { value: 1855, .. })
));
assert!(matches!(
value("Industry Code"),
Some(FieldValue::Lookup { value: 4, .. })
));
assert!(matches!(value("Unique ID"), Some(FieldValue::Integer(0))));
assert!(matches!(
value("Number of Selection Pairs"),
Some(FieldValue::Integer(0))
));
let frame = RawFrame::new(
None,
3,
126208,
7,
80,
[
0x05, 0x14, 0xf2, 0x01, 0x00, 0x01, 0x01, 0x01, 0x66, 0x01, 0x05,
],
);
let dec = db().decode(&frame).expect("decode");
assert!(dec.field_by_name("Manufacturer Code").is_none());
assert!(matches!(
dec.field_by_name("Unique ID").map(|f| &f.value),
Some(FieldValue::Integer(0))
));
assert!(matches!(
dec.field_by_name("Number of Selection Pairs")
.map(|f| &f.value),
Some(FieldValue::Integer(1))
));
}
}