use std::error::Error;
use std::fmt;
use crate::engine::decode::field_present;
use crate::engine::types::{FieldInfo, FieldRef, FieldType, PgnInfo};
use crate::engine::db::PgnDatabase;
use crate::engine::frame::RawFrame;
#[derive(Debug, Clone, PartialEq)]
pub enum EncodeValue {
Number(f64),
Int(i64),
Raw(u64),
Lookup(String),
Text(String),
Bytes(Vec<u8>),
Pgn(u32),
NotAvailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Raw(pub u64);
impl From<Raw> for EncodeValue {
fn from(v: Raw) -> Self {
EncodeValue::Raw(v.0)
}
}
impl From<f64> for EncodeValue {
fn from(v: f64) -> Self {
EncodeValue::Number(v)
}
}
impl From<i64> for EncodeValue {
fn from(v: i64) -> Self {
EncodeValue::Int(v)
}
}
impl From<i32> for EncodeValue {
fn from(v: i32) -> Self {
EncodeValue::Int(v as i64)
}
}
impl From<u32> for EncodeValue {
fn from(v: u32) -> Self {
EncodeValue::Int(v as i64)
}
}
impl From<&str> for EncodeValue {
fn from(v: &str) -> Self {
EncodeValue::Text(v.to_string())
}
}
impl From<String> for EncodeValue {
fn from(v: String) -> Self {
EncodeValue::Text(v)
}
}
impl From<Vec<u8>> for EncodeValue {
fn from(v: Vec<u8>) -> Self {
EncodeValue::Bytes(v)
}
}
impl From<&[u8]> for EncodeValue {
fn from(v: &[u8]) -> Self {
EncodeValue::Bytes(v.to_vec())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum EncodeError {
NoSuchPgnId(String),
NoSuchPgn(u32),
AmbiguousPgn { pgn: u32, variants: usize },
NoSuchField { pgn_id: &'static str, field: String },
UnknownLookupLabel { field: &'static str, label: String },
ValueOutOfRange {
field: &'static str,
value: f64,
range: Option<(f64, f64)>,
},
NotFixedLength(&'static str),
TypeMismatch {
field: &'static str,
expected: &'static str,
},
LengthMismatch { pgn: u32, expected: u32, got: usize },
VariableUnresolved {
field: &'static str,
reason: &'static str,
},
NoRepeatingSet { pgn_id: &'static str, set: usize },
NoSuchInstance {
pgn_id: &'static str,
set: usize,
instance: usize,
},
ProprietaryHeader {
field: &'static str,
target_pgn: Option<u32>,
proprietary: bool,
},
}
impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncodeError::NoSuchPgnId(id) => write!(f, "no PGN with id '{id}'"),
EncodeError::NoSuchPgn(p) => write!(f, "no PGN {p} in the schema"),
EncodeError::AmbiguousPgn { pgn, variants } => {
write!(f, "PGN {pgn} has {variants} variants; select one by id")
}
EncodeError::NoSuchField { pgn_id, field } => {
write!(f, "PGN '{pgn_id}' has no field '{field}'")
}
EncodeError::UnknownLookupLabel { field, label } => {
write!(f, "field '{field}': '{label}' is not a valid value")
}
EncodeError::ValueOutOfRange {
field,
value,
range: Some((min, max)),
} => {
write!(
f,
"field '{field}': value {value} is outside its range {min} to {max}"
)
}
EncodeError::ValueOutOfRange {
field,
value,
range: None,
} => {
write!(f, "field '{field}': value {value} out of range")
}
EncodeError::NotFixedLength(field) => {
write!(
f,
"field '{field}': dynamic group-function field must be set explicitly"
)
}
EncodeError::VariableUnresolved { field, reason } => {
write!(f, "field '{field}': {reason}")
}
EncodeError::NoRepeatingSet { pgn_id, set } => {
write!(f, "PGN '{pgn_id}' has no repeating field set {set}")
}
EncodeError::NoSuchInstance {
pgn_id,
set,
instance,
} => {
write!(
f,
"PGN '{pgn_id}' set {set}: instance {instance} was never added"
)
}
EncodeError::TypeMismatch { field, expected } => {
write!(f, "field '{field}': expected {expected}")
}
EncodeError::ProprietaryHeader {
field,
target_pgn: Some(pgn),
proprietary: true,
} => {
write!(f, "field '{field}' must be set: PGN {pgn} is proprietary")
}
EncodeError::ProprietaryHeader {
field,
target_pgn: Some(pgn),
proprietary: false,
} => {
write!(
f,
"field '{field}' is only sent for a proprietary PGN, and {pgn} is not"
)
}
EncodeError::ProprietaryHeader {
field,
target_pgn: None,
..
} => {
write!(
f,
"field '{field}' is only sent for a proprietary PGN, and no PGN is set"
)
}
EncodeError::LengthMismatch { pgn, expected, got } => {
write!(f, "PGN {pgn}: packed {got} bytes, schema says {expected}")
}
}
}
}
impl Error for EncodeError {}
pub struct PgnBuilder {
db: &'static PgnDatabase,
pgn: &'static PgnInfo,
prio: u8,
src: u8,
dst: u8,
timestamp: Option<String>,
staged: Vec<Option<Staged>>,
sets: [Vec<Vec<Option<Staged>>>; 2],
}
#[derive(Clone)]
enum Staged {
Scalar(u64),
Bytes(Vec<u8>),
Deferred(EncodeValue),
}
impl PgnBuilder {
pub(crate) fn for_pgn(db: &'static PgnDatabase, pgn: &'static PgnInfo) -> Self {
Self {
db,
pgn,
prio: if db.protocol().standard_frames() {
0
} else {
pgn.priority.unwrap_or(6)
},
src: 0,
dst: 255,
timestamp: None,
staged: vec![None; pgn.fields.len()],
sets: [Vec::new(), Vec::new()],
}
}
pub fn priority(mut self, prio: u8) -> Self {
self.prio = prio;
self
}
pub fn source(mut self, src: u8) -> Self {
self.src = src;
self
}
pub fn destination(mut self, dst: u8) -> Self {
self.dst = dst;
self
}
pub fn timestamp(mut self, ts: impl Into<String>) -> Self {
self.timestamp = Some(ts.into());
self
}
pub fn push(
&mut self,
field: FieldRef,
value: impl Into<EncodeValue>,
) -> Result<&mut Self, EncodeError> {
debug_assert_eq!(
field.pgn.id, self.pgn.id,
"FieldRef/PGN mismatch: a `{}` field ref was used on a `{}` builder",
field.pgn.id, self.pgn.id,
);
let idx = (field.field.order as usize)
.checked_sub(1)
.filter(|&i| i < self.pgn.fields.len())
.ok_or(EncodeError::NoSuchField {
pgn_id: self.pgn.id,
field: field.field.id.to_string(),
})?;
let staged = self.stage_value(&self.pgn.fields[idx], value.into())?;
self.staged[idx] = Some(staged);
Ok(self)
}
pub fn push_by_name(
&mut self,
field: &str,
value: impl Into<EncodeValue>,
) -> Result<&mut Self, EncodeError> {
let idx = self.find_by_name(field)?;
let staged = self.stage_value(&self.pgn.fields[idx], value.into())?;
self.staged[idx] = Some(staged);
Ok(self)
}
pub fn push_arg(&mut self, field: &str, value: &str) -> Result<&mut Self, EncodeError> {
let idx = self.find_by_name_or_id(field)?;
let f = &self.pgn.fields[idx];
let ev = coerce_arg(f, value)?;
let staged = self.stage_value(f, ev)?;
self.staged[idx] = Some(staged);
Ok(self)
}
fn set_layout(&self, set: usize) -> Option<(usize, usize, Option<usize>)> {
let (start, size, count) = match set {
1 => (
self.pgn.repeating_field_set1_start_field,
self.pgn.repeating_field_set1_size,
self.pgn.repeating_field_set1_count_field,
),
2 => (
self.pgn.repeating_field_set2_start_field,
self.pgn.repeating_field_set2_size,
self.pgn.repeating_field_set2_count_field,
),
_ => (None, None, None),
};
Some((
start? as usize - 1,
size? as usize,
count.map(|c| c as usize - 1),
))
}
pub fn add_set_instance(&mut self, set: usize) -> Result<usize, EncodeError> {
let (_, size, _) = self.set_layout(set).ok_or(EncodeError::NoRepeatingSet {
pgn_id: self.pgn.id,
set,
})?;
let instances = &mut self.sets[set - 1];
instances.push(vec![None; size]);
Ok(instances.len() - 1)
}
pub fn push_in_set(
&mut self,
set: usize,
instance: usize,
field: &str,
value: impl Into<EncodeValue>,
) -> Result<&mut Self, EncodeError> {
let (start, size, _) = self.set_layout(set).ok_or(EncodeError::NoRepeatingSet {
pgn_id: self.pgn.id,
set,
})?;
let rel = self.pgn.fields[start..start + size]
.iter()
.position(|f| f.name == field || f.id == field)
.ok_or_else(|| EncodeError::NoSuchField {
pgn_id: self.pgn.id,
field: field.to_string(),
})?;
let f = &self.pgn.fields[start + rel];
let staged = if f.field_type == Some(FieldType::Variable) {
Staged::Deferred(value.into())
} else {
self.stage_value(f, value.into())?
};
let inst = self.sets[set - 1]
.get_mut(instance)
.ok_or(EncodeError::NoSuchInstance {
pgn_id: self.pgn.id,
set,
instance,
})?;
inst[rel] = Some(staged);
Ok(self)
}
pub fn build(&self) -> Result<RawFrame, EncodeError> {
let mut buf: Vec<u8> = Vec::new();
let mut next_bit = 0usize;
let set1 = self.set_layout(1);
let set2 = self.set_layout(2);
let mut idx = 0usize;
while idx < self.pgn.fields.len() {
let mut in_set = false;
for (layout, instances) in [(&set1, &self.sets[0]), (&set2, &self.sets[1])] {
if let Some((start, size, _)) = *layout
&& idx == start
{
for inst in instances {
for (j, f) in self.pgn.fields[start..start + size].iter().enumerate() {
match &inst[j] {
Some(Staged::Deferred(v)) => self.emit_variable(
&mut buf,
&mut next_bit,
f,
inst,
start,
size,
v,
)?,
None => {
let fields = &self.pgn.fields[start..start + size];
let derived = continuing_staged(fields, inst, j);
emit_field(
&mut buf,
&mut next_bit,
self.pgn.pgn,
f,
derived.as_ref(),
)?
}
other => emit_field(
&mut buf,
&mut next_bit,
self.pgn.pgn,
f,
other.as_ref(),
)?,
}
}
}
idx += size;
in_set = true;
break;
}
}
if in_set {
continue;
}
let f = &self.pgn.fields[idx];
if f.condition.is_some() {
let target_pgn = self.staged_target_pgn();
let staged = self.staged[idx].is_some();
if !field_present(f.condition, target_pgn) {
if staged {
return Err(EncodeError::ProprietaryHeader {
field: f.name,
target_pgn,
proprietary: false,
});
}
idx += 1;
continue;
}
if !staged && f.field_type != Some(FieldType::Reserved) {
return Err(EncodeError::ProprietaryHeader {
field: f.name,
target_pgn,
proprietary: true,
});
}
}
let auto_count = [(&set1, &self.sets[0]), (&set2, &self.sets[1])]
.into_iter()
.find_map(|(layout, instances)| match *layout {
Some((_, _, Some(count_idx)))
if count_idx == idx && self.staged[idx].is_none() =>
{
Some(Staged::Scalar(instances.len() as u64))
}
_ => None,
});
let derived = match self.staged[idx] {
None => continuing_staged(self.pgn.fields, &self.staged, idx),
Some(_) => None,
};
emit_field(
&mut buf,
&mut next_bit,
self.pgn.pgn,
f,
auto_count
.as_ref()
.or(self.staged[idx].as_ref())
.or(derived.as_ref()),
)?;
idx += 1;
}
if !next_bit.is_multiple_of(8) {
let pad = 8 - (next_bit % 8);
write_bits(&mut buf, &mut next_bit, pad, u64::MAX);
}
if let Some(expected) = self.pgn.length
&& buf.len() != expected as usize
{
return Err(EncodeError::LengthMismatch {
pgn: self.pgn.pgn,
expected,
got: buf.len(),
});
}
Ok(RawFrame {
timestamp: self.timestamp.clone(),
prio: self.prio,
pgn: self.pgn.pgn,
src: self.src,
dst: self.dst,
data: buf.into_iter().collect(),
})
}
pub fn pgn_info(&self) -> &'static PgnInfo {
self.pgn
}
fn find_by_name(&self, name: &str) -> Result<usize, EncodeError> {
self.pgn
.fields
.iter()
.position(|f| f.name == name)
.ok_or_else(|| EncodeError::NoSuchField {
pgn_id: self.pgn.id,
field: name.to_string(),
})
}
fn find_by_name_or_id(&self, key: &str) -> Result<usize, EncodeError> {
self.pgn
.fields
.iter()
.position(|f| f.name == key || f.id == key)
.ok_or_else(|| EncodeError::NoSuchField {
pgn_id: self.pgn.id,
field: key.to_string(),
})
}
fn staged_parameter_pairs(&self) -> Vec<(u32, &EncodeValue)> {
let mut pairs = Vec::new();
for set in 1..=2 {
let Some((start, size, _)) = self.set_layout(set) else {
continue;
};
for inst in &self.sets[set - 1] {
let mut index = None;
let mut value = None;
for (j, tf) in self.pgn.fields[start..start + size].iter().enumerate() {
match &inst[j] {
Some(Staged::Scalar(n)) if tf.field_type == Some(FieldType::FieldIndex) => {
index = Some(*n as u32)
}
Some(Staged::Deferred(v)) => value = Some(v),
_ => {}
}
}
if let (Some(i), Some(v)) = (index, value) {
pairs.push((i, v));
}
}
}
pairs
}
fn staged_target_pgn(&self) -> Option<u32> {
self.pgn
.fields
.iter()
.position(|tf| tf.field_type == Some(FieldType::Pgn))
.and_then(|i| match self.staged[i] {
Some(Staged::Scalar(p)) => Some(p as u32),
_ => None,
})
}
fn match_target_variant(&self, target_pgn: u32) -> Option<&'static PgnInfo> {
let first = self.db.specific_variants(target_pgn).next()?;
if !first.fields.iter().any(|fi| fi.match_value.is_some()) {
return Some(first);
}
let pairs = self.staged_parameter_pairs();
if crate::engine::decode::is_pgn_proprietary(target_pgn)
&& (pairs.len() < 2 || pairs[0].0 != 1 || pairs[1].0 != 3)
{
return None;
}
'variant: for cand in self.db.specific_variants(target_pgn) {
for &(idx, value) in &pairs {
if idx == 0 {
continue 'variant;
}
let Some(field) = cand.fields.get(idx as usize - 1) else {
continue 'variant;
};
let Some(want) = field.match_value else {
continue;
};
let staged_value = match value {
EncodeValue::Text(s) if field.lookup_enumeration.is_some() => {
EncodeValue::Lookup(s.clone())
}
other => other.clone(),
};
match self.stage_value(field, staged_value) {
Ok(Staged::Scalar(raw)) if raw as i64 == want => {}
_ => continue 'variant,
}
}
return Some(cand);
}
None
}
#[allow(clippy::too_many_arguments)]
fn emit_variable(
&self,
buf: &mut Vec<u8>,
next_bit: &mut usize,
f: &FieldInfo,
inst: &[Option<Staged>],
start: usize,
size: usize,
v: &EncodeValue,
) -> Result<(), EncodeError> {
let unresolved = |reason| EncodeError::VariableUnresolved {
field: f.name,
reason,
};
let target_pgn = self
.staged_target_pgn()
.ok_or(unresolved("no staged PGN field names the referenced PGN"))?;
let target_info = self
.match_target_variant(target_pgn)
.or_else(|| self.db.first_pgn(target_pgn))
.or_else(|| self.db.fallback_pgn(target_pgn))
.ok_or(unresolved("referenced PGN is not in the schema"))?;
let param_idx = self.pgn.fields[start..start + size]
.iter()
.enumerate()
.find(|(_, tf)| tf.field_type == Some(FieldType::FieldIndex))
.and_then(|(j, _)| match inst[j] {
Some(Staged::Scalar(n)) => Some(n as u32),
_ => None,
})
.ok_or(unresolved("no staged FIELD_INDEX selects the target field"))?;
let target_field = target_info
.fields
.iter()
.find(|tf| (tf.order as u32) == param_idx)
.ok_or(unresolved(
"FIELD_INDEX is beyond the referenced PGN's fields",
))?;
let is_time = matches!(
target_field.field_type,
Some(FieldType::Time) | Some(FieldType::Duration)
);
let value = match v {
EncodeValue::Text(s) if target_field.lookup_enumeration.is_some() => {
EncodeValue::Lookup(s.clone())
}
EncodeValue::Text(s) if is_time => match crate::engine::output::parse_time(s) {
Some(seconds) => EncodeValue::Number(seconds),
None => v.clone(),
},
other => other.clone(),
};
match self.stage_value(target_field, value)? {
Staged::Scalar(raw) => {
let bl = target_field
.bit_length
.ok_or(EncodeError::NotFixedLength(target_field.name))?
as usize;
write_bits(buf, next_bit, bl, raw);
if !next_bit.is_multiple_of(8) {
let pad = 8 - (*next_bit % 8);
write_bits(buf, next_bit, pad, 0);
}
}
Staged::Bytes(bytes) => {
for b in bytes {
write_bits(buf, next_bit, 8, u64::from(b));
}
}
Staged::Deferred(_) => unreachable!("stage_value never defers"),
}
Ok(())
}
fn stage_value(&self, f: &FieldInfo, v: EncodeValue) -> Result<Staged, EncodeError> {
match f.field_type {
Some(FieldType::StringFix) => stage_string_fix(f, v, string_fix_pad(self.pgn.pgn)),
Some(FieldType::StringLz) => stage_string_lz(f, v),
Some(FieldType::StringLau) => stage_string_lau(f, v),
Some(FieldType::Binary) => stage_binary(f, v),
Some(FieldType::DynamicFieldValue) => stage_dynamic_value(f, v),
_ => Ok(Staged::Scalar(self.value_to_raw(f, v)?)),
}
}
fn value_to_raw(&self, f: &FieldInfo, v: EncodeValue) -> Result<u64, EncodeError> {
let bl = f.bit_length.ok_or(EncodeError::NotFixedLength(f.name))?;
let width = bl + f.continuation.map_or(0, |c| c.bit_length);
let v = match (f.field_type, v) {
(Some(FieldType::Float), EncodeValue::Int(n)) => EncodeValue::Number(n as f64),
(_, v) => v,
};
let raw_i64: i64 = match (f.field_type, v) {
(_, EncodeValue::NotAvailable) => return Ok(default_raw(f)),
(_, EncodeValue::Raw(raw)) => return Ok(raw & all_ones(width)),
(Some(FieldType::Decimal), v) => return decimal_to_raw(f, width, v),
(
Some(FieldType::Lookup) | Some(FieldType::IndirectLookup),
EncodeValue::Lookup(label),
) => self.resolve_lookup(f, &label)? as i64,
(Some(FieldType::Lookup) | Some(FieldType::IndirectLookup), EncodeValue::Int(n)) => n,
(Some(FieldType::BitLookup), EncodeValue::Int(n)) => n,
(Some(FieldType::Pgn), EncodeValue::Pgn(p)) => p as i64,
(Some(FieldType::Pgn), EncodeValue::Int(n)) => n,
(Some(FieldType::Float), EncodeValue::Number(x)) => {
let wire = match f.resolution {
Some(r) if r != 0.0 => (x - f.unit_offset) / r,
_ => x,
};
return Ok(u64::from((wire as f32).to_bits()));
}
(_, EncodeValue::Text(_)) | (_, EncodeValue::Bytes(_)) => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "a numeric value",
});
}
(_, EncodeValue::Pgn(p)) => p as i64,
(Some(t), EncodeValue::Int(n)) if is_scaled(t) => int_to_raw(f, n)?,
(_, EncodeValue::Number(x)) => scaled_to_raw(f, x)?,
(_, EncodeValue::Int(n)) => n,
(_, EncodeValue::Lookup(_)) => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "a numeric value",
});
}
};
Ok(mask_to_width(raw_i64, width))
}
fn resolve_lookup(&self, f: &FieldInfo, label: &str) -> Result<u64, EncodeError> {
let table_name = f.lookup_enumeration.ok_or(EncodeError::TypeMismatch {
field: f.name,
expected: "a value for a lookup field",
})?;
if let Some(table) = self.db.lookup(table_name) {
for lv in table.values {
if lv.name == label || lv.id == Some(label) {
return Ok(lv.value);
}
}
}
Err(EncodeError::UnknownLookupLabel {
field: f.name,
label: label.to_string(),
})
}
}
fn is_scaled(t: FieldType) -> bool {
matches!(
t,
FieldType::Number
| FieldType::Decimal
| FieldType::Float
| FieldType::Date
| FieldType::Time
| FieldType::Duration
)
}
fn int_to_raw(f: &FieldInfo, n: i64) -> Result<i64, EncodeError> {
let unscaled =
f.resolution.unwrap_or(1.0) == 1.0 && f.offset.unwrap_or(0) == 0 && f.unit_offset == 0.0;
if !unscaled {
return scaled_to_raw(f, n as f64);
}
if let (Some(min), Some(max)) = (f.range_min, f.range_max)
&& ((n as f64) < min || (n as f64) > max)
{
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: n as f64,
range: Some((min, max)),
});
}
Ok(n)
}
fn decimal_to_raw(f: &FieldInfo, width: u32, v: EncodeValue) -> Result<u64, EncodeError> {
let integer = !matches!(v, EncodeValue::Text(_));
let digits = match v {
EncodeValue::Text(s) => s,
EncodeValue::Int(n) if n >= 0 => n.to_string(),
EncodeValue::Number(x) if x >= 0.0 && x.fract() == 0.0 && x < 1e19 => {
(x as u64).to_string()
}
EncodeValue::Int(_) | EncodeValue::Number(_) | EncodeValue::Pgn(_) => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "a positive whole number",
});
}
_ => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "decimal digits",
});
}
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "decimal digits",
});
}
let nbytes = width / 8;
let ndigits = 2 * nbytes as usize;
let digits = if integer && digits.len() < ndigits {
format!("{digits:0>w$}", w = ndigits - 1)
} else {
digits
};
if digits.len() > ndigits {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: digits.parse::<f64>().unwrap_or(f64::NAN),
range: f.range_min.zip(f.range_max),
});
}
let mut raw = 0u64;
let mut pairs = digits.as_bytes().chunks(2);
for i in 0..nbytes {
let pair = match pairs.next() {
Some([hi, lo]) => (hi - b'0') * 10 + (lo - b'0'),
Some([hi]) => (hi - b'0') * 10,
_ => 0,
};
raw |= u64::from(pair) << (8 * i);
}
Ok(raw)
}
fn scaled_to_raw(f: &FieldInfo, scaled: f64) -> Result<i64, EncodeError> {
let scaled = full_turn_angle(f, scaled);
let resolution = f.resolution.unwrap_or(1.0);
let display_offset = f.offset.map(|o| o as f64).unwrap_or(0.0);
let raw = (scaled - f.unit_offset - display_offset) / resolution;
let rounded = raw.round();
if !rounded.is_finite() {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: scaled,
range: None,
});
}
if let (Some(min), Some(max)) = (f.range_min, f.range_max) {
let raw_of = |v: f64| ((v - f.unit_offset - display_offset) / resolution).round();
if rounded < raw_of(min) || rounded > raw_of(max) {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: scaled,
range: Some((min, max)),
});
}
}
Ok(rounded as i64)
}
fn full_turn_angle(f: &FieldInfo, value: f64) -> f64 {
if f.physical_quantity != Some("ANGLE") || f.range_min != Some(0.0) || value >= 0.0 {
return value;
}
let full = match f.unit {
Some("rad") => std::f64::consts::TAU,
Some("deg") => 360.0,
_ => return value,
};
match f.range_max {
Some(max) if max >= full * 0.999 && value >= -full / 2.0 => value + full,
_ => value,
}
}
const AIS_TEXT_PGNS: [u32; 4] = [129040, 129794, 129809, 129810];
fn string_fix_pad(pgn: u32) -> u8 {
if AIS_TEXT_PGNS.contains(&pgn) {
b'@'
} else {
0xff
}
}
fn stage_string_fix(f: &FieldInfo, v: EncodeValue, pad: u8) -> Result<Staged, EncodeError> {
let byte_len = f.bit_length.ok_or(EncodeError::NotFixedLength(f.name))? as usize / 8;
let text = match v {
EncodeValue::Text(s) => s,
EncodeValue::NotAvailable => String::new(),
EncodeValue::Bytes(b) => {
if b.len() != byte_len {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: b.len() as f64,
range: None,
});
}
return Ok(Staged::Bytes(b));
}
_ => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "text",
});
}
};
let bytes = fit_utf8(&text, byte_len).as_bytes();
let mut buf = vec![pad; byte_len];
buf[..bytes.len()].copy_from_slice(bytes);
Ok(Staged::Bytes(buf))
}
fn fit_utf8(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
fn text_of(f: &FieldInfo, v: EncodeValue) -> Result<String, EncodeError> {
match v {
EncodeValue::Text(s) => Ok(s),
EncodeValue::NotAvailable => Ok(String::new()),
_ => Err(EncodeError::TypeMismatch {
field: f.name,
expected: "text",
}),
}
}
fn stage_string_lz(f: &FieldInfo, v: EncodeValue) -> Result<Staged, EncodeError> {
let text = text_of(f, v)?;
let room = match f.bit_length {
Some(bl) => (bl as usize / 8).saturating_sub(2),
None => u8::MAX as usize,
};
let content = fit_utf8(&text, room.min(u8::MAX as usize)).as_bytes();
let mut buf = Vec::with_capacity(content.len() + 2);
buf.push(content.len() as u8);
buf.extend_from_slice(content);
buf.push(0);
if let Some(bl) = f.bit_length {
buf.resize(bl as usize / 8, 0);
}
Ok(Staged::Bytes(buf))
}
fn stage_string_lau(f: &FieldInfo, v: EncodeValue) -> Result<Staged, EncodeError> {
let text = text_of(f, v)?;
let content = fit_utf8(&text, u8::MAX as usize - 2).as_bytes();
let mut buf = Vec::with_capacity(content.len() + 2);
buf.push((content.len() + 2) as u8);
buf.push(1); buf.extend_from_slice(content);
Ok(Staged::Bytes(buf))
}
fn stage_binary(f: &FieldInfo, v: EncodeValue) -> Result<Staged, EncodeError> {
let mut bytes = match v {
EncodeValue::Bytes(b) => b,
EncodeValue::NotAvailable => Vec::new(),
EncodeValue::Raw(raw) if f.bit_length.is_some_and(|bl| bl <= 64) => {
let width = f.bit_length.unwrap().div_ceil(8) as usize;
raw.to_le_bytes()[..width].to_vec()
}
_ => {
return Err(EncodeError::TypeMismatch {
field: f.name,
expected: "bytes",
});
}
};
if let Some(bl) = f.bit_length {
if bl % 8 != 0 && bl <= 64 {
let want = bl.div_ceil(8) as usize;
if bytes.len() > want {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: bytes.len() as f64,
range: None,
});
}
let mut raw: u64 = 0;
for (i, b) in bytes.iter().enumerate() {
raw |= u64::from(*b) << (8 * i);
}
raw &= u64::MAX >> (64 - bl);
return Ok(Staged::Scalar(raw));
}
let want = bl as usize / 8;
if bytes.len() > want {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: bytes.len() as f64,
range: None,
});
}
bytes.resize(want, 0);
}
Ok(Staged::Bytes(bytes))
}
fn stage_dynamic_value(f: &FieldInfo, v: EncodeValue) -> Result<Staged, EncodeError> {
match v {
EncodeValue::Bytes(b) => Ok(Staged::Bytes(b)),
EncodeValue::NotAvailable => Ok(Staged::Bytes(Vec::new())),
_ => Err(EncodeError::TypeMismatch {
field: f.name,
expected: "value bytes for a dynamic (key/value) field",
}),
}
}
fn default_raw(f: &FieldInfo) -> u64 {
let bl = f.bit_length.unwrap_or(0) + f.continuation.map_or(0, |c| c.bit_length);
if let Some(mv) = f.match_value {
return mask_to_width(mv, bl);
}
match f.field_type {
Some(FieldType::Spare) => 0,
_ => f.unknown_value.unwrap_or_else(|| all_ones(bl)),
}
}
fn continuing_staged(fields: &[FieldInfo], staged: &[Option<Staged>], j: usize) -> Option<Staged> {
let target = fields[j].continues?;
let t = fields[..j].iter().position(|g| g.id == target)?;
let low = &fields[t];
let shift = low.bit_length?;
let value = match &staged[t] {
Some(Staged::Scalar(v)) => *v,
None => default_raw(low),
_ => return None,
};
Some(Staged::Scalar(value >> shift))
}
fn emit_field(
buf: &mut Vec<u8>,
next_bit: &mut usize,
pgn: u32,
f: &FieldInfo,
staged: Option<&Staged>,
) -> Result<(), EncodeError> {
match staged {
Some(Staged::Bytes(bytes)) => {
for &b in bytes {
write_bits(buf, next_bit, 8, u64::from(b));
}
Ok(())
}
Some(Staged::Scalar(v)) => {
let bl = f.bit_length.ok_or(EncodeError::NotFixedLength(f.name))? as usize;
write_bits(buf, next_bit, bl, *v);
Ok(())
}
Some(Staged::Deferred(_)) => Err(EncodeError::NotFixedLength(f.name)),
None => write_unset(buf, next_bit, pgn, f),
}
}
fn write_unset(
buf: &mut Vec<u8>,
next_bit: &mut usize,
pgn: u32,
f: &FieldInfo,
) -> Result<(), EncodeError> {
match f.field_type {
Some(FieldType::StringFix) if f.bit_length.is_some() => {
for _ in 0..f.bit_length.unwrap() as usize / 8 {
write_bits(buf, next_bit, 8, u64::from(string_fix_pad(pgn)));
}
}
Some(FieldType::Binary) if f.bit_length.is_some() => {
for _ in 0..f.bit_length.unwrap() as usize / 8 {
write_bits(buf, next_bit, 8, 0xff);
}
}
Some(FieldType::Binary) => {}
Some(FieldType::StringLz) => write_bits(buf, next_bit, 8, 0), Some(FieldType::StringLau) => {
write_bits(buf, next_bit, 8, 2); write_bits(buf, next_bit, 8, 1); }
Some(FieldType::Variable)
| Some(FieldType::DynamicFieldKey)
| Some(FieldType::DynamicFieldLength)
| Some(FieldType::DynamicFieldValue) => {
return Err(EncodeError::NotFixedLength(f.name));
}
_ => {
let bl = f.bit_length.ok_or(EncodeError::NotFixedLength(f.name))? as usize;
write_bits(buf, next_bit, bl, default_raw(f));
}
}
Ok(())
}
fn mask_to_width(value: i64, bits: u32) -> u64 {
(value as u64) & all_ones(bits)
}
fn all_ones(bits: u32) -> u64 {
if bits >= 64 {
u64::MAX
} else {
(1u64 << bits) - 1
}
}
fn write_bits(buf: &mut Vec<u8>, next_bit: &mut usize, bits: usize, mut value: u64) {
for _ in 0..bits {
let idx = *next_bit >> 3;
let bitpos = *next_bit & 7;
if bitpos == 0 {
buf.push(0);
}
if value & 1 != 0 {
buf[idx] |= 1 << bitpos;
}
value >>= 1;
*next_bit += 1;
}
}
fn coerce_arg(f: &FieldInfo, s: &str) -> Result<EncodeValue, EncodeError> {
let s = s.trim();
if s.eq_ignore_ascii_case("n/a") || s.is_empty() {
return Ok(EncodeValue::NotAvailable);
}
if let Some(raw) = s.strip_prefix("raw:") {
let raw = raw.trim();
let parsed = match raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
Some(hex) => u64::from_str_radix(hex, 16),
None => raw.parse::<u64>(),
};
return parsed
.map(EncodeValue::Raw)
.map_err(|_| EncodeError::ValueOutOfRange {
field: f.name,
value: f64::NAN,
range: None,
});
}
if f.field_type != Some(FieldType::Decimal)
&& let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
&& let Ok(n) = i64::from_str_radix(hex, 16)
{
return Ok(EncodeValue::Int(n));
}
match f.field_type {
Some(FieldType::Lookup) | Some(FieldType::IndirectLookup) => {
match s.parse::<i64>() {
Ok(n) => Ok(EncodeValue::Int(n)),
Err(_) => Ok(EncodeValue::Lookup(s.to_string())),
}
}
Some(FieldType::Pgn) => {
s.parse::<u32>()
.map(EncodeValue::Pgn)
.map_err(|_| EncodeError::ValueOutOfRange {
field: f.name,
value: f64::NAN,
range: None,
})
}
Some(FieldType::StringFix)
| Some(FieldType::StringLz)
| Some(FieldType::StringLau)
| Some(FieldType::Variable) => Ok(EncodeValue::Text(s.to_string())),
Some(FieldType::Decimal) => Ok(EncodeValue::Text(s.to_string())),
_ => {
if let Ok(n) = s.parse::<i64>() {
Ok(EncodeValue::Int(n))
} else if let Ok(x) = s.parse::<f64>() {
Ok(EncodeValue::Number(x))
} else {
Err(EncodeError::ValueOutOfRange {
field: f.name,
value: f64::NAN,
range: None,
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn db() -> &'static PgnDatabase {
PgnDatabase::embedded(crate::engine::Units::Metric)
}
#[test]
fn a_number_outside_its_field_range_is_refused() {
use crate::engine::field::gps_almanac_data::INCLINATION_ANGLE;
let si = PgnDatabase::embedded(crate::engine::Units::Si);
let inclination = |v: f64| {
si.encode_for(crate::engine::pgn::GPS_ALMANAC_DATA)
.push(INCLINATION_ANGLE, v)
.map(|_| ())
};
let err = inclination(0.3 * std::f64::consts::PI).unwrap_err();
assert!(matches!(
err,
EncodeError::ValueOutOfRange {
field: "Inclination Angle",
range: Some((min, max)),
..
} if min < -0.196 && max > 0.196
));
assert!(err.to_string().contains("outside its range"));
assert!(inclination(-0.3 * std::f64::consts::PI).is_err());
let res = INCLINATION_ANGLE.field.resolution.unwrap();
let max = INCLINATION_ANGLE.field.range_max.unwrap();
let min = INCLINATION_ANGLE.field.range_min.unwrap();
inclination(max).unwrap();
inclination(min).unwrap();
inclination(max + res * 0.4).unwrap();
assert!(inclination(max + res).is_err());
}
#[test]
fn a_full_turn_angle_takes_a_negative_angle_as_the_same_direction() {
use crate::engine::field::wind_data::WIND_ANGLE;
let si = PgnDatabase::embedded(crate::engine::Units::Si);
let wind = |v: f64| {
let mut b = si.encode_for(crate::engine::pgn::WIND_DATA);
b.push(WIND_ANGLE, v)?;
Ok::<_, EncodeError>(b.build()?.data[3..5].to_vec())
};
let tau = std::f64::consts::TAU;
assert_eq!(wind(-0.5).unwrap(), wind(tau - 0.5).unwrap());
assert_eq!(
wind(-std::f64::consts::PI).unwrap(),
wind(std::f64::consts::PI).unwrap()
);
assert!(wind(-3.2).is_err());
assert!(wind(tau + 0.1).is_err());
}
#[test]
fn a_decimal_is_digit_pairs_and_round_trips() {
use crate::engine::decode::FieldValue;
let address = |v: &str| {
let mut b = db().encode_for(crate::engine::pgn::DSC_CALL_INFORMATION);
b.push_arg("dscMessageAddress", v)?;
b.build()
};
let decoded = |v: &str| {
let frame = address(v).unwrap();
let decoded = db().decode(&frame).unwrap();
let f = decoded
.fields
.iter()
.find(|f| f.id() == "dscMessageAddress");
(frame.data[2..7].to_vec(), f.map(|f| f.value.clone()))
};
let (wire, value) = decoded("0244123456");
assert_eq!(wire, [2, 44, 12, 34, 56]);
assert!(matches!(value, Some(FieldValue::Decimal(d)) if d == "0244123456"));
let (wire, value) = decoded("244060807");
assert_eq!(wire, [24, 40, 60, 80, 70]);
assert!(matches!(value, Some(FieldValue::Decimal(d)) if d == "244060807"));
let (wire, value) = decoded("002241024");
assert_eq!(wire, [0, 22, 41, 2, 40]);
assert!(matches!(value, Some(FieldValue::Decimal(d)) if d == "002241024"));
let int = |n: i64| {
let mut b = db().encode_for(crate::engine::pgn::DSC_CALL_INFORMATION);
b.push_by_name("DSC Message Address", n).unwrap();
b.build().unwrap().data[2..7].to_vec()
};
assert_eq!(int(244123456), [24, 41, 23, 45, 60]);
assert_eq!(int(2241024), [0, 22, 41, 2, 40]);
assert_eq!(int(2241024000), [22, 41, 2, 40, 0]);
assert!(matches!(
address("12345678901"),
Err(EncodeError::ValueOutOfRange { .. })
));
assert!(matches!(
address("12ab"),
Err(EncodeError::TypeMismatch { .. })
));
assert!(matches!(
address("0x12"),
Err(EncodeError::TypeMismatch { .. })
));
assert_eq!(address("raw:0xffffffffff").unwrap().data[2..7], [0xff; 5]);
}
#[test]
fn iso_request_matches_c_format_message() {
let frame = db()
.encode_for(crate::engine::pgn::ISO_REQUEST)
.priority(6)
.destination(255)
.push(crate::engine::field::iso_request::PGN, 126996u32)
.unwrap()
.build()
.unwrap();
assert_eq!(frame.pgn, 59904);
assert_eq!(frame.prio, 6);
assert_eq!(frame.dst, 255);
assert_eq!(frame.data.as_slice(), &[0x14, 0xf0, 0x01]);
}
#[test]
fn j1939_temperatures_encode_exactly() {
use crate::engine::Units;
for (units, coolant, fuel, oil) in [
(Units::Si, 323.15, 233.15, 373.15),
(Units::Metric, 50.0, -40.0, 100.0),
] {
let db = PgnDatabase::embedded_j1939(units);
let mut b = db.encode_by_pgn(65262).unwrap();
b.push_by_name("Engine Coolant Temp", coolant)
.unwrap()
.push_by_name("Engine Fuel Temp 1", fuel)
.unwrap()
.push_by_name("Engine Oil Temp 1", oil)
.unwrap();
let frame = b.build().unwrap();
assert_eq!(&frame.data[..4], &[90, 0, 0xa0, 0x2e], "{units:?}");
}
}
#[test]
fn a_dm1_spn_is_split_on_encode_and_joined_on_decode() {
use crate::engine::decode::FieldValue;
let db = PgnDatabase::embedded_j1939(crate::engine::Units::Si);
let dtc = |spn: EncodeValue| {
let mut b = db.encode_by_pgn(65226).unwrap();
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "spn", spn)
.unwrap()
.push_in_set(1, i, "fmi", 2u32)
.unwrap()
.push_in_set(1, i, "oc", 5u32)
.unwrap()
.push_in_set(1, i, "cm", 0u32)
.unwrap();
b.build().unwrap()
};
let frame = dtc(EncodeValue::Int(520192));
assert_eq!(&frame.data[2..6], &[0x00, 0xf0, 0xe2, 0x05]);
let decoded = db.decode(&frame).unwrap();
let spn = decoded.fields.iter().find(|f| f.id() == "spn").unwrap();
assert!(matches!(
spn.value,
FieldValue::Lookup {
value: 520192,
name: None
}
));
assert!(decoded.fields.iter().all(|f| f.id() != "spnHigh"));
let mut short = frame.clone();
short.data.truncate(3);
let decoded = db.decode(&short).unwrap();
assert!(
decoded.fields.iter().all(|f| f.id() != "spn"),
"a cut-off SPN is left out"
);
let frame = dtc(EncodeValue::Lookup("Engine Oil Pressure".into()));
assert_eq!(&frame.data[2..6], &[0x64, 0x00, 0x02, 0x05]);
let decoded = db.decode(&frame).unwrap();
let spn = decoded.fields.iter().find(|f| f.id() == "spn").unwrap();
assert!(matches!(
spn.value,
FieldValue::Lookup {
value: 100,
name: Some("Engine Oil Pressure")
}
));
}
#[test]
fn ambiguous_pgn_number_is_rejected() {
assert!(db().encode_by_pgn(59904).is_ok());
assert!(matches!(
db().encode_by_pgn(126208),
Err(EncodeError::AmbiguousPgn { pgn: 126208, .. })
));
}
#[test]
fn unset_fields_default_and_length_checks() {
let frame = db().encode_for(crate::engine::pgn::ISO_REQUEST).build();
let f = frame.unwrap();
assert_eq!(f.data.len(), 3);
assert_eq!(f.data.as_slice(), &[0xff, 0xff, 0xff]);
}
#[test]
fn wind_data_round_trips_through_decode() {
use crate::engine::field::wind_data as wd;
let db = db();
let frame = db
.encode_for(crate::engine::pgn::WIND_DATA)
.push(wd::SID, 7)
.unwrap()
.push(wd::WIND_SPEED, 5.23)
.unwrap()
.push(wd::WIND_ANGLE, 1.5)
.unwrap()
.push(wd::REFERENCE, EncodeValue::Lookup("Apparent".into()))
.unwrap()
.build()
.unwrap();
assert_eq!(frame.pgn, 130306);
assert_eq!(frame.data.len(), 8);
let decoded = db.decode(&frame).unwrap();
assert_eq!(decoded.id, "windData");
let num = |name: &str| match &decoded.field_by_name(name).unwrap().value {
crate::engine::FieldValue::Number(x) => *x,
crate::engine::FieldValue::Integer(n) => *n as f64,
other => panic!("field {name}: expected a number, got {other:?}"),
};
assert!((num("Wind Speed") - 5.23).abs() < 0.01);
assert!((num("Wind Angle") - 1.5).abs() < 0.01);
assert_eq!(num("SID"), 7.0);
match &decoded.field_by_name("Reference").unwrap().value {
crate::engine::FieldValue::Lookup { value, name } => {
assert_eq!(*value, 2);
assert_eq!(*name, Some("Apparent"));
}
other => panic!("Reference: expected lookup, got {other:?}"),
}
}
#[test]
fn repeating_set_round_trips_through_decode() {
let db = db();
let mut b = db.encode("gnssSatsInView").unwrap();
b.push_by_name("SID", 9).unwrap();
for (prn, snr) in [(7i64, 30.0f64), (11, 42.5)] {
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "PRN", prn).unwrap();
b.push_in_set(1, i, "SNR", snr).unwrap();
}
let frame = b.build().unwrap();
assert_eq!(frame.pgn, 129540);
let decoded = db.decode(&frame).unwrap();
assert!(decoded.has_repeating_set[0]);
let count = match &decoded.field_by_name("Sats in View").unwrap().value {
crate::engine::FieldValue::Integer(n) => *n,
crate::engine::FieldValue::Number(x) => *x as i64,
other => panic!("Sats in View: expected a number, got {other:?}"),
};
assert_eq!(count, 2, "count field must auto-fill from instances");
let per_iter = |id: &str| -> Vec<f64> {
decoded
.fields
.iter()
.filter(|f| f.info.id == id && f.repeat_set == 1)
.map(|f| match &f.value {
crate::engine::FieldValue::Integer(n) => *n as f64,
crate::engine::FieldValue::Number(x) => *x,
other => panic!("{id}: expected a number, got {other:?}"),
})
.collect()
};
assert_eq!(per_iter("prn"), vec![7.0, 11.0]);
let snrs = per_iter("snr");
assert_eq!(snrs.len(), 2);
assert!((snrs[0] - 30.0).abs() < 0.01);
assert!((snrs[1] - 42.5).abs() < 0.01);
}
#[test]
fn repeating_set_errors() {
let db = db();
assert!(matches!(
db.encode("windData").unwrap().add_set_instance(1),
Err(EncodeError::NoRepeatingSet { set: 1, .. })
));
assert!(matches!(
db.encode("gnssSatsInView")
.unwrap()
.push_in_set(1, 0, "PRN", 1),
Err(EncodeError::NoSuchInstance { instance: 0, .. })
));
}
#[test]
fn group_function_variable_values_encode_via_the_referenced_pgn() {
let db = db();
let mut b = db.encode("nmeaCommandGroupFunction").unwrap();
b.push_by_name("PGN", EncodeValue::Pgn(127250)).unwrap();
let heading_order = db
.first_pgn(127250)
.unwrap()
.fields
.iter()
.find(|f| f.id == "heading")
.unwrap()
.order as i64;
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "Parameter", heading_order).unwrap();
b.push_in_set(1, i, "Value", 1.5f64).unwrap();
let frame = b.build().map_err(|e| format!("{e}")).unwrap();
assert_eq!(frame.pgn, 126208);
let decoded = db.decode(&frame).unwrap();
let val = decoded
.fields
.iter()
.find(|f| f.repeat_set == 1 && f.info.field_type == Some(FieldType::Variable))
.expect("decoded VARIABLE value");
match &val.value {
crate::engine::FieldValue::Number(x) => assert!((x - 1.5).abs() < 0.01, "got {x}"),
crate::engine::FieldValue::Integer(n) => {
assert!((*n as f64 - 1.5).abs() < 1.0, "got {n}")
}
other => panic!("value: {other:?}"),
}
}
#[test]
fn variable_target_variant_follows_the_parameter_pairs() {
let db = db();
let mut b = db.encode("nmeaCommandGroupFunction").unwrap();
b.push_by_name("PGN", EncodeValue::Pgn(130833)).unwrap();
for (param, value) in [(1i64, 1855f64), (3, 4.0), (5, 3.21)] {
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "Parameter", param).unwrap();
b.push_in_set(1, i, "Value", value).unwrap();
}
let frame = b.build().map_err(|e| format!("{e}")).unwrap();
assert_eq!(frame.data.len(), 14, "data: {:02x?}", frame.data);
assert_eq!(
&frame.data[12..],
&[0x41, 0x01],
"data: {:02x?}",
frame.data
);
let decoded = db.decode(&frame).unwrap();
let val = decoded
.fields
.iter()
.rfind(|f| f.repeat_set == 1 && f.info.field_type == Some(FieldType::Variable))
.expect("decoded VARIABLE value");
match &val.value {
crate::engine::FieldValue::Number(x) => assert!((x - 3.21).abs() < 0.005, "got {x}"),
other => panic!("value: {other:?}"),
}
}
#[test]
fn variable_target_without_narrowing_pairs_keeps_the_leading_definition() {
let db = db();
let mut b = db.encode("nmeaCommandGroupFunction").unwrap();
b.push_by_name("PGN", EncodeValue::Pgn(130833)).unwrap();
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "Parameter", 5i64).unwrap();
b.push_in_set(1, i, "Value", 3f64).unwrap();
let frame = b.build().map_err(|e| format!("{e}")).unwrap();
assert_eq!(frame.data.len(), 15, "data: {:02x?}", frame.data);
assert_eq!(frame.data[7], 3, "data: {:02x?}", frame.data);
}
#[test]
fn variable_without_target_is_a_clear_error() {
let db = db();
let mut b = db.encode("nmeaCommandGroupFunction").unwrap();
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "Value", 1i64).unwrap();
assert!(matches!(
b.build(),
Err(EncodeError::VariableUnresolved { .. })
));
}
#[test]
fn unknown_lookup_label_is_rejected() {
assert!(matches!(
db().encode("windData")
.unwrap()
.push_by_name("Reference", EncodeValue::Lookup("Nonsense".into())),
Err(EncodeError::UnknownLookupLabel { .. })
));
}
#[test]
fn no_such_field_and_pgn() {
assert!(matches!(
db().encode("isoRequest").unwrap().push_by_name("Nope", 1),
Err(EncodeError::NoSuchField { .. })
));
assert!(matches!(
db().encode("notAPgnId"),
Err(EncodeError::NoSuchPgnId(_))
));
}
#[test]
fn string_fix_round_trips() {
use crate::engine::field::product_information::MODEL_ID;
let frame = db()
.encode("productInformation")
.unwrap()
.push(MODEL_ID, "SCX-20")
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(MODEL_ID).and_then(|f| f.value.as_str()),
Some("SCX-20")
);
}
#[test]
fn ais_text_is_padded_with_at_signs() {
use crate::engine::field::ais_class_bstatic_data_msg24_part_a::NAME;
let frame = db()
.encode("aisClassBStaticDataMsg24PartA")
.unwrap()
.push(NAME, "MERRIMAC")
.unwrap()
.build()
.unwrap();
assert_eq!(&frame.data[5..25], b"MERRIMAC@@@@@@@@@@@@");
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(NAME).and_then(|f| f.value.as_str()),
Some("MERRIMAC")
);
}
#[test]
fn unset_ais_text_is_all_at_signs() {
let frame = db()
.encode("aisClassBStaticDataMsg24PartB")
.unwrap()
.build()
.unwrap();
assert_eq!(&frame.data[6..20], &[b'@'; 14], "Vendor ID and Callsign");
for pgn in AIS_TEXT_PGNS {
let info = db().pgns().find(|p| p.pgn == pgn).unwrap();
let frame = db().encode(info.id).unwrap().build().unwrap();
let mut offset = 0;
for f in info.fields {
let bits = f.bit_length.unwrap_or(0) as usize;
if f.field_type == Some(FieldType::StringFix) {
let text = &frame.data[offset / 8..(offset + bits) / 8];
assert!(
text.iter().all(|&b| b == b'@'),
"{pgn} {}: {text:?}",
f.name
);
}
offset += bits;
}
}
}
#[test]
fn other_fixed_strings_are_padded_with_0xff() {
use crate::engine::field::product_information::MODEL_ID;
let frame = db()
.encode("productInformation")
.unwrap()
.push(MODEL_ID, "SCX-20")
.unwrap()
.build()
.unwrap();
assert_eq!(&frame.data[4..10], b"SCX-20");
assert!(frame.data[10..36].iter().all(|&b| b == 0xff));
}
#[test]
fn string_lau_round_trips() {
use crate::engine::field::configuration_information::INSTALLATION_DESCRIPTION1 as DESC1;
let frame = db()
.encode("configurationInformation")
.unwrap()
.push(DESC1, "Helm Station")
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(DESC1).and_then(|f| f.value.as_str()),
Some("Helm Station")
);
}
#[test]
fn string_lz_round_trips() {
use crate::engine::field::navico_ascii_identifier::IDENTIFIER;
let frame = db()
.encode("navicoAsciiIdentifier")
.unwrap()
.push(IDENTIFIER, "BOW")
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(IDENTIFIER).and_then(|f| f.value.as_str()),
Some("BOW")
);
}
#[test]
fn string_lz_is_written_as_fusion_sends_it() {
use crate::engine::field::fusion_set_zone_name::{NAME, ZONE};
let frame = db()
.encode("fusionSetZoneName")
.unwrap()
.push(ZONE, 0.0)
.unwrap()
.push(NAME, "COCKPIT")
.unwrap()
.build()
.unwrap();
assert_eq!(
frame.data.as_slice(),
&[
0xa3, 0x99, 0x22, 0x00, 0x00, 0x07, b'C', b'O', b'C', b'K', b'P', b'I', b'T', 0x00
]
);
}
#[test]
fn a_fixed_width_string_lz_keeps_room_for_its_nul() {
use crate::engine::field::fusion_rds_data::RDS;
let long = "x".repeat(40);
let frame = db()
.encode("fusionRdsData")
.unwrap()
.push(RDS, long.as_str())
.unwrap()
.build()
.unwrap();
let tail = &frame.data[frame.data.len() - 32..];
assert_eq!(tail[0], 30);
assert_eq!(&tail[1..31], "x".repeat(30).as_bytes());
assert_eq!(tail[31], 0);
}
#[test]
fn too_long_strings_are_shortened_not_refused() {
use crate::engine::field::product_information::MODEL_ID;
let text = format!("{}é", "a".repeat(31));
let frame = db()
.encode("productInformation")
.unwrap()
.push(MODEL_ID, text.as_str())
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(MODEL_ID).and_then(|f| f.value.as_str()),
Some("a".repeat(31).as_str())
);
use crate::engine::field::configuration_information::INSTALLATION_DESCRIPTION1 as DESC1;
let frame = db()
.encode("configurationInformation")
.unwrap()
.push(DESC1, "y".repeat(300).as_str())
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
assert_eq!(
d.field(DESC1).and_then(|f| f.value.as_str()).map(str::len),
Some(253)
);
}
#[test]
fn binary_round_trips() {
use crate::engine::field::iso_transport_protocol_data_transfer::DATA;
let payload = vec![0xde_u8, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03];
let frame = db()
.encode("isoTransportProtocolDataTransfer")
.unwrap()
.push(DATA, payload.clone())
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
match d.field(DATA).map(|f| &f.value) {
Some(crate::engine::FieldValue::Binary(v)) => assert_eq!(v, &payload),
other => panic!("expected binary, got {other:?}"),
}
}
#[test]
fn float_round_trips() {
use crate::engine::field::garmin_autopilot_heading_to_steer::HEADING_TO_STEER as HTS;
let frame = db()
.encode("garminAutopilotHeadingToSteer")
.unwrap()
.push(HTS, 1.5)
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
let got = d.field(HTS).and_then(|f| f.value.as_f64()).unwrap();
assert!((got - 1.5).abs() < 1e-6, "got {got}");
}
#[test]
fn an_integer_is_a_value_in_the_fields_unit() {
use crate::engine::field::temperature::ACTUAL_TEMPERATURE;
use crate::engine::field::wind_data::{REFERENCE, WIND_SPEED};
let si = PgnDatabase::embedded(crate::engine::Units::Si);
let wind = |v: EncodeValue| {
si.encode_for(crate::engine::pgn::WIND_DATA)
.push(WIND_SPEED, v)
.unwrap()
.build()
.unwrap()
.data
};
assert_eq!(wind(5.into()), wind(5.0.into()));
let temperature = |v: EncodeValue| {
si.encode_for(crate::engine::pgn::TEMPERATURE)
.push(ACTUAL_TEMPERATURE, v)
.unwrap()
.build()
.unwrap()
};
let frame = temperature(300.into());
assert_eq!(frame.data, temperature(300.0.into()).data);
let d = si.decode(&frame).unwrap();
let got = d.field(ACTUAL_TEMPERATURE).and_then(|f| f.value.as_f64());
assert!((got.unwrap() - 300.0).abs() < 1e-9, "{got:?}");
let frame = si
.encode_for(crate::engine::pgn::WIND_DATA)
.push(REFERENCE, 2)
.unwrap()
.build()
.unwrap();
let d = si.decode(&frame).unwrap();
assert_eq!(
d.field(REFERENCE).and_then(|f| f.value.as_str()),
Some("Apparent")
);
}
#[test]
fn a_signed_field_takes_its_full_negative_end() {
use crate::engine::field::battery_status::CURRENT;
let si = PgnDatabase::embedded(crate::engine::Units::Si);
let current = |a: f64| {
si.encode_for(crate::engine::pgn::BATTERY_STATUS)
.push(CURRENT, a)
.and_then(|b| b.build())
};
let frame = current(-3276.8).unwrap();
assert_eq!(&frame.data[3..5], &[0x00, 0x80]);
let d = si.decode(&frame).unwrap();
let got = d.field(CURRENT).and_then(|f| f.value.as_f64()).unwrap();
assert!((got + 3276.8).abs() < 1e-9, "{got}");
assert!(matches!(
current(-3276.9),
Err(EncodeError::ValueOutOfRange { .. })
));
}
#[test]
fn raw_writes_the_wire_bits() {
use crate::engine::field::temperature::{ACTUAL_TEMPERATURE, INSTANCE};
let si = PgnDatabase::embedded(crate::engine::Units::Si);
let b = || si.encode_for(crate::engine::pgn::TEMPERATURE);
assert!(matches!(
b().push(INSTANCE, 255),
Err(EncodeError::ValueOutOfRange { .. })
));
let frame = b()
.push(INSTANCE, Raw(255))
.unwrap()
.push(ACTUAL_TEMPERATURE, Raw(0x7531))
.unwrap()
.build()
.unwrap();
assert_eq!(frame.data[1], 255);
assert_eq!(&frame.data[3..5], &[0x31, 0x75]);
let frame = b().push(INSTANCE, Raw(0x1_23)).unwrap().build().unwrap();
assert_eq!(frame.data[1], 0x23);
}
#[test]
fn an_unscaled_integer_is_exact() {
let f = FieldInfo {
resolution: Some(1.0),
range_min: None,
range_max: None,
..db()
.pgns()
.flat_map(|p| p.fields)
.find(|f| f.field_type == Some(FieldType::Number) && f.bit_length == Some(64))
.copied()
.unwrap()
};
let n = (1i64 << 60) + 1;
assert_eq!(int_to_raw(&f, n).unwrap(), n);
}
#[test]
fn format_message_spells_raw_explicitly() {
use crate::engine::field::wind_data::{REFERENCE, WIND_SPEED};
let speed = WIND_SPEED.field;
assert_eq!(coerce_arg(speed, "5").unwrap(), EncodeValue::Int(5));
assert_eq!(coerce_arg(speed, "0x5").unwrap(), EncodeValue::Int(5));
assert_eq!(coerce_arg(speed, "5.0").unwrap(), EncodeValue::Number(5.0));
assert_eq!(
coerce_arg(speed, "raw:0xffff").unwrap(),
EncodeValue::Raw(0xffff)
);
assert_eq!(coerce_arg(speed, "raw:500").unwrap(), EncodeValue::Raw(500));
assert!(coerce_arg(speed, "raw:-1").is_err());
assert_eq!(
coerce_arg(REFERENCE.field, "2").unwrap(),
EncodeValue::Int(2)
);
}
#[test]
fn iso_name_round_trips() {
use crate::engine::field::simnet_data_source_selection::SOURCE;
let name: u64 = 0xC078_8C00_E7E0_4364;
let frame = db()
.encode("simnetDataSourceSelection")
.unwrap()
.push(SOURCE, Raw(name))
.unwrap()
.build()
.unwrap();
let d = db().decode(&frame).unwrap();
match d.field(SOURCE).map(|f| &f.value) {
Some(crate::engine::FieldValue::IsoName { value, .. }) => assert_eq!(*value, name),
other => panic!("expected iso name, got {other:?}"),
}
}
#[test]
fn simnet_key_value_encodes_key_and_value() {
let frame = db()
.encode("simnetKeyValue")
.unwrap()
.destination(255)
.push_by_name("Address", EncodeValue::Int(255))
.unwrap()
.push_by_name("Instance", Raw(255))
.unwrap()
.push_by_name("Network Group", EncodeValue::Int(1))
.unwrap()
.push_by_name("Source", Raw(255))
.unwrap()
.push_by_name("Key", EncodeValue::Int(9983))
.unwrap()
.push_by_name("Operation", EncodeValue::Int(1))
.unwrap()
.push_by_name("Value", EncodeValue::Bytes(vec![0x04]))
.unwrap()
.build()
.unwrap();
assert_eq!(frame.pgn, 130845);
assert_eq!(frame.data.len(), 11);
assert_eq!(&frame.data[6..9], &[0xff, 0x26, 0x00]);
assert_eq!(frame.data[9], 0x01); assert_eq!(frame.data[10], 0x04); }
#[test]
fn dynamic_field_value_rejects_non_bytes() {
assert!(matches!(
db().encode("simnetKeyValue")
.unwrap()
.push_by_name("Value", EncodeValue::Int(4)),
Err(EncodeError::TypeMismatch { .. })
));
}
#[test]
fn dynamic_group_function_field_is_a_clear_error() {
let mut b = db().encode("nmeaRequestGroupFunction").unwrap();
b.add_set_instance(1).unwrap();
let err = b.build();
assert!(
matches!(err, Err(EncodeError::NotFixedLength(_))),
"got {err:?}"
);
}
#[test]
fn zero_instance_repeating_pgn_builds_with_zero_count() {
let frame = db()
.encode("nmeaRequestGroupFunction")
.unwrap()
.build()
.unwrap();
assert_eq!(frame.pgn, 126208);
let decoded = db().decode(&frame).unwrap();
match &decoded
.field_by_name("Number of Parameters")
.expect("count field decoded")
.value
{
crate::engine::FieldValue::Integer(n) => assert_eq!(*n, 0),
crate::engine::FieldValue::Number(x) => assert_eq!(*x, 0.0),
other => panic!("count: expected a number, got {other:?}"),
}
}
fn write_fields(pgn: u32) -> PgnBuilder {
let mut b = db().encode("nmeaWriteFieldsGroupFunction").unwrap();
b.push_by_name("PGN", EncodeValue::Pgn(pgn)).unwrap();
b.push_by_name("Unique ID", 0i64).unwrap();
b
}
#[test]
fn group_function_header_is_left_out_for_a_standard_pgn() {
let mut b = write_fields(127508);
let i = b.add_set_instance(1).unwrap();
b.push_in_set(1, i, "selectionParameter", Raw(1)).unwrap();
b.push_in_set(1, i, "selectionValue", Raw(0x66)).unwrap();
let i = b.add_set_instance(2).unwrap();
b.push_in_set(2, i, "parameter", Raw(1)).unwrap();
b.push_in_set(2, i, "value", Raw(5)).unwrap();
let frame = b.build().unwrap();
assert_eq!(
frame.data.as_slice(),
&[
0x05, 0x14, 0xf2, 0x01, 0x00, 0x01, 0x01, 0x01, 0x66, 0x01, 0x05
]
);
}
#[test]
fn group_function_header_is_written_for_a_proprietary_pgn() {
let mut b = write_fields(130845);
b.push_by_name("Manufacturer Code", Raw(1855)).unwrap();
b.push_by_name("Industry Code", Raw(4)).unwrap();
let frame = b.build().unwrap();
assert_eq!(
frame.data.as_slice(),
&[0x05, 0x1d, 0xff, 0x01, 0x3f, 0x9f, 0x00, 0x00, 0x00]
);
let decoded = db().decode(&frame).unwrap();
assert!(matches!(
decoded.field_by_name("Manufacturer Code").map(|f| &f.value),
Some(crate::engine::FieldValue::Lookup { value: 1855, .. })
));
}
#[test]
fn group_function_header_must_match_the_commanded_pgn() {
let mut b = write_fields(130845);
b.push_by_name("Industry Code", Raw(4)).unwrap();
assert_eq!(
b.build().unwrap_err(),
EncodeError::ProprietaryHeader {
field: "Manufacturer Code",
target_pgn: Some(130845),
proprietary: true,
}
);
let mut b = write_fields(127508);
b.push_by_name("Manufacturer Code", Raw(1855)).unwrap();
assert_eq!(
b.build().unwrap_err(),
EncodeError::ProprietaryHeader {
field: "Manufacturer Code",
target_pgn: Some(127508),
proprietary: false,
}
);
}
}