use crate::engine::encode::{EncodeError, Raw};
use crate::engine::{ADDR_GLOBAL, PgnDatabase, RawFrame, Units, field};
const PGN_ISO_ACK: u32 = 59392;
const PGN_PGN_LIST: u32 = 126464;
const PGN_HEARTBEAT: u32 = 126993;
pub struct ProductInfo<'a> {
pub db_version: i64,
pub product_code: i64,
pub model_id: &'a str,
pub software_version: &'a str,
pub model_version: &'a str,
pub model_serial: &'a str,
pub certification_level: i64,
pub load_equivalency: i64,
}
impl ProductInfo<'_> {
pub fn frame(&self, src: u8) -> Option<RawFrame> {
use field::product_information as pi;
let db = PgnDatabase::embedded(Units::Si);
let build = || -> Result<RawFrame, crate::engine::encode::EncodeError> {
let mut b = db.encode("productInformation")?.source(src);
b.push(pi::NMEA2000_VERSION, Raw(self.db_version as u64))?;
b.push(pi::PRODUCT_CODE, self.product_code)?;
b.push(pi::MODEL_ID, self.model_id)?;
b.push(pi::SOFTWARE_VERSION_CODE, self.software_version)?;
b.push(pi::MODEL_VERSION, self.model_version)?;
b.push(pi::MODEL_SERIAL_CODE, self.model_serial)?;
b.push(pi::CERTIFICATION_LEVEL, self.certification_level)?;
b.push(pi::LOAD_EQUIVALENCY, self.load_equivalency)?;
b.build()
};
match build() {
Ok(f) => Some(f),
Err(e) => {
log::error!("PGN 126996 encode failed: {e}");
None
}
}
}
}
pub fn pgn_list_frames(src: u8, dst: u8, tx: &[u32], rx: &[u32]) -> Vec<RawFrame> {
[(0u8, tx), (1u8, rx)]
.into_iter()
.map(|(func, list)| {
let mut data = Vec::with_capacity(1 + 3 * list.len());
data.push(func);
for pgn in list {
data.extend_from_slice(&pgn.to_le_bytes()[..3]);
}
RawFrame::new(None, 6, PGN_PGN_LIST, src, dst, data)
})
.collect()
}
pub fn iso_ack_frame(src: u8, dst: u8, control: u8, pgn: u32) -> RawFrame {
let p = pgn.to_le_bytes();
let data = [control, 0xff, 0xff, 0xff, 0xff, p[0], p[1], p[2]];
RawFrame::new(None, 6, PGN_ISO_ACK, src, dst, data)
}
pub fn heartbeat_frame(src: u8, seq: u8, interval_ms: u64) -> Result<RawFrame, EncodeError> {
let f = field::heartbeat::DATA_TRANSMIT_OFFSET.field;
let (min, max) = (f.range_min.unwrap_or(0.0), f.range_max.unwrap_or(0.0));
let interval = interval_ms as f64 / 1000.0;
if interval > max {
return Err(EncodeError::ValueOutOfRange {
field: f.name,
value: interval,
range: Some((min, max)),
});
}
let offset = interval_ms as u16;
let data = [
offset as u8,
(offset >> 8) as u8,
seq,
0xCC,
0xff,
0xff,
0xff,
0xff,
];
Ok(RawFrame::new(
None,
7,
PGN_HEARTBEAT,
src,
ADDR_GLOBAL,
data,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn product_info_round_trips_and_carries_the_code() {
let pi = ProductInfo {
db_version: 2100,
product_code: 21488,
model_id: "H5000 Motion Sensor",
software_version: "1.2.4",
model_version: "Motion",
model_serial: "123",
certification_level: 2,
load_equivalency: 1,
};
let f = pi.frame(24).expect("encodes");
assert_eq!(f.pgn, 126996);
assert_eq!(f.src, 24);
assert_eq!(f.data.len(), 134);
let d = PgnDatabase::embedded(Units::Si).decode(&f).unwrap();
assert_eq!(
d.field(field::product_information::PRODUCT_CODE)
.and_then(|x| x.value.as_i64()),
Some(21488)
);
assert_eq!(
d.field(field::product_information::MODEL_ID)
.and_then(|x| x.value.as_str()),
Some("H5000 Motion Sensor")
);
}
#[test]
fn pgn_lists_are_two_framed_functions() {
let out = pgn_list_frames(24, ADDR_GLOBAL, &[60928, 126996], &[59904]);
assert_eq!(out.len(), 2);
assert_eq!(out[0].data[0], 0); assert_eq!(&out[0].data[1..4], &[0x00, 0xee, 0x00]); assert_eq!(out[1].data[0], 1); assert!(out.iter().all(|f| f.pgn == PGN_PGN_LIST));
}
#[test]
fn iso_ack_encodes_control_and_pgn() {
let f = iso_ack_frame(24, 9, 1, 126996);
assert_eq!(f.pgn, PGN_ISO_ACK);
assert_eq!(f.data[0], 1); assert_eq!(&f.data[5..8], &[0x14, 0xf0, 0x01]); }
#[test]
fn heartbeat_advertises_its_interval_in_milliseconds() {
let db = PgnDatabase::embedded(Units::Si);
let frame = heartbeat_frame(7, 155, 60_000).unwrap();
assert_eq!(&frame.data[..2], &[0x60, 0xea]);
let decoded = db.decode(&frame).unwrap();
let offset = decoded.field_by_name("Data transmit offset").unwrap();
assert_eq!(offset.value.as_f64(), Some(60.0));
let frame = heartbeat_frame(7, 0, 65_532).unwrap();
assert_eq!(&frame.data[..2], &[0xfc, 0xff]);
let decoded = db.decode(&frame).unwrap();
let offset = decoded.field_by_name("Data transmit offset").unwrap();
assert_eq!(offset.value.as_f64(), Some(65.532));
}
#[test]
fn heartbeat_refuses_an_interval_the_field_cannot_carry() {
for interval_ms in [65_533, 65_535, 3_600_000] {
match heartbeat_frame(7, 0, interval_ms) {
Err(EncodeError::ValueOutOfRange {
field,
value,
range,
}) => {
assert_eq!(field, "Data transmit offset");
assert_eq!(value, interval_ms as f64 / 1000.0);
assert_eq!(range, Some((0.0, 65.532)));
}
other => panic!("{interval_ms} ms: expected ValueOutOfRange, got {other:?}"),
}
}
}
}