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 #[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 #[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}