1use heapless::Vec;
31
32use crate::lss::{self, LssAddress, LssSlave};
33use crate::nmt::{self, NmtState, NmtStateMachine};
34use crate::object_dictionary::ObjectDictionary;
35use crate::pdo::{self, PdoMapping, TransmissionType};
36use crate::sdo::{self, SdoServer};
37use crate::types::NodeId;
38use crate::{Error, Result};
39
40pub const MAX_PDOS: usize = 4;
43
44pub const MAX_PDO_MAPPING: usize = 8;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct TxFrame {
51 pub cob_id: u16,
53 data: [u8; 8],
54 len: u8,
55}
56
57impl TxFrame {
58 fn new(cob_id: u16, bytes: &[u8]) -> Self {
59 let len = bytes.len().min(8);
60 let mut data = [0u8; 8];
61 data[..len].copy_from_slice(&bytes[..len]);
62 Self {
63 cob_id,
64 data,
65 len: len as u8,
66 }
67 }
68
69 pub fn data(&self) -> &[u8] {
71 &self.data[..self.len as usize]
72 }
73}
74
75#[derive(Debug)]
77struct RpdoSlot {
78 cob_id: u16,
79 mapping: PdoMapping<MAX_PDO_MAPPING>,
80}
81
82#[derive(Debug)]
84struct TpdoSlot {
85 cob_id: u16,
86 mapping: PdoMapping<MAX_PDO_MAPPING>,
87 transmission: TransmissionType,
88}
89
90#[derive(Debug)]
93pub struct Node<const N: usize> {
94 node_id: NodeId,
95 od: ObjectDictionary<N>,
96 sdo: SdoServer,
97 nmt: NmtStateMachine,
98 rpdos: Vec<RpdoSlot, MAX_PDOS>,
99 tpdos: Vec<TpdoSlot, MAX_PDOS>,
100 lss: Option<LssSlave>,
101}
102
103impl<const N: usize> Node<N> {
104 pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
107 Self {
108 node_id,
109 od,
110 sdo: SdoServer::new(node_id),
111 nmt: NmtStateMachine::new(),
112 rpdos: Vec::new(),
113 tpdos: Vec::new(),
114 lss: None,
115 }
116 }
117
118 pub fn enable_lss(&mut self, address: LssAddress) {
126 self.lss = Some(LssSlave::new(address, self.node_id.raw()));
127 }
128
129 pub fn set_node_id(&mut self, node_id: NodeId) {
132 self.node_id = node_id;
133 self.sdo = SdoServer::new(node_id);
134 }
135
136 pub fn apply_lss_node_id(&mut self) -> Option<NodeId> {
140 let pending = self.lss.as_ref()?.pending_node_id();
141 let node_id = NodeId::new(pending).ok()?;
142 self.set_node_id(node_id);
143 if let Some(lss) = &mut self.lss {
144 lss.adopt_pending();
145 }
146 Some(node_id)
147 }
148
149 pub fn lss(&self) -> Option<&LssSlave> {
151 self.lss.as_ref()
152 }
153
154 pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
159 self.rpdos
160 .push(RpdoSlot { cob_id, mapping })
161 .map_err(|_| Error::MappingFull)
162 }
163
164 pub fn add_tpdo(
169 &mut self,
170 cob_id: u16,
171 mapping: PdoMapping<MAX_PDO_MAPPING>,
172 transmission: TransmissionType,
173 ) -> Result<()> {
174 self.tpdos
175 .push(TpdoSlot {
176 cob_id,
177 mapping,
178 transmission,
179 })
180 .map_err(|_| Error::MappingFull)
181 }
182
183 pub fn node_id(&self) -> NodeId {
185 self.node_id
186 }
187
188 pub fn state(&self) -> NmtState {
190 self.nmt.state()
191 }
192
193 pub fn od(&self) -> &ObjectDictionary<N> {
195 &self.od
196 }
197
198 pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
200 &mut self.od
201 }
202
203 pub fn boot(&mut self) -> TxFrame {
206 self.nmt.boot();
207 TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
208 }
209
210 pub fn heartbeat(&self) -> TxFrame {
213 TxFrame::new(
214 nmt::heartbeat_cob_id(self.node_id),
215 &nmt::encode_heartbeat(self.nmt.state()),
216 )
217 }
218
219 pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
227 if cob_id == nmt::NMT_COMMAND_COB_ID {
228 self.on_nmt(data);
229 None
230 } else if cob_id == lss::LSS_MASTER_COB_ID {
231 self.on_lss(data)
232 } else if cob_id == self.sdo.request_cob_id() {
233 self.on_sdo(data)
234 } else {
235 self.on_rpdo(cob_id, data);
236 None
237 }
238 }
239
240 fn on_lss(&mut self, data: &[u8]) -> Option<TxFrame> {
241 let lss = self.lss.as_mut()?;
242 if data.len() > 8 {
243 return None;
244 }
245 let mut frame: lss::LssFrame = [0u8; 8];
246 frame[..data.len()].copy_from_slice(data);
247 lss.handle(&frame)
248 .map(|resp| TxFrame::new(lss::LSS_SLAVE_COB_ID, &resp))
249 }
250
251 pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
257 let mut frames = Vec::new();
258 if self.nmt.state() != NmtState::Operational {
259 return frames;
260 }
261 for slot in &self.tpdos {
262 if is_synchronous(slot.transmission) {
263 if let Some(frame) = self.build_tpdo(slot) {
264 let _ = frames.push(frame);
266 }
267 }
268 }
269 frames
270 }
271
272 pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
275 if self.nmt.state() != NmtState::Operational {
276 return None;
277 }
278 self.build_tpdo(self.tpdos.get(index)?)
279 }
280
281 fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
282 if slot.mapping.is_empty() {
283 return None;
284 }
285 let mut buf = [0u8; 8];
286 let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
287 Some(TxFrame::new(slot.cob_id, &buf[..len]))
288 }
289
290 fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
291 if self.nmt.state() != NmtState::Operational {
293 return;
294 }
295 if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
296 let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
298 }
299 }
300
301 fn on_nmt(&mut self, data: &[u8]) {
302 if data.len() < 2 {
304 return;
305 }
306 if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
307 if target == NodeId::BROADCAST || target == self.node_id {
308 self.nmt.apply(command);
309 }
310 }
311 }
312
313 fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
314 if !matches!(
316 self.nmt.state(),
317 NmtState::PreOperational | NmtState::Operational
318 ) {
319 return None;
320 }
321 let mut payload: sdo::SdoPayload = [0u8; 8];
322 if data.len() > payload.len() {
323 return None;
324 }
325 payload[..data.len()].copy_from_slice(data);
326 let response = self.sdo.handle(&mut self.od, &payload)?;
327 Some(TxFrame::new(self.sdo.response_cob_id(), &response))
328 }
329}
330
331fn is_synchronous(transmission: TransmissionType) -> bool {
333 matches!(
334 transmission,
335 TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
336 )
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::object_dictionary::{Address, Entry};
343 use crate::pdo::MappingEntry;
344 use crate::sdo::{encode_download_expedited, encode_upload_request};
345 use crate::{DataType, NmtCommand, Value};
346
347 fn start(n: &mut Node<8>) {
348 n.on_frame(
349 nmt::NMT_COMMAND_COB_ID,
350 &[NmtCommand::StartRemoteNode as u8, 0x10],
351 );
352 }
353
354 fn od() -> ObjectDictionary<8> {
355 let mut od = ObjectDictionary::new();
356 od.insert(
357 Address::new(0x1000, 0),
358 Entry::constant(Value::Unsigned32(0x192)),
359 )
360 .unwrap();
361 od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
362 .unwrap();
363 od
364 }
365
366 fn node() -> Node<8> {
367 Node::new(NodeId::new(0x10).unwrap(), od())
368 }
369
370 #[test]
371 fn boots_from_init_to_preop_and_announces() {
372 let mut n = node();
373 assert_eq!(n.state(), NmtState::Initialising);
374 let boot = n.boot();
375 assert_eq!(n.state(), NmtState::PreOperational);
376 assert_eq!(boot.cob_id, 0x710); assert_eq!(boot.data(), &[0x00]);
378 }
379
380 #[test]
381 fn heartbeat_reflects_state() {
382 let mut n = node();
383 n.boot();
384 assert_eq!(n.heartbeat().data(), &[0x7F]); n.on_frame(
386 nmt::NMT_COMMAND_COB_ID,
387 &[NmtCommand::StartRemoteNode as u8, 0x10],
388 );
389 assert_eq!(n.state(), NmtState::Operational);
390 assert_eq!(n.heartbeat().data(), &[0x05]); }
392
393 #[test]
394 fn serves_sdo_read_when_preoperational() {
395 let mut n = node();
396 n.boot();
397 let req = encode_upload_request(Address::new(0x1000, 0));
398 let resp = n.on_frame(0x610, &req).expect("SDO response");
399 assert_eq!(resp.cob_id, 0x590); let (_, value) = crate::sdo::decode_upload_expedited_response(
401 resp.data().try_into().unwrap(),
402 DataType::Unsigned32,
403 )
404 .unwrap();
405 assert_eq!(value, Value::Unsigned32(0x192));
406 }
407
408 #[test]
409 fn ignores_sdo_before_boot() {
410 let mut n = node(); let req = encode_upload_request(Address::new(0x1000, 0));
412 assert!(n.on_frame(0x610, &req).is_none());
413 }
414
415 #[test]
416 fn ignores_sdo_when_stopped() {
417 let mut n = node();
418 n.boot();
419 n.on_frame(
420 nmt::NMT_COMMAND_COB_ID,
421 &[NmtCommand::StopRemoteNode as u8, 0x10],
422 );
423 assert_eq!(n.state(), NmtState::Stopped);
424 let req = encode_upload_request(Address::new(0x1000, 0));
425 assert!(n.on_frame(0x610, &req).is_none());
426 }
427
428 #[test]
429 fn nmt_command_for_other_node_is_ignored() {
430 let mut n = node();
431 n.boot();
432 n.on_frame(
434 nmt::NMT_COMMAND_COB_ID,
435 &[NmtCommand::StartRemoteNode as u8, 0x20],
436 );
437 assert_eq!(n.state(), NmtState::PreOperational); }
439
440 #[test]
441 fn broadcast_nmt_applies() {
442 let mut n = node();
443 n.boot();
444 n.on_frame(
445 nmt::NMT_COMMAND_COB_ID,
446 &[NmtCommand::StartRemoteNode as u8, 0x00],
447 );
448 assert_eq!(n.state(), NmtState::Operational);
449 }
450
451 #[test]
452 fn serves_sdo_write_and_updates_od() {
453 let mut n = node();
454 n.boot();
455 let req =
456 encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
457 assert!(n.on_frame(0x610, &req).is_some());
458 assert_eq!(
459 n.od().read(Address::new(0x1017, 0)).unwrap(),
460 Value::Unsigned16(1234)
461 );
462 }
463
464 #[test]
465 fn ignores_unrelated_cob_id() {
466 let mut n = node();
467 n.boot();
468 assert!(n.on_frame(0x123, &[0; 8]).is_none());
469 }
470
471 fn pdo_od() -> ObjectDictionary<8> {
473 let mut od = ObjectDictionary::new();
474 od.insert(
476 Address::new(0x6000, 1),
477 Entry::rw(Value::Unsigned16(0xBEEF)),
478 )
479 .unwrap();
480 od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
481 .unwrap();
482 od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
483 .unwrap();
484 od
485 }
486
487 fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
488 let mut m = PdoMapping::new();
489 for &(index, sub, bits) in entries {
490 m.push(MappingEntry::new(index, sub, bits)).unwrap();
491 }
492 m
493 }
494
495 #[test]
496 fn tpdo_transmits_only_when_operational() {
497 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
498 n.add_tpdo(
499 0x18A,
500 mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
501 TransmissionType::SynchronousAcyclic,
502 )
503 .unwrap();
504 n.boot();
505
506 assert!(n.sync_tpdos().is_empty());
508
509 start(&mut n);
510 let frames = n.sync_tpdos();
511 assert_eq!(frames.len(), 1);
512 assert_eq!(frames[0].cob_id, 0x18A);
513 assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
515 }
516
517 #[test]
518 fn event_tpdo_by_index() {
519 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
520 n.add_tpdo(
522 0x18A,
523 mapping(&[(0x6000, 2, 8)]),
524 TransmissionType::EventDrivenProfile,
525 )
526 .unwrap();
527 n.boot();
528 start(&mut n);
529 assert!(n.sync_tpdos().is_empty());
530 assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
531 assert!(n.tpdo(1).is_none());
532 }
533
534 #[test]
535 fn rpdo_applies_only_when_operational() {
536 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
537 n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
538 n.boot();
539
540 assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
542 assert_eq!(
543 n.od().read(Address::new(0x6200, 1)).unwrap(),
544 Value::Unsigned16(0)
545 );
546
547 start(&mut n);
549 n.on_frame(0x20A, &[0x34, 0x12]);
550 assert_eq!(
551 n.od().read(Address::new(0x6200, 1)).unwrap(),
552 Value::Unsigned16(0x1234)
553 );
554 }
555
556 #[test]
557 fn pdo_capacity_is_enforced() {
558 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
559 for _ in 0..MAX_PDOS {
560 n.add_tpdo(
561 0x18A,
562 mapping(&[(0x6000, 2, 8)]),
563 TransmissionType::SynchronousAcyclic,
564 )
565 .unwrap();
566 }
567 assert_eq!(
568 n.add_tpdo(
569 0x18A,
570 mapping(&[(0x6000, 2, 8)]),
571 TransmissionType::SynchronousAcyclic
572 ),
573 Err(Error::MappingFull)
574 );
575 }
576
577 use crate::lss::{self, encode_configure_node_id, encode_switch_global, LssAddress, LssState};
579
580 fn lss_address() -> LssAddress {
581 LssAddress {
582 vendor_id: 0x1F,
583 product_code: 0x2A,
584 revision_number: 1,
585 serial_number: 0x99,
586 }
587 }
588
589 #[test]
590 fn routes_lss_frames_when_enabled() {
591 let mut n = node();
592 n.enable_lss(lss_address());
593 assert!(n
595 .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
596 .is_none());
597 assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
598 }
599
600 #[test]
601 fn lss_frames_ignored_when_disabled() {
602 let mut n = node(); assert!(n
604 .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
605 .is_none());
606 assert!(n.lss().is_none());
607 }
608
609 #[test]
610 fn lss_assigns_node_id_and_moves_sdo_cob_id() {
611 let mut n = Node::new(NodeId::new(1).unwrap(), od());
614 n.enable_lss(lss_address());
615 assert_eq!(n.node_id(), NodeId::new(1).unwrap());
616
617 n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
619 let resp = n
620 .on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x20))
621 .expect("configure response");
622 assert_eq!(resp.cob_id, lss::LSS_SLAVE_COB_ID);
623 assert_eq!(&resp.data()[..2], &[0x11, 0x00]); assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x20).unwrap()));
627 assert_eq!(n.node_id(), NodeId::new(0x20).unwrap());
628
629 n.boot();
630 let req = encode_upload_request(Address::new(0x1000, 0));
631 assert!(n.on_frame(0x601, &req).is_none()); assert!(n.on_frame(0x620, &req).is_some()); }
634}