Skip to main content

stackforge_core/layer/dot15d4/
command.rs

1//! IEEE 802.15.4 MAC Command frame parsing and building.
2//!
3//! MAC command frames carry the Command Frame Identifier (1 byte)
4//! followed by command-specific payloads:
5//! - Association Request (1 byte capability info)
6//! - Association Response (2 bytes short addr + 1 byte status)
7//! - Disassociation Notification (1 byte reason)
8//! - Data Request (no payload)
9//! - PAN ID Conflict Notification (no payload)
10//! - Orphan Notification (no payload)
11//! - Beacon Request (no payload)
12//! - Coordinator Realignment (7 bytes)
13//! - GTS Request (1 byte characteristics)
14
15use crate::layer::field::{FieldError, read_u16_le};
16
17use super::types;
18
19/// Parsed MAC command frame.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct CommandFrame {
22    /// Command Frame Identifier (1 byte).
23    pub cmd_id: u8,
24    /// Command-specific payload.
25    pub payload: CommandPayload,
26}
27
28/// Command-specific payload variants.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum CommandPayload {
31    /// Association Request: Capability Information (1 byte).
32    AssocReq(AssocReqPayload),
33    /// Association Response: Short Address (2 bytes) + Status (1 byte).
34    AssocResp(AssocRespPayload),
35    /// Disassociation Notification: Reason (1 byte).
36    DisassocNotify(DisassocNotifyPayload),
37    /// Data Request: no payload.
38    DataReq,
39    /// PAN ID Conflict Notification: no payload.
40    PanIdConflict,
41    /// Orphan Notification: no payload.
42    OrphanNotify,
43    /// Beacon Request: no payload.
44    BeaconReq,
45    /// Coordinator Realignment.
46    CoordRealign(CoordRealignPayload),
47    /// GTS Request: Characteristics (1 byte).
48    GtsReq(GtsReqPayload),
49    /// Unknown command with raw bytes.
50    Unknown(Vec<u8>),
51}
52
53/// Association Request capability information.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct AssocReqPayload {
56    /// Alternate PAN Coordinator (bit 0).
57    pub alternate_pan_coordinator: bool,
58    /// Device Type (bit 1): 1=FFD, 0=RFD.
59    pub device_type: bool,
60    /// Power Source (bit 2): 1=mains, 0=battery.
61    pub power_source: bool,
62    /// Receiver On When Idle (bit 3).
63    pub receiver_on_when_idle: bool,
64    /// Security Capability (bit 6).
65    pub security_capability: bool,
66    /// Allocate Address (bit 7).
67    pub allocate_address: bool,
68}
69
70/// Association Response payload.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct AssocRespPayload {
73    /// Short address assigned by coordinator (0xFFFF = none).
74    pub short_address: u16,
75    /// Association status.
76    pub association_status: u8,
77}
78
79/// Disassociation Notification payload.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct DisassocNotifyPayload {
82    /// Disassociation reason.
83    pub reason: u8,
84}
85
86/// Coordinator Realignment payload.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct CoordRealignPayload {
89    /// PAN Identifier (2 bytes, LE).
90    pub panid: u16,
91    /// Coordinator Short Address (2 bytes, LE).
92    pub coord_address: u16,
93    /// Logical Channel (1 byte).
94    pub channel: u8,
95    /// Short Address assigned to device (2 bytes, LE).
96    pub dev_address: u16,
97}
98
99/// GTS Request payload.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct GtsReqPayload {
102    /// GTS Length (4 bits).
103    pub gts_len: u8,
104    /// GTS Direction (1 bit).
105    pub gts_dir: bool,
106    /// Characteristics Type (1 bit).
107    pub charact_type: bool,
108}
109
110impl CommandFrame {
111    /// Parse a command frame from the buffer at the given offset.
112    /// Returns the parsed frame and the number of bytes consumed.
113    pub fn parse(buf: &[u8], offset: usize) -> Result<(Self, usize), FieldError> {
114        if buf.len() < offset + 1 {
115            return Err(FieldError::BufferTooShort {
116                offset,
117                need: 1,
118                have: buf.len().saturating_sub(offset),
119            });
120        }
121
122        let cmd_id = buf[offset];
123        let payload_start = offset + 1;
124        let remaining = &buf[payload_start..];
125
126        let (payload, payload_len) = match cmd_id {
127            types::cmd_id::ASSOC_REQ => {
128                if remaining.is_empty() {
129                    return Err(FieldError::BufferTooShort {
130                        offset: payload_start,
131                        need: 1,
132                        have: 0,
133                    });
134                }
135                let cap = remaining[0];
136                let payload = AssocReqPayload {
137                    alternate_pan_coordinator: (cap & 0x01) != 0,
138                    device_type: (cap & 0x02) != 0,
139                    power_source: (cap & 0x04) != 0,
140                    receiver_on_when_idle: (cap & 0x08) != 0,
141                    security_capability: (cap & 0x40) != 0,
142                    allocate_address: (cap & 0x80) != 0,
143                };
144                (CommandPayload::AssocReq(payload), 1)
145            },
146            types::cmd_id::ASSOC_RESP => {
147                if remaining.len() < 3 {
148                    return Err(FieldError::BufferTooShort {
149                        offset: payload_start,
150                        need: 3,
151                        have: remaining.len(),
152                    });
153                }
154                let short_address = read_u16_le(remaining, 0)?;
155                let association_status = remaining[2];
156                let payload = AssocRespPayload {
157                    short_address,
158                    association_status,
159                };
160                (CommandPayload::AssocResp(payload), 3)
161            },
162            types::cmd_id::DISASSOC_NOTIFY => {
163                if remaining.is_empty() {
164                    return Err(FieldError::BufferTooShort {
165                        offset: payload_start,
166                        need: 1,
167                        have: 0,
168                    });
169                }
170                let payload = DisassocNotifyPayload {
171                    reason: remaining[0],
172                };
173                (CommandPayload::DisassocNotify(payload), 1)
174            },
175            types::cmd_id::DATA_REQ => (CommandPayload::DataReq, 0),
176            types::cmd_id::PAN_ID_CONFLICT => (CommandPayload::PanIdConflict, 0),
177            types::cmd_id::ORPHAN_NOTIFY => (CommandPayload::OrphanNotify, 0),
178            types::cmd_id::BEACON_REQ => (CommandPayload::BeaconReq, 0),
179            types::cmd_id::COORD_REALIGN => {
180                if remaining.len() < 7 {
181                    return Err(FieldError::BufferTooShort {
182                        offset: payload_start,
183                        need: 7,
184                        have: remaining.len(),
185                    });
186                }
187                let panid = read_u16_le(remaining, 0)?;
188                let coord_address = read_u16_le(remaining, 2)?;
189                let channel = remaining[4];
190                let dev_address = read_u16_le(remaining, 5)?;
191                let payload = CoordRealignPayload {
192                    panid,
193                    coord_address,
194                    channel,
195                    dev_address,
196                };
197                (CommandPayload::CoordRealign(payload), 7)
198            },
199            types::cmd_id::GTS_REQ => {
200                if remaining.is_empty() {
201                    return Err(FieldError::BufferTooShort {
202                        offset: payload_start,
203                        need: 1,
204                        have: 0,
205                    });
206                }
207                let charact = remaining[0];
208                let payload = GtsReqPayload {
209                    gts_len: charact & 0x0F,
210                    gts_dir: (charact & 0x10) != 0,
211                    charact_type: (charact & 0x20) != 0,
212                };
213                (CommandPayload::GtsReq(payload), 1)
214            },
215            _ => {
216                let payload = CommandPayload::Unknown(remaining.to_vec());
217                let len = remaining.len();
218                (payload, len)
219            },
220        };
221
222        let consumed = 1 + payload_len;
223        Ok((CommandFrame { cmd_id, payload }, consumed))
224    }
225
226    /// Build the command frame bytes.
227    #[must_use]
228    pub fn build(&self) -> Vec<u8> {
229        let mut out = Vec::new();
230        out.push(self.cmd_id);
231
232        match &self.payload {
233            CommandPayload::AssocReq(p) => {
234                let mut cap: u8 = 0;
235                if p.alternate_pan_coordinator {
236                    cap |= 0x01;
237                }
238                if p.device_type {
239                    cap |= 0x02;
240                }
241                if p.power_source {
242                    cap |= 0x04;
243                }
244                if p.receiver_on_when_idle {
245                    cap |= 0x08;
246                }
247                if p.security_capability {
248                    cap |= 0x40;
249                }
250                if p.allocate_address {
251                    cap |= 0x80;
252                }
253                out.push(cap);
254            },
255            CommandPayload::AssocResp(p) => {
256                out.extend_from_slice(&p.short_address.to_le_bytes());
257                out.push(p.association_status);
258            },
259            CommandPayload::DisassocNotify(p) => {
260                out.push(p.reason);
261            },
262            CommandPayload::DataReq
263            | CommandPayload::PanIdConflict
264            | CommandPayload::OrphanNotify
265            | CommandPayload::BeaconReq => {},
266            CommandPayload::CoordRealign(p) => {
267                out.extend_from_slice(&p.panid.to_le_bytes());
268                out.extend_from_slice(&p.coord_address.to_le_bytes());
269                out.push(p.channel);
270                out.extend_from_slice(&p.dev_address.to_le_bytes());
271            },
272            CommandPayload::GtsReq(p) => {
273                let mut charact: u8 = p.gts_len & 0x0F;
274                if p.gts_dir {
275                    charact |= 0x10;
276                }
277                if p.charact_type {
278                    charact |= 0x20;
279                }
280                out.push(charact);
281            },
282            CommandPayload::Unknown(data) => {
283                out.extend_from_slice(data);
284            },
285        }
286
287        out
288    }
289
290    /// Get a human-readable summary of this command.
291    #[must_use]
292    pub fn summary(&self) -> String {
293        format!("802.15.4 Command {}", types::cmd_id_name(self.cmd_id))
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_parse_assoc_req() {
303        let buf = [0x01, 0x83];
304        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
305        assert_eq!(consumed, 2);
306        assert_eq!(cmd.cmd_id, 1);
307        if let CommandPayload::AssocReq(p) = &cmd.payload {
308            assert!(p.allocate_address);
309            assert!(p.device_type);
310            assert!(p.alternate_pan_coordinator);
311            assert!(!p.power_source);
312            assert!(!p.receiver_on_when_idle);
313            assert!(!p.security_capability);
314        } else {
315            panic!("Expected AssocReq payload");
316        }
317    }
318
319    #[test]
320    fn test_parse_assoc_resp() {
321        let buf = [0x02, 0x34, 0x12, 0x00];
322        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
323        assert_eq!(consumed, 4);
324        assert_eq!(cmd.cmd_id, 2);
325        if let CommandPayload::AssocResp(p) = &cmd.payload {
326            assert_eq!(p.short_address, 0x1234);
327            assert_eq!(p.association_status, 0);
328        } else {
329            panic!("Expected AssocResp payload");
330        }
331    }
332
333    #[test]
334    fn test_parse_disassoc_notify() {
335        let buf = [0x03, 0x02];
336        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
337        assert_eq!(consumed, 2);
338        if let CommandPayload::DisassocNotify(p) = &cmd.payload {
339            assert_eq!(p.reason, 2);
340        } else {
341            panic!("Expected DisassocNotify payload");
342        }
343    }
344
345    #[test]
346    fn test_parse_data_req() {
347        let buf = [0x04];
348        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
349        assert_eq!(consumed, 1);
350        assert_eq!(cmd.payload, CommandPayload::DataReq);
351    }
352
353    #[test]
354    fn test_parse_panid_conflict() {
355        let buf = [0x05];
356        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
357        assert_eq!(consumed, 1);
358        assert_eq!(cmd.payload, CommandPayload::PanIdConflict);
359    }
360
361    #[test]
362    fn test_parse_orphan_notify() {
363        let buf = [0x06];
364        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
365        assert_eq!(consumed, 1);
366        assert_eq!(cmd.payload, CommandPayload::OrphanNotify);
367    }
368
369    #[test]
370    fn test_parse_beacon_req() {
371        let buf = [0x07];
372        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
373        assert_eq!(consumed, 1);
374        assert_eq!(cmd.payload, CommandPayload::BeaconReq);
375    }
376
377    #[test]
378    fn test_parse_coord_realign() {
379        let buf = [0x08, 0xFF, 0xFF, 0x00, 0x00, 0x0B, 0xCD, 0xAB];
380        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
381        assert_eq!(consumed, 8);
382        if let CommandPayload::CoordRealign(p) = &cmd.payload {
383            assert_eq!(p.panid, 0xFFFF);
384            assert_eq!(p.coord_address, 0x0000);
385            assert_eq!(p.channel, 11);
386            assert_eq!(p.dev_address, 0xABCD);
387        } else {
388            panic!("Expected CoordRealign payload");
389        }
390    }
391
392    #[test]
393    fn test_parse_gts_req() {
394        let buf = [0x09, 0x15];
395        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
396        assert_eq!(consumed, 2);
397        if let CommandPayload::GtsReq(p) = &cmd.payload {
398            assert_eq!(p.gts_len, 5);
399            assert!(p.gts_dir);
400            assert!(!p.charact_type);
401        } else {
402            panic!("Expected GtsReq payload");
403        }
404    }
405
406    #[test]
407    fn test_build_roundtrip_assoc_req() {
408        let cmd = CommandFrame {
409            cmd_id: types::cmd_id::ASSOC_REQ,
410            payload: CommandPayload::AssocReq(AssocReqPayload {
411                alternate_pan_coordinator: false,
412                device_type: true,
413                power_source: true,
414                receiver_on_when_idle: true,
415                security_capability: false,
416                allocate_address: true,
417            }),
418        };
419        let bytes = cmd.build();
420        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
421        assert_eq!(parsed, cmd);
422    }
423
424    #[test]
425    fn test_build_roundtrip_assoc_resp() {
426        let cmd = CommandFrame {
427            cmd_id: types::cmd_id::ASSOC_RESP,
428            payload: CommandPayload::AssocResp(AssocRespPayload {
429                short_address: 0x5678,
430                association_status: types::assoc_status::PAN_AT_CAPACITY,
431            }),
432        };
433        let bytes = cmd.build();
434        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
435        assert_eq!(parsed, cmd);
436    }
437
438    #[test]
439    fn test_build_roundtrip_disassoc() {
440        let cmd = CommandFrame {
441            cmd_id: types::cmd_id::DISASSOC_NOTIFY,
442            payload: CommandPayload::DisassocNotify(DisassocNotifyPayload {
443                reason: types::disassoc_reason::DEVICE_WISHES_TO_LEAVE,
444            }),
445        };
446        let bytes = cmd.build();
447        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
448        assert_eq!(parsed, cmd);
449    }
450
451    #[test]
452    fn test_build_roundtrip_data_req() {
453        let cmd = CommandFrame {
454            cmd_id: types::cmd_id::DATA_REQ,
455            payload: CommandPayload::DataReq,
456        };
457        let bytes = cmd.build();
458        assert_eq!(bytes, vec![0x04]);
459        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
460        assert_eq!(parsed, cmd);
461    }
462
463    #[test]
464    fn test_build_roundtrip_coord_realign() {
465        let cmd = CommandFrame {
466            cmd_id: types::cmd_id::COORD_REALIGN,
467            payload: CommandPayload::CoordRealign(CoordRealignPayload {
468                panid: 0x1234,
469                coord_address: 0x0000,
470                channel: 15,
471                dev_address: 0xFFFF,
472            }),
473        };
474        let bytes = cmd.build();
475        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
476        assert_eq!(parsed, cmd);
477    }
478
479    #[test]
480    fn test_build_roundtrip_gts_req() {
481        let cmd = CommandFrame {
482            cmd_id: types::cmd_id::GTS_REQ,
483            payload: CommandPayload::GtsReq(GtsReqPayload {
484                gts_len: 7,
485                gts_dir: true,
486                charact_type: true,
487            }),
488        };
489        let bytes = cmd.build();
490        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
491        assert_eq!(parsed, cmd);
492    }
493
494    #[test]
495    fn test_parse_with_offset() {
496        let mut buf = vec![0xAA, 0xBB];
497        buf.push(0x04);
498        let (cmd, consumed) = CommandFrame::parse(&buf, 2).unwrap();
499        assert_eq!(consumed, 1);
500        assert_eq!(cmd.cmd_id, types::cmd_id::DATA_REQ);
501    }
502
503    #[test]
504    fn test_parse_buffer_too_short() {
505        let buf: [u8; 0] = [];
506        let result = CommandFrame::parse(&buf, 0);
507        assert!(result.is_err());
508    }
509
510    #[test]
511    fn test_summary() {
512        let cmd = CommandFrame {
513            cmd_id: types::cmd_id::ASSOC_REQ,
514            payload: CommandPayload::AssocReq(AssocReqPayload {
515                alternate_pan_coordinator: false,
516                device_type: true,
517                power_source: false,
518                receiver_on_when_idle: false,
519                security_capability: false,
520                allocate_address: true,
521            }),
522        };
523        let summary = cmd.summary();
524        assert!(summary.contains("AssocReq"));
525    }
526}