1use heapless::Vec;
31
32use crate::nmt::{self, NmtState, NmtStateMachine};
33use crate::object_dictionary::ObjectDictionary;
34use crate::pdo::{self, PdoMapping, TransmissionType};
35use crate::sdo::{self, SdoServer};
36use crate::types::NodeId;
37use crate::{Error, Result};
38
39pub const MAX_PDOS: usize = 4;
42
43pub const MAX_PDO_MAPPING: usize = 8;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct TxFrame {
50 pub cob_id: u16,
52 data: [u8; 8],
53 len: u8,
54}
55
56impl TxFrame {
57 fn new(cob_id: u16, bytes: &[u8]) -> Self {
58 let len = bytes.len().min(8);
59 let mut data = [0u8; 8];
60 data[..len].copy_from_slice(&bytes[..len]);
61 Self {
62 cob_id,
63 data,
64 len: len as u8,
65 }
66 }
67
68 pub fn data(&self) -> &[u8] {
70 &self.data[..self.len as usize]
71 }
72}
73
74#[derive(Debug)]
76struct RpdoSlot {
77 cob_id: u16,
78 mapping: PdoMapping<MAX_PDO_MAPPING>,
79}
80
81#[derive(Debug)]
83struct TpdoSlot {
84 cob_id: u16,
85 mapping: PdoMapping<MAX_PDO_MAPPING>,
86 transmission: TransmissionType,
87}
88
89#[derive(Debug)]
92pub struct Node<const N: usize> {
93 node_id: NodeId,
94 od: ObjectDictionary<N>,
95 sdo: SdoServer,
96 nmt: NmtStateMachine,
97 rpdos: Vec<RpdoSlot, MAX_PDOS>,
98 tpdos: Vec<TpdoSlot, MAX_PDOS>,
99}
100
101impl<const N: usize> Node<N> {
102 pub fn new(node_id: NodeId, od: ObjectDictionary<N>) -> Self {
105 Self {
106 node_id,
107 od,
108 sdo: SdoServer::new(node_id),
109 nmt: NmtStateMachine::new(),
110 rpdos: Vec::new(),
111 tpdos: Vec::new(),
112 }
113 }
114
115 pub fn add_rpdo(&mut self, cob_id: u16, mapping: PdoMapping<MAX_PDO_MAPPING>) -> Result<()> {
120 self.rpdos
121 .push(RpdoSlot { cob_id, mapping })
122 .map_err(|_| Error::MappingFull)
123 }
124
125 pub fn add_tpdo(
130 &mut self,
131 cob_id: u16,
132 mapping: PdoMapping<MAX_PDO_MAPPING>,
133 transmission: TransmissionType,
134 ) -> Result<()> {
135 self.tpdos
136 .push(TpdoSlot {
137 cob_id,
138 mapping,
139 transmission,
140 })
141 .map_err(|_| Error::MappingFull)
142 }
143
144 pub fn node_id(&self) -> NodeId {
146 self.node_id
147 }
148
149 pub fn state(&self) -> NmtState {
151 self.nmt.state()
152 }
153
154 pub fn od(&self) -> &ObjectDictionary<N> {
156 &self.od
157 }
158
159 pub fn od_mut(&mut self) -> &mut ObjectDictionary<N> {
161 &mut self.od
162 }
163
164 pub fn boot(&mut self) -> TxFrame {
167 self.nmt.boot();
168 TxFrame::new(nmt::heartbeat_cob_id(self.node_id), &nmt::BOOTUP_FRAME)
169 }
170
171 pub fn heartbeat(&self) -> TxFrame {
174 TxFrame::new(
175 nmt::heartbeat_cob_id(self.node_id),
176 &nmt::encode_heartbeat(self.nmt.state()),
177 )
178 }
179
180 pub fn on_frame(&mut self, cob_id: u16, data: &[u8]) -> Option<TxFrame> {
187 if cob_id == nmt::NMT_COMMAND_COB_ID {
188 self.on_nmt(data);
189 None
190 } else if cob_id == self.sdo.request_cob_id() {
191 self.on_sdo(data)
192 } else {
193 self.on_rpdo(cob_id, data);
194 None
195 }
196 }
197
198 pub fn sync_tpdos(&self) -> Vec<TxFrame, MAX_PDOS> {
204 let mut frames = Vec::new();
205 if self.nmt.state() != NmtState::Operational {
206 return frames;
207 }
208 for slot in &self.tpdos {
209 if is_synchronous(slot.transmission) {
210 if let Some(frame) = self.build_tpdo(slot) {
211 let _ = frames.push(frame);
213 }
214 }
215 }
216 frames
217 }
218
219 pub fn tpdo(&self, index: usize) -> Option<TxFrame> {
222 if self.nmt.state() != NmtState::Operational {
223 return None;
224 }
225 self.build_tpdo(self.tpdos.get(index)?)
226 }
227
228 fn build_tpdo(&self, slot: &TpdoSlot) -> Option<TxFrame> {
229 if slot.mapping.is_empty() {
230 return None;
231 }
232 let mut buf = [0u8; 8];
233 let len = pdo::pack(&slot.mapping, &self.od, &mut buf).ok()?;
234 Some(TxFrame::new(slot.cob_id, &buf[..len]))
235 }
236
237 fn on_rpdo(&mut self, cob_id: u16, data: &[u8]) {
238 if self.nmt.state() != NmtState::Operational {
240 return;
241 }
242 if let Some(i) = self.rpdos.iter().position(|r| r.cob_id == cob_id) {
243 let _ = pdo::unpack(&self.rpdos[i].mapping, &mut self.od, data);
245 }
246 }
247
248 fn on_nmt(&mut self, data: &[u8]) {
249 if data.len() < 2 {
251 return;
252 }
253 if let Ok((command, target)) = nmt::decode_command(&[data[0], data[1]]) {
254 if target == NodeId::BROADCAST || target == self.node_id {
255 self.nmt.apply(command);
256 }
257 }
258 }
259
260 fn on_sdo(&mut self, data: &[u8]) -> Option<TxFrame> {
261 if !matches!(
263 self.nmt.state(),
264 NmtState::PreOperational | NmtState::Operational
265 ) {
266 return None;
267 }
268 let mut payload: sdo::SdoPayload = [0u8; 8];
269 if data.len() > payload.len() {
270 return None;
271 }
272 payload[..data.len()].copy_from_slice(data);
273 let response = self.sdo.handle(&mut self.od, &payload)?;
274 Some(TxFrame::new(self.sdo.response_cob_id(), &response))
275 }
276}
277
278fn is_synchronous(transmission: TransmissionType) -> bool {
280 matches!(
281 transmission,
282 TransmissionType::SynchronousAcyclic | TransmissionType::SynchronousCyclic(_)
283 )
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::object_dictionary::{Address, Entry};
290 use crate::pdo::MappingEntry;
291 use crate::sdo::{encode_download_expedited, encode_upload_request};
292 use crate::{DataType, NmtCommand, Value};
293
294 fn start(n: &mut Node<8>) {
295 n.on_frame(
296 nmt::NMT_COMMAND_COB_ID,
297 &[NmtCommand::StartRemoteNode as u8, 0x10],
298 );
299 }
300
301 fn od() -> ObjectDictionary<8> {
302 let mut od = ObjectDictionary::new();
303 od.insert(
304 Address::new(0x1000, 0),
305 Entry::constant(Value::Unsigned32(0x192)),
306 )
307 .unwrap();
308 od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))
309 .unwrap();
310 od
311 }
312
313 fn node() -> Node<8> {
314 Node::new(NodeId::new(0x10).unwrap(), od())
315 }
316
317 #[test]
318 fn boots_from_init_to_preop_and_announces() {
319 let mut n = node();
320 assert_eq!(n.state(), NmtState::Initialising);
321 let boot = n.boot();
322 assert_eq!(n.state(), NmtState::PreOperational);
323 assert_eq!(boot.cob_id, 0x710); assert_eq!(boot.data(), &[0x00]);
325 }
326
327 #[test]
328 fn heartbeat_reflects_state() {
329 let mut n = node();
330 n.boot();
331 assert_eq!(n.heartbeat().data(), &[0x7F]); n.on_frame(
333 nmt::NMT_COMMAND_COB_ID,
334 &[NmtCommand::StartRemoteNode as u8, 0x10],
335 );
336 assert_eq!(n.state(), NmtState::Operational);
337 assert_eq!(n.heartbeat().data(), &[0x05]); }
339
340 #[test]
341 fn serves_sdo_read_when_preoperational() {
342 let mut n = node();
343 n.boot();
344 let req = encode_upload_request(Address::new(0x1000, 0));
345 let resp = n.on_frame(0x610, &req).expect("SDO response");
346 assert_eq!(resp.cob_id, 0x590); let (_, value) = crate::sdo::decode_upload_expedited_response(
348 resp.data().try_into().unwrap(),
349 DataType::Unsigned32,
350 )
351 .unwrap();
352 assert_eq!(value, Value::Unsigned32(0x192));
353 }
354
355 #[test]
356 fn ignores_sdo_before_boot() {
357 let mut n = node(); let req = encode_upload_request(Address::new(0x1000, 0));
359 assert!(n.on_frame(0x610, &req).is_none());
360 }
361
362 #[test]
363 fn ignores_sdo_when_stopped() {
364 let mut n = node();
365 n.boot();
366 n.on_frame(
367 nmt::NMT_COMMAND_COB_ID,
368 &[NmtCommand::StopRemoteNode as u8, 0x10],
369 );
370 assert_eq!(n.state(), NmtState::Stopped);
371 let req = encode_upload_request(Address::new(0x1000, 0));
372 assert!(n.on_frame(0x610, &req).is_none());
373 }
374
375 #[test]
376 fn nmt_command_for_other_node_is_ignored() {
377 let mut n = node();
378 n.boot();
379 n.on_frame(
381 nmt::NMT_COMMAND_COB_ID,
382 &[NmtCommand::StartRemoteNode as u8, 0x20],
383 );
384 assert_eq!(n.state(), NmtState::PreOperational); }
386
387 #[test]
388 fn broadcast_nmt_applies() {
389 let mut n = node();
390 n.boot();
391 n.on_frame(
392 nmt::NMT_COMMAND_COB_ID,
393 &[NmtCommand::StartRemoteNode as u8, 0x00],
394 );
395 assert_eq!(n.state(), NmtState::Operational);
396 }
397
398 #[test]
399 fn serves_sdo_write_and_updates_od() {
400 let mut n = node();
401 n.boot();
402 let req =
403 encode_download_expedited(Address::new(0x1017, 0), &Value::Unsigned16(1234)).unwrap();
404 assert!(n.on_frame(0x610, &req).is_some());
405 assert_eq!(
406 n.od().read(Address::new(0x1017, 0)).unwrap(),
407 Value::Unsigned16(1234)
408 );
409 }
410
411 #[test]
412 fn ignores_unrelated_cob_id() {
413 let mut n = node();
414 n.boot();
415 assert!(n.on_frame(0x123, &[0; 8]).is_none());
416 }
417
418 fn pdo_od() -> ObjectDictionary<8> {
420 let mut od = ObjectDictionary::new();
421 od.insert(
423 Address::new(0x6000, 1),
424 Entry::rw(Value::Unsigned16(0xBEEF)),
425 )
426 .unwrap();
427 od.insert(Address::new(0x6000, 2), Entry::rw(Value::Unsigned8(0x42)))
428 .unwrap();
429 od.insert(Address::new(0x6200, 1), Entry::rw(Value::Unsigned16(0)))
430 .unwrap();
431 od
432 }
433
434 fn mapping(entries: &[(u16, u8, u8)]) -> PdoMapping<MAX_PDO_MAPPING> {
435 let mut m = PdoMapping::new();
436 for &(index, sub, bits) in entries {
437 m.push(MappingEntry::new(index, sub, bits)).unwrap();
438 }
439 m
440 }
441
442 #[test]
443 fn tpdo_transmits_only_when_operational() {
444 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
445 n.add_tpdo(
446 0x18A,
447 mapping(&[(0x6000, 1, 16), (0x6000, 2, 8)]),
448 TransmissionType::SynchronousAcyclic,
449 )
450 .unwrap();
451 n.boot();
452
453 assert!(n.sync_tpdos().is_empty());
455
456 start(&mut n);
457 let frames = n.sync_tpdos();
458 assert_eq!(frames.len(), 1);
459 assert_eq!(frames[0].cob_id, 0x18A);
460 assert_eq!(frames[0].data(), &[0xEF, 0xBE, 0x42]);
462 }
463
464 #[test]
465 fn event_tpdo_by_index() {
466 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
467 n.add_tpdo(
469 0x18A,
470 mapping(&[(0x6000, 2, 8)]),
471 TransmissionType::EventDrivenProfile,
472 )
473 .unwrap();
474 n.boot();
475 start(&mut n);
476 assert!(n.sync_tpdos().is_empty());
477 assert_eq!(n.tpdo(0).unwrap().data(), &[0x42]);
478 assert!(n.tpdo(1).is_none());
479 }
480
481 #[test]
482 fn rpdo_applies_only_when_operational() {
483 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
484 n.add_rpdo(0x20A, mapping(&[(0x6200, 1, 16)])).unwrap();
485 n.boot();
486
487 assert!(n.on_frame(0x20A, &[0x34, 0x12]).is_none());
489 assert_eq!(
490 n.od().read(Address::new(0x6200, 1)).unwrap(),
491 Value::Unsigned16(0)
492 );
493
494 start(&mut n);
496 n.on_frame(0x20A, &[0x34, 0x12]);
497 assert_eq!(
498 n.od().read(Address::new(0x6200, 1)).unwrap(),
499 Value::Unsigned16(0x1234)
500 );
501 }
502
503 #[test]
504 fn pdo_capacity_is_enforced() {
505 let mut n = Node::new(NodeId::new(0x10).unwrap(), pdo_od());
506 for _ in 0..MAX_PDOS {
507 n.add_tpdo(
508 0x18A,
509 mapping(&[(0x6000, 2, 8)]),
510 TransmissionType::SynchronousAcyclic,
511 )
512 .unwrap();
513 }
514 assert_eq!(
515 n.add_tpdo(
516 0x18A,
517 mapping(&[(0x6000, 2, 8)]),
518 TransmissionType::SynchronousAcyclic
519 ),
520 Err(Error::MappingFull)
521 );
522 }
523}