1use crate::layer::field::{FieldError, read_u16_le};
16
17use super::types;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct CommandFrame {
22 pub cmd_id: u8,
24 pub payload: CommandPayload,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum CommandPayload {
31 AssocReq(AssocReqPayload),
33 AssocResp(AssocRespPayload),
35 DisassocNotify(DisassocNotifyPayload),
37 DataReq,
39 PanIdConflict,
41 OrphanNotify,
43 BeaconReq,
45 CoordRealign(CoordRealignPayload),
47 GtsReq(GtsReqPayload),
49 Unknown(Vec<u8>),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct AssocReqPayload {
56 pub alternate_pan_coordinator: bool,
58 pub device_type: bool,
60 pub power_source: bool,
62 pub receiver_on_when_idle: bool,
64 pub security_capability: bool,
66 pub allocate_address: bool,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct AssocRespPayload {
73 pub short_address: u16,
75 pub association_status: u8,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct DisassocNotifyPayload {
82 pub reason: u8,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct CoordRealignPayload {
89 pub panid: u16,
91 pub coord_address: u16,
93 pub channel: u8,
95 pub dev_address: u16,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct GtsReqPayload {
102 pub gts_len: u8,
104 pub gts_dir: bool,
106 pub charact_type: bool,
108}
109
110impl CommandFrame {
111 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 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 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}