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    pub fn build(&self) -> Vec<u8> {
228        let mut out = Vec::new();
229        out.push(self.cmd_id);
230
231        match &self.payload {
232            CommandPayload::AssocReq(p) => {
233                let mut cap: u8 = 0;
234                if p.alternate_pan_coordinator {
235                    cap |= 0x01;
236                }
237                if p.device_type {
238                    cap |= 0x02;
239                }
240                if p.power_source {
241                    cap |= 0x04;
242                }
243                if p.receiver_on_when_idle {
244                    cap |= 0x08;
245                }
246                if p.security_capability {
247                    cap |= 0x40;
248                }
249                if p.allocate_address {
250                    cap |= 0x80;
251                }
252                out.push(cap);
253            }
254            CommandPayload::AssocResp(p) => {
255                out.extend_from_slice(&p.short_address.to_le_bytes());
256                out.push(p.association_status);
257            }
258            CommandPayload::DisassocNotify(p) => {
259                out.push(p.reason);
260            }
261            CommandPayload::DataReq
262            | CommandPayload::PanIdConflict
263            | CommandPayload::OrphanNotify
264            | CommandPayload::BeaconReq => {}
265            CommandPayload::CoordRealign(p) => {
266                out.extend_from_slice(&p.panid.to_le_bytes());
267                out.extend_from_slice(&p.coord_address.to_le_bytes());
268                out.push(p.channel);
269                out.extend_from_slice(&p.dev_address.to_le_bytes());
270            }
271            CommandPayload::GtsReq(p) => {
272                let mut charact: u8 = p.gts_len & 0x0F;
273                if p.gts_dir {
274                    charact |= 0x10;
275                }
276                if p.charact_type {
277                    charact |= 0x20;
278                }
279                out.push(charact);
280            }
281            CommandPayload::Unknown(data) => {
282                out.extend_from_slice(data);
283            }
284        }
285
286        out
287    }
288
289    /// Get a human-readable summary of this command.
290    pub fn summary(&self) -> String {
291        format!("802.15.4 Command {}", types::cmd_id_name(self.cmd_id))
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_parse_assoc_req() {
301        let buf = [0x01, 0x83];
302        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
303        assert_eq!(consumed, 2);
304        assert_eq!(cmd.cmd_id, 1);
305        if let CommandPayload::AssocReq(p) = &cmd.payload {
306            assert!(p.allocate_address);
307            assert!(p.device_type);
308            assert!(p.alternate_pan_coordinator);
309            assert!(!p.power_source);
310            assert!(!p.receiver_on_when_idle);
311            assert!(!p.security_capability);
312        } else {
313            panic!("Expected AssocReq payload");
314        }
315    }
316
317    #[test]
318    fn test_parse_assoc_resp() {
319        let buf = [0x02, 0x34, 0x12, 0x00];
320        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
321        assert_eq!(consumed, 4);
322        assert_eq!(cmd.cmd_id, 2);
323        if let CommandPayload::AssocResp(p) = &cmd.payload {
324            assert_eq!(p.short_address, 0x1234);
325            assert_eq!(p.association_status, 0);
326        } else {
327            panic!("Expected AssocResp payload");
328        }
329    }
330
331    #[test]
332    fn test_parse_disassoc_notify() {
333        let buf = [0x03, 0x02];
334        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
335        assert_eq!(consumed, 2);
336        if let CommandPayload::DisassocNotify(p) = &cmd.payload {
337            assert_eq!(p.reason, 2);
338        } else {
339            panic!("Expected DisassocNotify payload");
340        }
341    }
342
343    #[test]
344    fn test_parse_data_req() {
345        let buf = [0x04];
346        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
347        assert_eq!(consumed, 1);
348        assert_eq!(cmd.payload, CommandPayload::DataReq);
349    }
350
351    #[test]
352    fn test_parse_panid_conflict() {
353        let buf = [0x05];
354        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
355        assert_eq!(consumed, 1);
356        assert_eq!(cmd.payload, CommandPayload::PanIdConflict);
357    }
358
359    #[test]
360    fn test_parse_orphan_notify() {
361        let buf = [0x06];
362        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
363        assert_eq!(consumed, 1);
364        assert_eq!(cmd.payload, CommandPayload::OrphanNotify);
365    }
366
367    #[test]
368    fn test_parse_beacon_req() {
369        let buf = [0x07];
370        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
371        assert_eq!(consumed, 1);
372        assert_eq!(cmd.payload, CommandPayload::BeaconReq);
373    }
374
375    #[test]
376    fn test_parse_coord_realign() {
377        let buf = [0x08, 0xFF, 0xFF, 0x00, 0x00, 0x0B, 0xCD, 0xAB];
378        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
379        assert_eq!(consumed, 8);
380        if let CommandPayload::CoordRealign(p) = &cmd.payload {
381            assert_eq!(p.panid, 0xFFFF);
382            assert_eq!(p.coord_address, 0x0000);
383            assert_eq!(p.channel, 11);
384            assert_eq!(p.dev_address, 0xABCD);
385        } else {
386            panic!("Expected CoordRealign payload");
387        }
388    }
389
390    #[test]
391    fn test_parse_gts_req() {
392        let buf = [0x09, 0x15];
393        let (cmd, consumed) = CommandFrame::parse(&buf, 0).unwrap();
394        assert_eq!(consumed, 2);
395        if let CommandPayload::GtsReq(p) = &cmd.payload {
396            assert_eq!(p.gts_len, 5);
397            assert!(p.gts_dir);
398            assert!(!p.charact_type);
399        } else {
400            panic!("Expected GtsReq payload");
401        }
402    }
403
404    #[test]
405    fn test_build_roundtrip_assoc_req() {
406        let cmd = CommandFrame {
407            cmd_id: types::cmd_id::ASSOC_REQ,
408            payload: CommandPayload::AssocReq(AssocReqPayload {
409                alternate_pan_coordinator: false,
410                device_type: true,
411                power_source: true,
412                receiver_on_when_idle: true,
413                security_capability: false,
414                allocate_address: true,
415            }),
416        };
417        let bytes = cmd.build();
418        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
419        assert_eq!(parsed, cmd);
420    }
421
422    #[test]
423    fn test_build_roundtrip_assoc_resp() {
424        let cmd = CommandFrame {
425            cmd_id: types::cmd_id::ASSOC_RESP,
426            payload: CommandPayload::AssocResp(AssocRespPayload {
427                short_address: 0x5678,
428                association_status: types::assoc_status::PAN_AT_CAPACITY,
429            }),
430        };
431        let bytes = cmd.build();
432        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
433        assert_eq!(parsed, cmd);
434    }
435
436    #[test]
437    fn test_build_roundtrip_disassoc() {
438        let cmd = CommandFrame {
439            cmd_id: types::cmd_id::DISASSOC_NOTIFY,
440            payload: CommandPayload::DisassocNotify(DisassocNotifyPayload {
441                reason: types::disassoc_reason::DEVICE_WISHES_TO_LEAVE,
442            }),
443        };
444        let bytes = cmd.build();
445        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
446        assert_eq!(parsed, cmd);
447    }
448
449    #[test]
450    fn test_build_roundtrip_data_req() {
451        let cmd = CommandFrame {
452            cmd_id: types::cmd_id::DATA_REQ,
453            payload: CommandPayload::DataReq,
454        };
455        let bytes = cmd.build();
456        assert_eq!(bytes, vec![0x04]);
457        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
458        assert_eq!(parsed, cmd);
459    }
460
461    #[test]
462    fn test_build_roundtrip_coord_realign() {
463        let cmd = CommandFrame {
464            cmd_id: types::cmd_id::COORD_REALIGN,
465            payload: CommandPayload::CoordRealign(CoordRealignPayload {
466                panid: 0x1234,
467                coord_address: 0x0000,
468                channel: 15,
469                dev_address: 0xFFFF,
470            }),
471        };
472        let bytes = cmd.build();
473        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
474        assert_eq!(parsed, cmd);
475    }
476
477    #[test]
478    fn test_build_roundtrip_gts_req() {
479        let cmd = CommandFrame {
480            cmd_id: types::cmd_id::GTS_REQ,
481            payload: CommandPayload::GtsReq(GtsReqPayload {
482                gts_len: 7,
483                gts_dir: true,
484                charact_type: true,
485            }),
486        };
487        let bytes = cmd.build();
488        let (parsed, _) = CommandFrame::parse(&bytes, 0).unwrap();
489        assert_eq!(parsed, cmd);
490    }
491
492    #[test]
493    fn test_parse_with_offset() {
494        let mut buf = vec![0xAA, 0xBB];
495        buf.push(0x04);
496        let (cmd, consumed) = CommandFrame::parse(&buf, 2).unwrap();
497        assert_eq!(consumed, 1);
498        assert_eq!(cmd.cmd_id, types::cmd_id::DATA_REQ);
499    }
500
501    #[test]
502    fn test_parse_buffer_too_short() {
503        let buf: [u8; 0] = [];
504        let result = CommandFrame::parse(&buf, 0);
505        assert!(result.is_err());
506    }
507
508    #[test]
509    fn test_summary() {
510        let cmd = CommandFrame {
511            cmd_id: types::cmd_id::ASSOC_REQ,
512            payload: CommandPayload::AssocReq(AssocReqPayload {
513                alternate_pan_coordinator: false,
514                device_type: true,
515                power_source: false,
516                receiver_on_when_idle: false,
517                security_capability: false,
518                allocate_address: true,
519            }),
520        };
521        let summary = cmd.summary();
522        assert!(summary.contains("AssocReq"));
523    }
524}