use heapless::Vec;
use crate::lss::{self, LssAddress, LssSlave};
use crate::nmt::{self, NmtState, NmtStateMachine};
use crate::object_dictionary::ObjectDictionary;
use crate::pdo::{self, PdoMapping, TransmissionType};
use crate::sdo::{self, SdoServer};
use crate::types::NodeId;
use crate::{Error, Result};
pub const MAX_PDOS: usize = 4;
pub const MAX_PDO_MAPPING: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TxFrame {
pub cob_id: u16,
data: [u8; 8],
len: u8,
}
impl TxFrame {
fn new(cob_id: u16, bytes: &[u8]) -> Self {
let len = bytes.len().min(8);
let mut data = [0u8; 8];
data[..len].copy_from_slice(&bytes[..len]);
Self {
cob_id,
data,
len: len as u8,
}
}
pub fn data(&self) -> &[u8] {
&self.data[..self.len as usize]
}
}
#[derive(Debug)]
struct RpdoSlot {
cob_id: u16,
mapping: PdoMapping<MAX_PDO_MAPPING>,
}
#[derive(Debug)]
struct TpdoSlot {
cob_id: u16,
mapping: PdoMapping<MAX_PDO_MAPPING>,
transmission: TransmissionType,
}
#[derive(Debug)]
pub struct Node<const N: usize> {
node_id: NodeId,
od: ObjectDictionary<N>,
sdo: SdoServer,
nmt: NmtStateMachine,
rpdos: Vec<RpdoSlot, MAX_PDOS>,
tpdos: Vec<TpdoSlot, MAX_PDOS>,
lss: Option<LssSlave>,
guard_toggle: bool,
}
impl<const N: usize> Node<N> {
pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
Self {
node_id,
od,
sdo: SdoServer::new(node_id),
nmt: NmtStateMachine::new(),
rpdos: Vec::new(),
tpdos: Vec::new(),
lss: None,
guard_toggle: false,
}
}
pub fn enable_lss(&mut self, address: LssAddress) {
self.lss = Some(LssSlave::new(address, self.node_id.raw()));
}
pub fn set_node_id(&mut self, node_id: NodeId) {
self.node_id = node_id;
self.sdo = SdoServer::new(node_id);
}
pub fn apply_lss_node_id(&mut self) -> Option<NodeId> {
let pending = self.lss.as_ref()?.pending_node_id();
let node_id = NodeId::new(pending).ok()?;
self.set_node_id(node_id);
if let Some(lss) = &mut self.lss {
lss.adopt_pending();
}
Some(node_id)
}
pub fn lss(&self) -> Option<&LssSlave> {
self.lss.as_ref()
}
pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
self.rpdos
.push(RpdoSlot { cob_id, mapping })
.map_err(|_| Error::MappingFull)
}
pub fn add_tpdo(
&mut self,
cob_id: u16,
mapping: PdoMapping<MAX_PDO_MAPPING>,
transmission: TransmissionType,
) -> Result<()> {
self.tpdos
.push(TpdoSlot {
cob_id,
mapping,
transmission,
})
.map_err(|_| Error::MappingFull)
}
pub fn configure_pdos_from_od(&mut self) {
self.rpdos.clear();
self.tpdos.clear();
for n in 0..MAX_PDOS as u16 {
if let Some(slot) = build_rpdo(&self.od, n) {
let _ = self.rpdos.push(slot);
}
if let Some(slot) = build_tpdo(&self.od, n) {
let _ = self.tpdos.push(slot);
}
}
}
pub fn take_written_object(&mut self) -> Option<crate::object_dictionary::Address> {
self.sdo.take_write()
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn state(&self) -> NmtState {
self.nmt.state()
}
pub fn od(&self) -> &ObjectDictionary<N> {
&self.od
}
pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
&mut self.od
}
pub fn boot(&mut self) -> TxFrame {
self.nmt.boot();
self.guard_toggle = false;
TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
}
pub fn node_guard_response(&mut self) -> TxFrame {
let byte = nmt::encode_node_guard(self.nmt.state(), self.guard_toggle);
self.guard_toggle = !self.guard_toggle;
TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &[byte])
}
pub fn heartbeat(&self) -> TxFrame {
TxFrame::new(
nmt::heartbeat_cob_id(self.node_id),
&nmt::encode_heartbeat(self.nmt.state()),
)
}
pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
if cob_id == nmt::NMT_COMMAND_COB_ID {
self.on_nmt(data);
None
} else if cob_id == lss::LSS_MASTER_COB_ID {
self.on_lss(data)
} else if cob_id == self.sdo.request_cob_id() {
self.on_sdo(data)
} else {
self.on_rpdo(cob_id, data);
None
}
}
fn on_lss(&mut self, data: &[u8]) -> Option<TxFrame> {
let lss = self.lss.as_mut()?;
if data.len() > 8 {
return None;
}
let mut frame: lss::LssFrame = [0u8; 8];
frame[..data.len()].copy_from_slice(data);
lss.handle(&frame)
.map(|resp| TxFrame::new(lss::LSS_SLAVE_COB_ID, &resp))
}
pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
let mut frames = Vec::new();
if self.nmt.state() != NmtState::Operational {
return frames;
}
for slot in &self.tpdos {
if is_synchronous(slot.transmission) {
if let Some(frame) = self.build_tpdo(slot) {
let _ = frames.push(frame);
}
}
}
frames
}
pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
if self.nmt.state() != NmtState::Operational {
return None;
}
self.build_tpdo(self.tpdos.get(index)?)
}
fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
if slot.mapping.is_empty() {
return None;
}
let mut buf = [0u8; 8];
let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
Some(TxFrame::new(slot.cob_id, &buf[..len]))
}
fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
if self.nmt.state() != NmtState::Operational {
return;
}
if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
}
}
fn on_nmt(&mut self, data: &[u8]) {
if data.len() < 2 {
return;
}
if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
if target == NodeId::BROADCAST || target == self.node_id {
self.nmt.apply(command);
}
}
}
fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
if !matches!(
self.nmt.state(),
NmtState::PreOperational | NmtState::Operational
) {
return None;
}
let mut payload: sdo::SdoPayload = [0u8; 8];
if data.len() > payload.len() {
return None;
}
payload[..data.len()].copy_from_slice(data);
let response = self.sdo.handle(&mut self.od, &payload)?;
Some(TxFrame::new(self.sdo.response_cob_id(), &response))
}
}
fn is_synchronous(transmission: TransmissionType) -> bool {
matches!(
transmission,
TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
)
}
fn od_u32<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u32> {
match od
.read(crate::object_dictionary::Address::new(index, sub))
.ok()?
{
crate::datatypes::Value::Unsigned32(v) => Some(v),
_ => None,
}
}
fn od_u8<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u8> {
match od
.read(crate::object_dictionary::Address::new(index, sub))
.ok()?
{
crate::datatypes::Value::Unsigned8(v) => Some(v),
_ => None,
}
}
fn read_pdo_mapping<const N: usize>(
od: &ObjectDictionary<N>,
map_index: u16,
) -> Option<PdoMapping<MAX_PDO_MAPPING>> {
let count = od_u8(od, map_index, 0)?;
let mut mapping = PdoMapping::new();
for sub in 1..=count {
mapping
.push(pdo::MappingEntry::from_u32(od_u32(od, map_index, sub)?))
.ok()?;
}
Some(mapping)
}
fn build_rpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<RpdoSlot> {
let cob_id = od_u32(od, 0x1400 + n, 1)?;
if !pdo::pdo_is_valid(cob_id) {
return None;
}
Some(RpdoSlot {
cob_id: pdo::pdo_can_id(cob_id),
mapping: read_pdo_mapping(od, 0x1600 + n)?,
})
}
fn build_tpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<TpdoSlot> {
let cob_id = od_u32(od, 0x1800 + n, 1)?;
if !pdo::pdo_is_valid(cob_id) {
return None;
}
let transmission = od_u8(od, 0x1800 + n, 2)
.and_then(|b| TransmissionType::from_byte(b).ok())
.unwrap_or(TransmissionType::EventDrivenProfile);
Some(TpdoSlot {
cob_id: pdo::pdo_can_id(cob_id),
mapping: read_pdo_mapping(od, 0x1A00 + n)?,
transmission,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object_dictionary::{Address, Entry};
use crate::pdo::MappingEntry;
use crate::sdo::{encode_download_expedited, encode_upload_request};
use crate::{DataType, NmtCommand, Value};
fn start<const N: usize>(n: &mut Node<N>) {
n.on_frame(
nmt::NMT_COMMAND_COB_ID,
&[NmtCommand::StartRemoteNode as u8, 0x10],
);
}
fn od() -> ObjectDictionary<8> {
let mut od = ObjectDictionary::new();
od.insert(
Address::new(0x1000, 0),
Entry::constant(Value::Unsigned32(0x192)),
)
.unwrap();
od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
.unwrap();
od
}
fn node() -> Node<8> {
Node::new(NodeId::new(0x10).unwrap(), od())
}
#[test]
fn boots_from_init_to_preop_and_announces() {
let mut n = node();
assert_eq!(n.state(), NmtState::Initialising);
let boot = n.boot();
assert_eq!(n.state(), NmtState::PreOperational);
assert_eq!(boot.cob_id, 0x710); assert_eq!(boot.data(), &[0x00]);
}
#[test]
fn heartbeat_reflects_state() {
let mut n = node();
n.boot();
assert_eq!(n.heartbeat().data(), &[0x7F]); n.on_frame(
nmt::NMT_COMMAND_COB_ID,
&[NmtCommand::StartRemoteNode as u8, 0x10],
);
assert_eq!(n.state(), NmtState::Operational);
assert_eq!(n.heartbeat().data(), &[0x05]); }
#[test]
fn node_guard_response_toggles() {
let mut n = node();
n.boot();
start(&mut n); let first = n.node_guard_response();
assert_eq!(first.cob_id, 0x710); assert_eq!(first.data(), &[0x05]); assert_eq!(n.node_guard_response().data(), &[0x85]); assert_eq!(n.node_guard_response().data(), &[0x05]); }
#[test]
fn serves_sdo_read_when_preoperational() {
let mut n = node();
n.boot();
let req = encode_upload_request(Address::new(0x1000, 0));
let resp = n.on_frame(0x610, &req).expect("SDO response");
assert_eq!(resp.cob_id, 0x590); let (_, value) = crate::sdo::decode_upload_expedited_response(
resp.data().try_into().unwrap(),
DataType::Unsigned32,
)
.unwrap();
assert_eq!(value, Value::Unsigned32(0x192));
}
#[test]
fn ignores_sdo_before_boot() {
let mut n = node(); let req = encode_upload_request(Address::new(0x1000, 0));
assert!(n.on_frame(0x610, &req).is_none());
}
#[test]
fn ignores_sdo_when_stopped() {
let mut n = node();
n.boot();
n.on_frame(
nmt::NMT_COMMAND_COB_ID,
&[NmtCommand::StopRemoteNode as u8, 0x10],
);
assert_eq!(n.state(), NmtState::Stopped);
let req = encode_upload_request(Address::new(0x1000, 0));
assert!(n.on_frame(0x610, &req).is_none());
}
#[test]
fn nmt_command_for_other_node_is_ignored() {
let mut n = node();
n.boot();
n.on_frame(
nmt::NMT_COMMAND_COB_ID,
&[NmtCommand::StartRemoteNode as u8, 0x20],
);
assert_eq!(n.state(), NmtState::PreOperational); }
#[test]
fn broadcast_nmt_applies() {
let mut n = node();
n.boot();
n.on_frame(
nmt::NMT_COMMAND_COB_ID,
&[NmtCommand::StartRemoteNode as u8, 0x00],
);
assert_eq!(n.state(), NmtState::Operational);
}
#[test]
fn serves_sdo_write_and_updates_od() {
let mut n = node();
n.boot();
let req =
encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
assert!(n.on_frame(0x610, &req).is_some());
assert_eq!(
n.od().read(Address::new(0x1017, 0)).unwrap(),
Value::Unsigned16(1234)
);
}
#[test]
fn ignores_unrelated_cob_id() {
let mut n = node();
n.boot();
assert!(n.on_frame(0x123, &[0; 8]).is_none());
}
fn pdo_od() -> ObjectDictionary<8> {
let mut od = ObjectDictionary::new();
od.insert(
Address::new(0x6000, 1),
Entry::rw(Value::Unsigned16(0xBEEF)),
)
.unwrap();
od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
.unwrap();
od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
.unwrap();
od
}
fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
let mut m = PdoMapping::new();
for &(index, sub, bits) in entries {
m.push(MappingEntry::new(index, sub, bits)).unwrap();
}
m
}
#[test]
fn tpdo_transmits_only_when_operational() {
let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
n.add_tpdo(
0x18A,
mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
TransmissionType::SynchronousAcyclic,
)
.unwrap();
n.boot();
assert!(n.sync_tpdos().is_empty());
start(&mut n);
let frames = n.sync_tpdos();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].cob_id, 0x18A);
assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
}
#[test]
fn event_tpdo_by_index() {
let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
n.add_tpdo(
0x18A,
mapping(&[(0x6000, 2, 8)]),
TransmissionType::EventDrivenProfile,
)
.unwrap();
n.boot();
start(&mut n);
assert!(n.sync_tpdos().is_empty());
assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
assert!(n.tpdo(1).is_none());
}
#[test]
fn rpdo_applies_only_when_operational() {
let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
n.boot();
assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
assert_eq!(
n.od().read(Address::new(0x6200, 1)).unwrap(),
Value::Unsigned16(0)
);
start(&mut n);
n.on_frame(0x20A, &[0x34, 0x12]);
assert_eq!(
n.od().read(Address::new(0x6200, 1)).unwrap(),
Value::Unsigned16(0x1234)
);
}
#[test]
fn pdo_capacity_is_enforced() {
let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
for _ in 0..MAX_PDOS {
n.add_tpdo(
0x18A,
mapping(&[(0x6000, 2, 8)]),
TransmissionType::SynchronousAcyclic,
)
.unwrap();
}
assert_eq!(
n.add_tpdo(
0x18A,
mapping(&[(0x6000, 2, 8)]),
TransmissionType::SynchronousAcyclic
),
Err(Error::MappingFull)
);
}
use crate::lss::{self, encode_configure_node_id, encode_switch_global, LssAddress, LssState};
fn lss_address() -> LssAddress {
LssAddress {
vendor_id: 0x1F,
product_code: 0x2A,
revision_number: 1,
serial_number: 0x99,
}
}
#[test]
fn routes_lss_frames_when_enabled() {
let mut n = node();
n.enable_lss(lss_address());
assert!(n
.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
.is_none());
assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
}
#[test]
fn lss_frames_ignored_when_disabled() {
let mut n = node(); assert!(n
.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
.is_none());
assert!(n.lss().is_none());
}
#[test]
fn lss_assigns_node_id_and_moves_sdo_cob_id() {
let mut n = Node::new(NodeId::new(1).unwrap(), od());
n.enable_lss(lss_address());
assert_eq!(n.node_id(), NodeId::new(1).unwrap());
n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
let resp = n
.on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x20))
.expect("configure response");
assert_eq!(resp.cob_id, lss::LSS_SLAVE_COB_ID);
assert_eq!(&resp.data()[..2], &[0x11, 0x00]);
assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x20).unwrap()));
assert_eq!(n.node_id(), NodeId::new(0x20).unwrap());
n.boot();
let req = encode_upload_request(Address::new(0x1000, 0));
assert!(n.on_frame(0x601, &req).is_none()); assert!(n.on_frame(0x620, &req).is_some()); }
#[test]
fn configures_pdos_from_od() {
let mut od = ObjectDictionary::<16>::new();
od.insert(Address::new(0x1400, 1), Entry::rw(Value::Unsigned32(0x210)))
.unwrap();
od.insert(Address::new(0x1600, 0), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(
Address::new(0x1600, 1),
Entry::rw(Value::Unsigned32(MappingEntry::new(0x6200, 1, 16).to_u32())),
)
.unwrap();
od.insert(Address::new(0x1800, 1), Entry::rw(Value::Unsigned32(0x190)))
.unwrap();
od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
.unwrap(); od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(
Address::new(0x1A00, 1),
Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
)
.unwrap();
od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
.unwrap();
od.insert(
Address::new(0x6000, 1),
Entry::rw(Value::Unsigned16(0xBEEF)),
)
.unwrap();
let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
n.configure_pdos_from_od();
n.boot();
start(&mut n);
n.on_frame(0x210, &[0x34, 0x12]);
assert_eq!(
n.od().read(Address::new(0x6200, 1)).unwrap(),
Value::Unsigned16(0x1234)
);
let tpdos = n.sync_tpdos();
assert_eq!(tpdos.len(), 1);
assert_eq!(tpdos[0].cob_id, 0x190);
assert_eq!(tpdos[0].data(), &[0xEF, 0xBE]);
}
#[test]
fn skips_pdo_with_invalid_cob_id() {
let mut od = ObjectDictionary::<8>::new();
od.insert(
Address::new(0x1800, 1),
Entry::rw(Value::Unsigned32(0x8000_0190)),
)
.unwrap();
od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(
Address::new(0x1A00, 1),
Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
)
.unwrap();
od.insert(
Address::new(0x6000, 1),
Entry::rw(Value::Unsigned16(0xBEEF)),
)
.unwrap();
let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
n.configure_pdos_from_od();
n.boot();
start(&mut n);
assert!(n.sync_tpdos().is_empty()); }
#[test]
fn reacts_to_a_pdo_parameter_write() {
let mut od = ObjectDictionary::<16>::new();
od.insert(
Address::new(0x1800, 1),
Entry::rw(Value::Unsigned32(0x8000_0190)),
)
.unwrap();
od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
.unwrap();
od.insert(
Address::new(0x1A00, 1),
Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
)
.unwrap();
od.insert(
Address::new(0x6000, 1),
Entry::rw(Value::Unsigned16(0xBEEF)),
)
.unwrap();
let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
n.configure_pdos_from_od();
n.boot();
start(&mut n);
assert!(n.sync_tpdos().is_empty());
let req =
encode_download_expedited(Address::new(0x1800, 1), &Value::Unsigned32(0x190)).unwrap();
n.on_frame(0x610, &req);
let written = n.take_written_object().expect("a write was recorded");
assert_eq!(written, Address::new(0x1800, 1));
assert_eq!(n.take_written_object(), None); if (0x1400..=0x1BFF).contains(&written.index) {
n.configure_pdos_from_od();
}
let tpdos = n.sync_tpdos();
assert_eq!(tpdos.len(), 1);
assert_eq!(tpdos[0].cob_id, 0x190);
}
}