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, self.db_version)?;
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) -> RawFrame {
let offset = (interval_ms / 10) as u16;
let data = [
offset as u8,
(offset >> 8) as u8,
seq,
0xCC,
0xff,
0xff,
0xff,
0xff,
];
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]); }
}