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 guard_toggle: bool,
102}
103
104impl<const N: usize> Node<N> {
105 pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
108 Self {
109 node_id,
110 od,
111 sdo: SdoServer::new(node_id),
112 nmt: NmtStateMachine::new(),
113 rpdos: Vec::new(),
114 tpdos: Vec::new(),
115 lss: None,
116 guard_toggle: false,
117 }
118 }
119
120 pub fn enable_lss(&mut self, address: LssAddress) {
128 self.lss = Some(LssSlave::new(address, self.node_id.raw()));
129 }
130
131 pub fn set_node_id(&mut self, node_id: NodeId) {
134 self.node_id = node_id;
135 self.sdo = SdoServer::new(node_id);
136 }
137
138 pub fn apply_lss_node_id(&mut self) -> Option<NodeId> {
142 let pending = self.lss.as_ref()?.pending_node_id();
143 let node_id = NodeId::new(pending).ok()?;
144 self.set_node_id(node_id);
145 if let Some(lss) = &mut self.lss {
146 lss.adopt_pending();
147 }
148 Some(node_id)
149 }
150
151 pub fn lss(&self) -> Option<&LssSlave> {
153 self.lss.as_ref()
154 }
155
156 pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
161 self.rpdos
162 .push(RpdoSlot { cob_id, mapping })
163 .map_err(|_| Error::MappingFull)
164 }
165
166 pub fn add_tpdo(
171 &mut self,
172 cob_id: u16,
173 mapping: PdoMapping<MAX_PDO_MAPPING>,
174 transmission: TransmissionType,
175 ) -> Result<()> {
176 self.tpdos
177 .push(TpdoSlot {
178 cob_id,
179 mapping,
180 transmission,
181 })
182 .map_err(|_| Error::MappingFull)
183 }
184
185 pub fn configure_pdos_from_od(&mut self) {
195 self.rpdos.clear();
196 self.tpdos.clear();
197 for n in 0..MAX_PDOS as u16 {
198 if let Some(slot) = build_rpdo(&self.od, n) {
199 let _ = self.rpdos.push(slot);
200 }
201 if let Some(slot) = build_tpdo(&self.od, n) {
202 let _ = self.tpdos.push(slot);
203 }
204 }
205 }
206
207 pub fn take_written_object(&mut self) -> Option<crate::object_dictionary::Address> {
224 self.sdo.take_write()
225 }
226
227 pub fn node_id(&self) -> NodeId {
229 self.node_id
230 }
231
232 pub fn state(&self) -> NmtState {
234 self.nmt.state()
235 }
236
237 pub fn od(&self) -> &ObjectDictionary<N> {
239 &self.od
240 }
241
242 pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
244 &mut self.od
245 }
246
247 pub fn boot(&mut self) -> TxFrame {
250 self.nmt.boot();
251 self.guard_toggle = false;
252 TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
253 }
254
255 pub fn node_guard_response(&mut self) -> TxFrame {
260 let byte = nmt::encode_node_guard(self.nmt.state(), self.guard_toggle);
261 self.guard_toggle = !self.guard_toggle;
262 TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &[byte])
263 }
264
265 pub fn heartbeat(&self) -> TxFrame {
268 TxFrame::new(
269 nmt::heartbeat_cob_id(self.node_id),
270 &nmt::encode_heartbeat(self.nmt.state()),
271 )
272 }
273
274 pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
282 if cob_id == nmt::NMT_COMMAND_COB_ID {
283 self.on_nmt(data);
284 None
285 } else if cob_id == lss::LSS_MASTER_COB_ID {
286 self.on_lss(data)
287 } else if cob_id == self.sdo.request_cob_id() {
288 self.on_sdo(data)
289 } else {
290 self.on_rpdo(cob_id, data);
291 None
292 }
293 }
294
295 fn on_lss(&mut self, data: &[u8]) -> Option<TxFrame> {
296 let lss = self.lss.as_mut()?;
297 if data.len() > 8 {
298 return None;
299 }
300 let mut frame: lss::LssFrame = [0u8; 8];
301 frame[..data.len()].copy_from_slice(data);
302 lss.handle(&frame)
303 .map(|resp| TxFrame::new(lss::LSS_SLAVE_COB_ID, &resp))
304 }
305
306 pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
312 let mut frames = Vec::new();
313 if self.nmt.state() != NmtState::Operational {
314 return frames;
315 }
316 for slot in &self.tpdos {
317 if is_synchronous(slot.transmission) {
318 if let Some(frame) = self.build_tpdo(slot) {
319 let _ = frames.push(frame);
321 }
322 }
323 }
324 frames
325 }
326
327 pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
330 if self.nmt.state() != NmtState::Operational {
331 return None;
332 }
333 self.build_tpdo(self.tpdos.get(index)?)
334 }
335
336 fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
337 if slot.mapping.is_empty() {
338 return None;
339 }
340 let mut buf = [0u8; 8];
341 let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
342 Some(TxFrame::new(slot.cob_id, &buf[..len]))
343 }
344
345 fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
346 if self.nmt.state() != NmtState::Operational {
348 return;
349 }
350 if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
351 let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
353 }
354 }
355
356 fn on_nmt(&mut self, data: &[u8]) {
357 if data.len() < 2 {
359 return;
360 }
361 if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
362 if target == NodeId::BROADCAST || target == self.node_id {
363 self.nmt.apply(command);
364 }
365 }
366 }
367
368 fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
369 if !matches!(
371 self.nmt.state(),
372 NmtState::PreOperational | NmtState::Operational
373 ) {
374 return None;
375 }
376 let mut payload: sdo::SdoPayload = [0u8; 8];
377 if data.len() > payload.len() {
378 return None;
379 }
380 payload[..data.len()].copy_from_slice(data);
381 let response = self.sdo.handle(&mut self.od, &payload)?;
382 Some(TxFrame::new(self.sdo.response_cob_id(), &response))
383 }
384}
385
386fn is_synchronous(transmission: TransmissionType) -> bool {
388 matches!(
389 transmission,
390 TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
391 )
392}
393
394fn od_u32<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u32> {
397 match od
398 .read(crate::object_dictionary::Address::new(index, sub))
399 .ok()?
400 {
401 crate::datatypes::Value::Unsigned32(v) => Some(v),
402 _ => None,
403 }
404}
405
406fn od_u8<const N: usize>(od: &ObjectDictionary<N>, index: u16, sub: u8) -> Option<u8> {
407 match od
408 .read(crate::object_dictionary::Address::new(index, sub))
409 .ok()?
410 {
411 crate::datatypes::Value::Unsigned8(v) => Some(v),
412 _ => None,
413 }
414}
415
416fn read_pdo_mapping<const N: usize>(
419 od: &ObjectDictionary<N>,
420 map_index: u16,
421) -> Option<PdoMapping<MAX_PDO_MAPPING>> {
422 let count = od_u8(od, map_index, 0)?;
423 let mut mapping = PdoMapping::new();
424 for sub in 1..=count {
425 mapping
426 .push(pdo::MappingEntry::from_u32(od_u32(od, map_index, sub)?))
427 .ok()?;
428 }
429 Some(mapping)
430}
431
432fn build_rpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<RpdoSlot> {
433 let cob_id = od_u32(od, 0x1400 + n, 1)?;
434 if !pdo::pdo_is_valid(cob_id) {
435 return None;
436 }
437 Some(RpdoSlot {
438 cob_id: pdo::pdo_can_id(cob_id),
439 mapping: read_pdo_mapping(od, 0x1600 + n)?,
440 })
441}
442
443fn build_tpdo<const N: usize>(od: &ObjectDictionary<N>, n: u16) -> Option<TpdoSlot> {
444 let cob_id = od_u32(od, 0x1800 + n, 1)?;
445 if !pdo::pdo_is_valid(cob_id) {
446 return None;
447 }
448 let transmission = od_u8(od, 0x1800 + n, 2)
450 .and_then(|b| TransmissionType::from_byte(b).ok())
451 .unwrap_or(TransmissionType::EventDrivenProfile);
452 Some(TpdoSlot {
453 cob_id: pdo::pdo_can_id(cob_id),
454 mapping: read_pdo_mapping(od, 0x1A00 + n)?,
455 transmission,
456 })
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::object_dictionary::{Address, Entry};
463 use crate::pdo::MappingEntry;
464 use crate::sdo::{encode_download_expedited, encode_upload_request};
465 use crate::{DataType, NmtCommand, Value};
466
467 fn start<const N: usize>(n: &mut Node<N>) {
468 n.on_frame(
469 nmt::NMT_COMMAND_COB_ID,
470 &[NmtCommand::StartRemoteNode as u8, 0x10],
471 );
472 }
473
474 fn od() -> ObjectDictionary<8> {
475 let mut od = ObjectDictionary::new();
476 od.insert(
477 Address::new(0x1000, 0),
478 Entry::constant(Value::Unsigned32(0x192)),
479 )
480 .unwrap();
481 od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
482 .unwrap();
483 od
484 }
485
486 fn node() -> Node<8> {
487 Node::new(NodeId::new(0x10).unwrap(), od())
488 }
489
490 #[test]
491 fn boots_from_init_to_preop_and_announces() {
492 let mut n = node();
493 assert_eq!(n.state(), NmtState::Initialising);
494 let boot = n.boot();
495 assert_eq!(n.state(), NmtState::PreOperational);
496 assert_eq!(boot.cob_id, 0x710); assert_eq!(boot.data(), &[0x00]);
498 }
499
500 #[test]
501 fn heartbeat_reflects_state() {
502 let mut n = node();
503 n.boot();
504 assert_eq!(n.heartbeat().data(), &[0x7F]); n.on_frame(
506 nmt::NMT_COMMAND_COB_ID,
507 &[NmtCommand::StartRemoteNode as u8, 0x10],
508 );
509 assert_eq!(n.state(), NmtState::Operational);
510 assert_eq!(n.heartbeat().data(), &[0x05]); }
512
513 #[test]
514 fn node_guard_response_toggles() {
515 let mut n = node();
516 n.boot();
517 start(&mut n); let first = n.node_guard_response();
519 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]); }
524
525 #[test]
526 fn serves_sdo_read_when_preoperational() {
527 let mut n = node();
528 n.boot();
529 let req = encode_upload_request(Address::new(0x1000, 0));
530 let resp = n.on_frame(0x610, &req).expect("SDO response");
531 assert_eq!(resp.cob_id, 0x590); let (_, value) = crate::sdo::decode_upload_expedited_response(
533 resp.data().try_into().unwrap(),
534 DataType::Unsigned32,
535 )
536 .unwrap();
537 assert_eq!(value, Value::Unsigned32(0x192));
538 }
539
540 #[test]
541 fn ignores_sdo_before_boot() {
542 let mut n = node(); let req = encode_upload_request(Address::new(0x1000, 0));
544 assert!(n.on_frame(0x610, &req).is_none());
545 }
546
547 #[test]
548 fn ignores_sdo_when_stopped() {
549 let mut n = node();
550 n.boot();
551 n.on_frame(
552 nmt::NMT_COMMAND_COB_ID,
553 &[NmtCommand::StopRemoteNode as u8, 0x10],
554 );
555 assert_eq!(n.state(), NmtState::Stopped);
556 let req = encode_upload_request(Address::new(0x1000, 0));
557 assert!(n.on_frame(0x610, &req).is_none());
558 }
559
560 #[test]
561 fn nmt_command_for_other_node_is_ignored() {
562 let mut n = node();
563 n.boot();
564 n.on_frame(
566 nmt::NMT_COMMAND_COB_ID,
567 &[NmtCommand::StartRemoteNode as u8, 0x20],
568 );
569 assert_eq!(n.state(), NmtState::PreOperational); }
571
572 #[test]
573 fn broadcast_nmt_applies() {
574 let mut n = node();
575 n.boot();
576 n.on_frame(
577 nmt::NMT_COMMAND_COB_ID,
578 &[NmtCommand::StartRemoteNode as u8, 0x00],
579 );
580 assert_eq!(n.state(), NmtState::Operational);
581 }
582
583 #[test]
584 fn serves_sdo_write_and_updates_od() {
585 let mut n = node();
586 n.boot();
587 let req =
588 encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
589 assert!(n.on_frame(0x610, &req).is_some());
590 assert_eq!(
591 n.od().read(Address::new(0x1017, 0)).unwrap(),
592 Value::Unsigned16(1234)
593 );
594 }
595
596 #[test]
597 fn ignores_unrelated_cob_id() {
598 let mut n = node();
599 n.boot();
600 assert!(n.on_frame(0x123, &[0; 8]).is_none());
601 }
602
603 fn pdo_od() -> ObjectDictionary<8> {
605 let mut od = ObjectDictionary::new();
606 od.insert(
608 Address::new(0x6000, 1),
609 Entry::rw(Value::Unsigned16(0xBEEF)),
610 )
611 .unwrap();
612 od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
613 .unwrap();
614 od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
615 .unwrap();
616 od
617 }
618
619 fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
620 let mut m = PdoMapping::new();
621 for &(index, sub, bits) in entries {
622 m.push(MappingEntry::new(index, sub, bits)).unwrap();
623 }
624 m
625 }
626
627 #[test]
628 fn tpdo_transmits_only_when_operational() {
629 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
630 n.add_tpdo(
631 0x18A,
632 mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
633 TransmissionType::SynchronousAcyclic,
634 )
635 .unwrap();
636 n.boot();
637
638 assert!(n.sync_tpdos().is_empty());
640
641 start(&mut n);
642 let frames = n.sync_tpdos();
643 assert_eq!(frames.len(), 1);
644 assert_eq!(frames[0].cob_id, 0x18A);
645 assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
647 }
648
649 #[test]
650 fn event_tpdo_by_index() {
651 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
652 n.add_tpdo(
654 0x18A,
655 mapping(&[(0x6000, 2, 8)]),
656 TransmissionType::EventDrivenProfile,
657 )
658 .unwrap();
659 n.boot();
660 start(&mut n);
661 assert!(n.sync_tpdos().is_empty());
662 assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
663 assert!(n.tpdo(1).is_none());
664 }
665
666 #[test]
667 fn rpdo_applies_only_when_operational() {
668 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
669 n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
670 n.boot();
671
672 assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
674 assert_eq!(
675 n.od().read(Address::new(0x6200, 1)).unwrap(),
676 Value::Unsigned16(0)
677 );
678
679 start(&mut n);
681 n.on_frame(0x20A, &[0x34, 0x12]);
682 assert_eq!(
683 n.od().read(Address::new(0x6200, 1)).unwrap(),
684 Value::Unsigned16(0x1234)
685 );
686 }
687
688 #[test]
689 fn pdo_capacity_is_enforced() {
690 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
691 for _ in 0..MAX_PDOS {
692 n.add_tpdo(
693 0x18A,
694 mapping(&[(0x6000, 2, 8)]),
695 TransmissionType::SynchronousAcyclic,
696 )
697 .unwrap();
698 }
699 assert_eq!(
700 n.add_tpdo(
701 0x18A,
702 mapping(&[(0x6000, 2, 8)]),
703 TransmissionType::SynchronousAcyclic
704 ),
705 Err(Error::MappingFull)
706 );
707 }
708
709 use crate::lss::{self, encode_configure_node_id, encode_switch_global, LssAddress, LssState};
711
712 fn lss_address() -> LssAddress {
713 LssAddress {
714 vendor_id: 0x1F,
715 product_code: 0x2A,
716 revision_number: 1,
717 serial_number: 0x99,
718 }
719 }
720
721 #[test]
722 fn routes_lss_frames_when_enabled() {
723 let mut n = node();
724 n.enable_lss(lss_address());
725 assert!(n
727 .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
728 .is_none());
729 assert_eq!(n.lss().unwrap().state(), LssState::Configuration);
730 }
731
732 #[test]
733 fn lss_frames_ignored_when_disabled() {
734 let mut n = node(); assert!(n
736 .on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))
737 .is_none());
738 assert!(n.lss().is_none());
739 }
740
741 #[test]
742 fn lss_assigns_node_id_and_moves_sdo_cob_id() {
743 let mut n = Node::new(NodeId::new(1).unwrap(), od());
746 n.enable_lss(lss_address());
747 assert_eq!(n.node_id(), NodeId::new(1).unwrap());
748
749 n.on_frame(lss::LSS_MASTER_COB_ID, &encode_switch_global(true));
751 let resp = n
752 .on_frame(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x20))
753 .expect("configure response");
754 assert_eq!(resp.cob_id, lss::LSS_SLAVE_COB_ID);
755 assert_eq!(&resp.data()[..2], &[0x11, 0x00]); assert_eq!(n.apply_lss_node_id(), Some(NodeId::new(0x20).unwrap()));
759 assert_eq!(n.node_id(), NodeId::new(0x20).unwrap());
760
761 n.boot();
762 let req = encode_upload_request(Address::new(0x1000, 0));
763 assert!(n.on_frame(0x601, &req).is_none()); assert!(n.on_frame(0x620, &req).is_some()); }
766
767 #[test]
769 fn configures_pdos_from_od() {
770 let mut od = ObjectDictionary::<16>::new();
771 od.insert(Address::new(0x1400, 1), Entry::rw(Value::Unsigned32(0x210)))
773 .unwrap();
774 od.insert(Address::new(0x1600, 0), Entry::rw(Value::Unsigned8(1)))
775 .unwrap();
776 od.insert(
777 Address::new(0x1600, 1),
778 Entry::rw(Value::Unsigned32(MappingEntry::new(0x6200, 1, 16).to_u32())),
779 )
780 .unwrap();
781 od.insert(Address::new(0x1800, 1), Entry::rw(Value::Unsigned32(0x190)))
783 .unwrap();
784 od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
785 .unwrap(); od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
787 .unwrap();
788 od.insert(
789 Address::new(0x1A00, 1),
790 Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
791 )
792 .unwrap();
793 od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
795 .unwrap();
796 od.insert(
797 Address::new(0x6000, 1),
798 Entry::rw(Value::Unsigned16(0xBEEF)),
799 )
800 .unwrap();
801
802 let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
803 n.configure_pdos_from_od();
804 n.boot();
805 start(&mut n);
806
807 n.on_frame(0x210, &[0x34, 0x12]);
809 assert_eq!(
810 n.od().read(Address::new(0x6200, 1)).unwrap(),
811 Value::Unsigned16(0x1234)
812 );
813 let tpdos = n.sync_tpdos();
815 assert_eq!(tpdos.len(), 1);
816 assert_eq!(tpdos[0].cob_id, 0x190);
817 assert_eq!(tpdos[0].data(), &[0xEF, 0xBE]);
818 }
819
820 #[test]
821 fn skips_pdo_with_invalid_cob_id() {
822 let mut od = ObjectDictionary::<8>::new();
823 od.insert(
825 Address::new(0x1800, 1),
826 Entry::rw(Value::Unsigned32(0x8000_0190)),
827 )
828 .unwrap();
829 od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
830 .unwrap();
831 od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
832 .unwrap();
833 od.insert(
834 Address::new(0x1A00, 1),
835 Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
836 )
837 .unwrap();
838 od.insert(
839 Address::new(0x6000, 1),
840 Entry::rw(Value::Unsigned16(0xBEEF)),
841 )
842 .unwrap();
843
844 let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
845 n.configure_pdos_from_od();
846 n.boot();
847 start(&mut n);
848 assert!(n.sync_tpdos().is_empty()); }
850
851 #[test]
852 fn reacts_to_a_pdo_parameter_write() {
853 let mut od = ObjectDictionary::<16>::new();
855 od.insert(
856 Address::new(0x1800, 1),
857 Entry::rw(Value::Unsigned32(0x8000_0190)),
858 )
859 .unwrap();
860 od.insert(Address::new(0x1800, 2), Entry::rw(Value::Unsigned8(1)))
861 .unwrap();
862 od.insert(Address::new(0x1A00, 0), Entry::rw(Value::Unsigned8(1)))
863 .unwrap();
864 od.insert(
865 Address::new(0x1A00, 1),
866 Entry::rw(Value::Unsigned32(MappingEntry::new(0x6000, 1, 16).to_u32())),
867 )
868 .unwrap();
869 od.insert(
870 Address::new(0x6000, 1),
871 Entry::rw(Value::Unsigned16(0xBEEF)),
872 )
873 .unwrap();
874
875 let mut n = Node::new(NodeId::new(0x10).unwrap(), od);
876 n.configure_pdos_from_od();
877 n.boot();
878 start(&mut n);
879 assert!(n.sync_tpdos().is_empty());
880
881 let req =
883 encode_download_expedited(Address::new(0x1800, 1), &Value::Unsigned32(0x190)).unwrap();
884 n.on_frame(0x610, &req);
885
886 let written = n.take_written_object().expect("a write was recorded");
888 assert_eq!(written, Address::new(0x1800, 1));
889 assert_eq!(n.take_written_object(), None); if (0x1400..=0x1BFF).contains(&written.index) {
891 n.configure_pdos_from_od();
892 }
893
894 let tpdos = n.sync_tpdos();
896 assert_eq!(tpdos.len(), 1);
897 assert_eq!(tpdos[0].cob_id, 0x190);
898 }
899}