Skip to main content

hex_motor/canopen/
cob.rs

1//! COB-ID 计算 + custom-protocol.md 中保留 ID 的检查。
2//!
3//! 本模块只做纯函数运算;不发任何帧。
4
5use crate::error::{Error, Result};
6
7/// CANopen 通信对象 (function code 分类)。
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CommunicationObject {
10    /// NMT 命令帧 (0x000,主站发出)。
11    Nmt,
12    /// 同步帧 (0x080)。
13    Sync,
14    /// 紧急帧 (0x080 + node_id)。
15    Emergency,
16    /// 时间帧 (0x100)。
17    Time,
18    Tpdo1,
19    Rpdo1,
20    Tpdo2,
21    Rpdo2,
22    Tpdo3,
23    Rpdo3,
24    Tpdo4,
25    Rpdo4,
26    /// 服务器 → 客户端 (0x580 + nid)。
27    Tsdo,
28    /// 客户端 → 服务器 (0x600 + nid)。
29    Rsdo,
30    /// 心跳 (0x700 + nid)。
31    Heartbeat,
32}
33
34impl CommunicationObject {
35    /// 该 object 的 function code 部分(COB-ID 高 4 bits)。
36    pub fn function_code(self) -> u16 {
37        match self {
38            CommunicationObject::Nmt => 0x000,
39            CommunicationObject::Sync => 0x080,
40            CommunicationObject::Emergency => 0x080,
41            CommunicationObject::Time => 0x100,
42            CommunicationObject::Tpdo1 => 0x180,
43            CommunicationObject::Rpdo1 => 0x200,
44            CommunicationObject::Tpdo2 => 0x280,
45            CommunicationObject::Rpdo2 => 0x300,
46            CommunicationObject::Tpdo3 => 0x380,
47            CommunicationObject::Rpdo3 => 0x400,
48            CommunicationObject::Tpdo4 => 0x480,
49            CommunicationObject::Rpdo4 => 0x500,
50            CommunicationObject::Tsdo => 0x580,
51            CommunicationObject::Rsdo => 0x600,
52            CommunicationObject::Heartbeat => 0x700,
53        }
54    }
55
56    /// 拼出标准 COB-ID。
57    ///
58    /// `Nmt` / `Sync` / `Time` 不带 node id,调用方应传 0;其他必须 1..=127。
59    pub fn cob_id(self, node_id: u8) -> Result<u16> {
60        let base = self.function_code();
61        match self {
62            CommunicationObject::Nmt
63            | CommunicationObject::Sync
64            | CommunicationObject::Time => Ok(base),
65            _ => {
66                if node_id == 0 || node_id > 0x7F {
67                    return Err(Error::InvalidNodeId(node_id));
68                }
69                Ok(base | node_id as u16)
70            }
71        }
72    }
73}
74
75/// 从 COB-ID 反解 `(CommunicationObject, node_id)`。
76/// 对 Sync / Nmt / Time 返回 `node_id = 0`。
77pub fn parse_cob_id(cob_id: u16) -> Option<(CommunicationObject, u8)> {
78    if cob_id == 0x000 {
79        return Some((CommunicationObject::Nmt, 0));
80    }
81    if cob_id == 0x080 {
82        return Some((CommunicationObject::Sync, 0));
83    }
84    if cob_id == 0x100 {
85        return Some((CommunicationObject::Time, 0));
86    }
87
88    let function = cob_id & 0x780;
89    let nid = (cob_id & 0x7F) as u8;
90    if nid == 0 || nid > 0x7F {
91        return None;
92    }
93
94    let obj = match function {
95        0x080 => CommunicationObject::Emergency,
96        0x180 => CommunicationObject::Tpdo1,
97        0x200 => CommunicationObject::Rpdo1,
98        0x280 => CommunicationObject::Tpdo2,
99        0x300 => CommunicationObject::Rpdo2,
100        0x380 => CommunicationObject::Tpdo3,
101        0x400 => CommunicationObject::Rpdo3,
102        0x480 => CommunicationObject::Tpdo4,
103        0x500 => CommunicationObject::Rpdo4,
104        0x580 => CommunicationObject::Tsdo,
105        0x600 => CommunicationObject::Rsdo,
106        0x700 => CommunicationObject::Heartbeat,
107        _ => return None,
108    };
109    Some((obj, nid))
110}
111
112/// custom-protocol.md "保留的 CAN 帧范围 - 标准帧" 检查。
113/// 用户自定义帧应避开这些 COB-ID。
114pub fn is_reserved_standard_cob_id(cob_id: u16) -> bool {
115    matches!(
116        cob_id,
117        // 单点
118        0x000        // NMT
119        | 0x080      // SYNC
120        | 0x100      // TIME
121    ) || (0x081..=0x0FF).contains(&cob_id)   // EMCY
122        || (0x181..=0x1FF).contains(&cob_id) // TPDO1
123        || (0x201..=0x27F).contains(&cob_id) // RPDO1
124        || (0x281..=0x2FF).contains(&cob_id) // TPDO2
125        || (0x301..=0x37F).contains(&cob_id) // RPDO2
126        || (0x381..=0x3FF).contains(&cob_id) // TPDO3
127        || (0x401..=0x47F).contains(&cob_id) // RPDO3
128        || (0x481..=0x4FF).contains(&cob_id) // TPDO4
129        || (0x501..=0x57F).contains(&cob_id) // RPDO4
130        || (0x581..=0x5FF).contains(&cob_id) // TSDO
131        || (0x601..=0x67F).contains(&cob_id) // RSDO
132        || (0x701..=0x77F).contains(&cob_id) // HB
133        || (0x680..=0x6FF).contains(&cob_id) // LSS-ish
134        || (0x780..=0x7FF).contains(&cob_id) // LSS
135}
136
137/// custom-protocol.md "保留的 CAN 帧范围 - 扩展帧" 检查。
138pub fn is_reserved_extended_cob_id(cob_id: u32) -> bool {
139    matches!(cob_id, 0x09 | 0xA9 | 0xAA) || (0xFE00..=0xFEFF).contains(&cob_id)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn cob_id_tpdo1_node10() {
148        assert_eq!(CommunicationObject::Tpdo1.cob_id(0x10).unwrap(), 0x190);
149    }
150
151    #[test]
152    fn cob_id_heartbeat_node1() {
153        assert_eq!(CommunicationObject::Heartbeat.cob_id(0x01).unwrap(), 0x701);
154    }
155
156    #[test]
157    fn cob_id_rsdo_node127() {
158        assert_eq!(CommunicationObject::Rsdo.cob_id(0x7F).unwrap(), 0x67F);
159    }
160
161    #[test]
162    fn cob_id_nmt_ignores_node() {
163        assert_eq!(CommunicationObject::Nmt.cob_id(0).unwrap(), 0x000);
164    }
165
166    #[test]
167    fn cob_id_rejects_zero_node() {
168        assert!(matches!(
169            CommunicationObject::Tpdo1.cob_id(0),
170            Err(Error::InvalidNodeId(0))
171        ));
172    }
173
174    #[test]
175    fn cob_id_rejects_node_over_127() {
176        assert!(matches!(
177            CommunicationObject::Tpdo1.cob_id(128),
178            Err(Error::InvalidNodeId(128))
179        ));
180    }
181
182    #[test]
183    fn parse_heartbeat() {
184        assert_eq!(parse_cob_id(0x710), Some((CommunicationObject::Heartbeat, 0x10)));
185    }
186
187    #[test]
188    fn parse_tpdo1() {
189        assert_eq!(parse_cob_id(0x190), Some((CommunicationObject::Tpdo1, 0x10)));
190    }
191
192    #[test]
193    fn parse_sync() {
194        assert_eq!(parse_cob_id(0x080), Some((CommunicationObject::Sync, 0)));
195    }
196
197    #[test]
198    fn parse_emergency() {
199        // 0x081 是 EMCY for node 1
200        assert_eq!(parse_cob_id(0x081), Some((CommunicationObject::Emergency, 1)));
201    }
202
203    #[test]
204    fn parse_invalid() {
205        assert_eq!(parse_cob_id(0x150), None); // 0x100..=0x17F 无效区间
206    }
207
208    #[test]
209    fn reserved_standard_covers_all_canopen_objects() {
210        // 心跳
211        assert!(is_reserved_standard_cob_id(0x701));
212        assert!(is_reserved_standard_cob_id(0x77F));
213        // TPDO1 区间
214        assert!(is_reserved_standard_cob_id(0x181));
215        assert!(is_reserved_standard_cob_id(0x1FF));
216        // SDO 双向
217        assert!(is_reserved_standard_cob_id(0x581));
218        assert!(is_reserved_standard_cob_id(0x67F));
219        // SYNC / TIME / NMT
220        assert!(is_reserved_standard_cob_id(0x000));
221        assert!(is_reserved_standard_cob_id(0x080));
222        assert!(is_reserved_standard_cob_id(0x100));
223        // LSS 区
224        assert!(is_reserved_standard_cob_id(0x7E5));
225    }
226
227    #[test]
228    fn reserved_standard_allows_safe_ids() {
229        // 自定义安全区域示例。这些 ID 不在 custom-protocol.md 的保留列表里:
230        // 因为列表是 "181-1FF" 等(每段去掉了 base 那个 +0 的 ID)。
231        assert!(!is_reserved_standard_cob_id(0x101));
232        assert!(!is_reserved_standard_cob_id(0x17F));
233        assert!(!is_reserved_standard_cob_id(0x180));
234        assert!(!is_reserved_standard_cob_id(0x200));
235        assert!(!is_reserved_standard_cob_id(0x280));
236        assert!(!is_reserved_standard_cob_id(0x300));
237        assert!(!is_reserved_standard_cob_id(0x380));
238        assert!(!is_reserved_standard_cob_id(0x400));
239        assert!(!is_reserved_standard_cob_id(0x480));
240        assert!(!is_reserved_standard_cob_id(0x500));
241        assert!(!is_reserved_standard_cob_id(0x580));
242        assert!(!is_reserved_standard_cob_id(0x600));
243        assert!(!is_reserved_standard_cob_id(0x700));
244    }
245
246    #[test]
247    fn reserved_standard_includes_lss_range() {
248        // 0x680-0x6FF 撞 LSS,应当被认为保留。
249        assert!(is_reserved_standard_cob_id(0x680));
250        assert!(is_reserved_standard_cob_id(0x6FF));
251        assert!(is_reserved_standard_cob_id(0x780));
252        assert!(is_reserved_standard_cob_id(0x7FF));
253    }
254
255    #[test]
256    fn reserved_extended_known() {
257        assert!(is_reserved_extended_cob_id(0x09));
258        assert!(is_reserved_extended_cob_id(0xA9));
259        assert!(is_reserved_extended_cob_id(0xAA));
260        assert!(is_reserved_extended_cob_id(0xFE00));
261        assert!(is_reserved_extended_cob_id(0xFEFF));
262        assert!(!is_reserved_extended_cob_id(0xAB));
263        assert!(!is_reserved_extended_cob_id(0xFDFF));
264    }
265}