1#![cfg_attr(docsrs, feature(doc_cfg))]
2use bitflags::bitflags;
5use bytes::{Buf, BufMut, Bytes, BytesMut};
6use thiserror::Error;
7
8pub const HEADER_SIZE: usize = 8;
10
11bitflags! {
12 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14 pub struct CommandFlags: u16 {
15 const ACK_REQUIRED = 0x0001;
17 const BROADCAST = 0x8000;
19 const SCHEDULED_ACTION = 0x0002;
24 }
25}
26
27pub const PENDING_ACK_COMMAND: u16 = 0x0805;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum OpCode {
45 ReadRegister,
47 WriteRegister,
49 ReadMem,
51 WriteMem,
53}
54
55impl OpCode {
56 pub const fn command_code(self) -> u16 {
58 match self {
59 OpCode::ReadRegister => 0x0080,
60 OpCode::WriteRegister => 0x0082,
61 OpCode::ReadMem => 0x0084,
62 OpCode::WriteMem => 0x0086,
63 }
64 }
65
66 pub const fn ack_code(self) -> u16 {
68 self.command_code() + 1
69 }
70
71 #[allow(dead_code)]
72 fn from_command(code: u16) -> Result<Self, GenCpError> {
73 match code {
74 0x0080 => Ok(OpCode::ReadRegister),
75 0x0082 => Ok(OpCode::WriteRegister),
76 0x0084 => Ok(OpCode::ReadMem),
77 0x0086 => Ok(OpCode::WriteMem),
78 _ => Err(GenCpError::UnknownOpcode(code)),
79 }
80 }
81
82 fn from_ack(code: u16) -> Result<Self, GenCpError> {
83 match code {
84 0x0081 => Ok(OpCode::ReadRegister),
85 0x0083 => Ok(OpCode::WriteRegister),
86 0x0085 => Ok(OpCode::ReadMem),
87 0x0087 => Ok(OpCode::WriteMem),
88 _ => Err(GenCpError::UnknownOpcode(code)),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum StatusCode {
106 Success,
108 NotImplemented,
110 InvalidParameter,
112 InvalidAddress,
114 WriteProtect,
118 BadAlignment,
120 AccessDenied,
126 Busy,
130 GenericError,
132 Unknown(u16),
135}
136
137impl StatusCode {
138 pub fn from_raw(raw: u16) -> Self {
140 match raw {
141 0x0000 => StatusCode::Success,
142 0x8001 => StatusCode::NotImplemented,
143 0x8002 => StatusCode::InvalidParameter,
144 0x8003 => StatusCode::InvalidAddress,
145 0x8004 => StatusCode::WriteProtect,
146 0x8005 => StatusCode::BadAlignment,
147 0x8006 => StatusCode::AccessDenied,
148 0x8007 => StatusCode::Busy,
149 0x8FFF => StatusCode::GenericError,
150 other => StatusCode::Unknown(other),
151 }
152 }
153
154 pub const fn to_raw(self) -> u16 {
156 match self {
157 StatusCode::Success => 0x0000,
158 StatusCode::NotImplemented => 0x8001,
159 StatusCode::InvalidParameter => 0x8002,
160 StatusCode::InvalidAddress => 0x8003,
161 StatusCode::WriteProtect => 0x8004,
162 StatusCode::BadAlignment => 0x8005,
163 StatusCode::AccessDenied => 0x8006,
164 StatusCode::Busy => 0x8007,
165 StatusCode::GenericError => 0x8FFF,
166 StatusCode::Unknown(code) => code,
167 }
168 }
169
170 pub const fn name(self) -> &'static str {
172 match self {
173 StatusCode::Success => "SUCCESS",
174 StatusCode::NotImplemented => "NOT_IMPLEMENTED",
175 StatusCode::InvalidParameter => "INVALID_PARAMETER",
176 StatusCode::InvalidAddress => "INVALID_ADDRESS",
177 StatusCode::WriteProtect => "WRITE_PROTECT",
178 StatusCode::BadAlignment => "BAD_ALIGNMENT",
179 StatusCode::AccessDenied => "ACCESS_DENIED",
180 StatusCode::Busy => "BUSY",
181 StatusCode::GenericError => "ERROR",
182 StatusCode::Unknown(_) => "unknown status",
183 }
184 }
185
186 pub const fn is_retryable(self) -> bool {
191 matches!(self, StatusCode::Busy)
192 }
193}
194
195impl std::fmt::Display for StatusCode {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(f, "{} (0x{:04X})", self.name(), self.to_raw())
204 }
205}
206
207#[derive(Debug, Error)]
209#[non_exhaustive]
210pub enum GenCpError {
211 #[error("invalid packet: {0}")]
212 InvalidPacket(&'static str),
213 #[error("unknown opcode: {0:#06x}")]
214 UnknownOpcode(u16),
215 #[error("io: {0}")]
216 Io(#[from] std::io::Error),
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct CommandHeader {
222 pub flags: CommandFlags,
224 pub opcode: OpCode,
226 pub length: u16,
228 pub request_id: u16,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct AckHeader {
235 pub status: StatusCode,
237 pub opcode: OpCode,
239 pub length: u16,
241 pub request_id: u16,
243}
244
245#[derive(Debug, Clone)]
247pub struct GenCpCmd {
248 pub header: CommandHeader,
250 pub payload: Bytes,
252}
253
254#[derive(Debug, Clone)]
256pub struct GenCpAck {
257 pub header: AckHeader,
259 pub payload: Bytes,
261}
262
263pub fn encode_cmd(cmd: &GenCpCmd) -> Bytes {
267 debug_assert_eq!(cmd.header.length as usize, cmd.payload.len());
268 let mut buffer = BytesMut::with_capacity(HEADER_SIZE + cmd.payload.len());
269 buffer.put_u16(cmd.header.flags.bits());
270 buffer.put_u16(cmd.header.opcode.command_code());
271 buffer.put_u16(cmd.header.length);
272 buffer.put_u16(cmd.header.request_id);
273 buffer.extend_from_slice(&cmd.payload);
274 buffer.freeze()
275}
276
277pub fn decode_ack(buf: &[u8]) -> Result<GenCpAck, GenCpError> {
279 if buf.len() < HEADER_SIZE {
280 return Err(GenCpError::InvalidPacket("too short"));
281 }
282 let mut cursor = buf;
283 let status_raw = cursor.get_u16();
284 let opcode_raw = cursor.get_u16();
285 let length = cursor.get_u16();
286 let request_id = cursor.get_u16();
287
288 let expected = HEADER_SIZE + length as usize;
289 if buf.len() != expected {
290 return Err(GenCpError::InvalidPacket("length mismatch"));
291 }
292
293 let opcode = OpCode::from_ack(opcode_raw)?;
294 let status = StatusCode::from_raw(status_raw);
295
296 let payload = Bytes::copy_from_slice(&buf[HEADER_SIZE..]);
297 Ok(GenCpAck {
298 header: AckHeader {
299 status,
300 opcode,
301 length,
302 request_id,
303 },
304 payload,
305 })
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 const SPEC_STATUS_TABLE: &[(u16, StatusCode, &str)] = &[
317 (0x0000, StatusCode::Success, "SUCCESS"),
318 (0x8001, StatusCode::NotImplemented, "NOT_IMPLEMENTED"),
319 (0x8002, StatusCode::InvalidParameter, "INVALID_PARAMETER"),
320 (0x8003, StatusCode::InvalidAddress, "INVALID_ADDRESS"),
321 (0x8004, StatusCode::WriteProtect, "WRITE_PROTECT"),
322 (0x8005, StatusCode::BadAlignment, "BAD_ALIGNMENT"),
323 (0x8006, StatusCode::AccessDenied, "ACCESS_DENIED"),
324 (0x8007, StatusCode::Busy, "BUSY"),
325 (0x8FFF, StatusCode::GenericError, "ERROR"),
326 ];
327
328 #[test]
329 fn status_table_matches_the_specification() {
330 for &(raw, expected, name) in SPEC_STATUS_TABLE {
331 assert_eq!(
332 StatusCode::from_raw(raw),
333 expected,
334 "decoding {raw:#06x} ({name})"
335 );
336 assert_eq!(expected.to_raw(), raw, "re-encoding {name}");
337 assert_eq!(expected.name(), name);
338 }
339 }
340
341 #[test]
342 fn regression_the_three_codes_we_used_to_mislabel() {
343 assert_eq!(StatusCode::from_raw(0x8004), StatusCode::WriteProtect);
346 assert!(!StatusCode::from_raw(0x8004).is_retryable());
347
348 assert_eq!(StatusCode::from_raw(0x8005), StatusCode::BadAlignment);
350
351 assert_eq!(StatusCode::from_raw(0x8006), StatusCode::AccessDenied);
354 assert_eq!(StatusCode::from_raw(32774), StatusCode::AccessDenied);
355
356 assert!(StatusCode::from_raw(0x8007).is_retryable());
358 assert!(!StatusCode::from_raw(0x0000).is_retryable());
359 }
360
361 #[test]
362 fn display_carries_both_the_name_and_the_raw_value() {
363 assert_eq!(
364 StatusCode::AccessDenied.to_string(),
365 "ACCESS_DENIED (0x8006)"
366 );
367 assert_eq!(
369 StatusCode::from_raw(0x800C).to_string(),
370 "unknown status (0x800C)"
371 );
372 }
373
374 #[test]
375 fn transport_specific_codes_stay_unknown_here() {
376 assert_eq!(StatusCode::from_raw(0x800B), StatusCode::Unknown(0x800B));
381 assert_eq!(StatusCode::from_raw(0x800C), StatusCode::Unknown(0x800C));
383 assert_eq!(StatusCode::from_raw(0xA001), StatusCode::Unknown(0xA001));
384 assert_eq!(StatusCode::from_raw(0x800B).to_raw(), 0x800B);
386 }
387
388 #[test]
389 fn pending_ack_is_a_command_id_not_a_status() {
390 assert_eq!(PENDING_ACK_COMMAND, 0x0805);
392 assert_ne!(PENDING_ACK_COMMAND, StatusCode::AccessDenied.to_raw());
393 assert_eq!(OpCode::ReadMem.command_code(), 0x0084);
395 assert_eq!(OpCode::WriteMem.command_code(), 0x0086);
396 }
397
398 #[test]
399 fn encode_read_register_roundtrip() {
400 let payload = {
401 let mut p = BytesMut::with_capacity(4);
402 p.put_u32(0x0000_0a00);
403 p.freeze()
404 };
405 let cmd = GenCpCmd {
406 header: CommandHeader {
407 flags: CommandFlags::ACK_REQUIRED,
408 opcode: OpCode::ReadRegister,
409 length: payload.len() as u16,
410 request_id: 0x41,
411 },
412 payload,
413 };
414
415 let encoded = encode_cmd(&cmd);
416 assert_eq!(
417 &encoded[..2],
418 &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
419 );
420 assert_eq!(&encoded[2..4], &0x0080u16.to_be_bytes());
421 assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
422 assert_eq!(&encoded[6..8], &0x0041u16.to_be_bytes());
423 assert_eq!(&encoded[8..], &cmd.payload[..]);
424 }
425
426 #[test]
427 fn encode_write_register_roundtrip() {
428 let payload = {
429 let mut p = BytesMut::with_capacity(8);
430 p.put_u32(0x0000_0a00);
431 p.put_u32(0x0000_0002);
432 p.freeze()
433 };
434 let cmd = GenCpCmd {
435 header: CommandHeader {
436 flags: CommandFlags::ACK_REQUIRED,
437 opcode: OpCode::WriteRegister,
438 length: payload.len() as u16,
439 request_id: 0x43,
440 },
441 payload,
442 };
443
444 let encoded = encode_cmd(&cmd);
445 assert_eq!(
446 &encoded[..2],
447 &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
448 );
449 assert_eq!(&encoded[2..4], &0x0082u16.to_be_bytes());
450 assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
451 assert_eq!(&encoded[6..8], &0x0043u16.to_be_bytes());
452 assert_eq!(&encoded[8..], &cmd.payload[..]);
453 }
454
455 #[test]
456 fn decode_read_register_ack() {
457 let value = 0x0000_0002u32;
458 let mut buf = BytesMut::with_capacity(HEADER_SIZE + 4);
459 buf.put_u16(0x0000);
460 buf.put_u16(0x0081);
461 buf.put_u16(4);
462 buf.put_u16(0x4141);
463 buf.put_u32(value);
464
465 let ack = decode_ack(&buf).expect("decode");
466 assert_eq!(ack.header.status, StatusCode::Success);
467 assert_eq!(ack.header.opcode, OpCode::ReadRegister);
468 assert_eq!(ack.header.length, 4);
469 assert_eq!(ack.header.request_id, 0x4141);
470 assert_eq!(&ack.payload[..], &value.to_be_bytes());
471 }
472
473 #[test]
474 fn decode_write_register_ack() {
475 let index = 1u32;
476 let mut buf = BytesMut::with_capacity(HEADER_SIZE + 4);
477 buf.put_u16(0x0000);
478 buf.put_u16(0x0083);
479 buf.put_u16(4);
480 buf.put_u16(0x4343);
481 buf.put_u32(index);
482
483 let ack = decode_ack(&buf).expect("decode");
484 assert_eq!(ack.header.status, StatusCode::Success);
485 assert_eq!(ack.header.opcode, OpCode::WriteRegister);
486 assert_eq!(ack.header.length, 4);
487 assert_eq!(ack.header.request_id, 0x4343);
488 assert_eq!(&ack.payload[..], &index.to_be_bytes());
489 }
490
491 #[test]
492 fn encode_read_mem_roundtrip() {
493 let payload = {
494 let mut p = BytesMut::with_capacity(12);
495 p.put_u64(0x0010_0200);
496 p.put_u32(64);
497 p.freeze()
498 };
499 let cmd = GenCpCmd {
500 header: CommandHeader {
501 flags: CommandFlags::ACK_REQUIRED,
502 opcode: OpCode::ReadMem,
503 length: payload.len() as u16,
504 request_id: 0x42,
505 },
506 payload,
507 };
508
509 let encoded = encode_cmd(&cmd);
510 assert_eq!(
511 &encoded[..2],
512 &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
513 );
514 assert_eq!(&encoded[2..4], &0x0084u16.to_be_bytes());
515 assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
516 assert_eq!(&encoded[6..8], &0x0042u16.to_be_bytes());
517 assert_eq!(&encoded[8..], &cmd.payload[..]);
518 }
519
520 #[test]
521 fn decode_read_mem_ack() {
522 let payload = vec![0xAA; 4];
523 let mut buf = BytesMut::with_capacity(HEADER_SIZE + payload.len());
524 buf.put_u16(0x0000);
525 buf.put_u16(0x0085);
526 buf.put_u16(payload.len() as u16);
527 buf.put_u16(0x4242);
528 buf.extend_from_slice(&payload);
529
530 let ack = decode_ack(&buf).expect("decode");
531 assert_eq!(ack.header.status, StatusCode::Success);
532 assert_eq!(ack.header.opcode, OpCode::ReadMem);
533 assert_eq!(ack.header.length as usize, payload.len());
534 assert_eq!(ack.header.request_id, 0x4242);
535 assert_eq!(&ack.payload[..], &payload[..]);
536 }
537
538 #[test]
539 fn decode_write_mem_ack() {
540 let payload: Vec<u8> = Vec::new();
541 let mut buf = BytesMut::with_capacity(HEADER_SIZE + payload.len());
542 buf.put_u16(0x0000);
543 buf.put_u16(0x0087);
544 buf.put_u16(0);
545 buf.put_u16(0x1001);
546 let ack = decode_ack(&buf).expect("decode");
547 assert_eq!(ack.header.opcode, OpCode::WriteMem);
548 assert_eq!(ack.header.status, StatusCode::Success);
549 assert_eq!(ack.payload.len(), 0);
550 }
551
552 const GOLDEN_READ_REGISTER_ACK: [u8; 12] = [
572 0x00, 0x00, 0x00, 0x81, 0x00, 0x04, 0x12, 0x34, 0x00, 0x00, 0x3E, 0xF2, ];
578
579 #[test]
580 fn ack_header_fields_sit_at_the_specified_offsets() {
581 let b = &GOLDEN_READ_REGISTER_ACK;
582 assert_eq!(u16::from_be_bytes([b[0], b[1]]), 0x0000, "status at 0");
583 assert_eq!(u16::from_be_bytes([b[2], b[3]]), 0x0081, "ack id at 2");
584 assert_eq!(u16::from_be_bytes([b[4], b[5]]), 4, "length at 4");
585 assert_eq!(u16::from_be_bytes([b[6], b[7]]), 0x1234, "request id at 6");
586 assert_eq!(HEADER_SIZE, 8, "the payload begins at offset 8");
587
588 let ack = decode_ack(b).expect("decode the golden ack");
589 assert_eq!(ack.header.status, StatusCode::Success);
590 assert_eq!(ack.header.opcode, OpCode::ReadRegister);
591 assert_eq!(ack.header.length, 4);
592 assert_eq!(ack.header.request_id, 0x1234);
593 assert_eq!(&ack.payload[..], &[0x00, 0x00, 0x3E, 0xF2]);
594 }
595
596 #[test]
602 fn ack_length_counts_the_payload_only() {
603 let mut counts_the_header = GOLDEN_READ_REGISTER_ACK;
604 counts_the_header[4..6].copy_from_slice(&12u16.to_be_bytes());
605 assert!(
606 decode_ack(&counts_the_header).is_err(),
607 "a length of 12 describes a 20-byte datagram, not this one"
608 );
609
610 let mut over_promises = GOLDEN_READ_REGISTER_ACK;
612 over_promises[4..6].copy_from_slice(&8u16.to_be_bytes());
613 assert!(decode_ack(&over_promises).is_err());
614 }
615
616 #[test]
619 fn ack_ids_are_command_ids_plus_one() {
620 for (cmd, ack) in [
621 (0x0080u16, 0x0081u16),
622 (0x0082, 0x0083),
623 (0x0084, 0x0085),
624 (0x0086, 0x0087),
625 ] {
626 assert_eq!(
627 OpCode::from_command(cmd).expect("command id").ack_code(),
628 ack
629 );
630 assert!(
631 OpCode::from_ack(cmd).is_err(),
632 "{cmd:#06x} is a command id, not an acknowledgement"
633 );
634 }
635 }
636
637 #[test]
641 fn pending_ack_is_not_a_normal_acknowledgement() {
642 let mut pending = GOLDEN_READ_REGISTER_ACK;
643 pending[2..4].copy_from_slice(&PENDING_ACK_COMMAND.to_be_bytes());
644 assert_eq!(
645 u16::from_be_bytes([pending[0], pending[1]]),
646 0x0000,
647 "a pending-ack reports SUCCESS — the status cannot be the signal"
648 );
649 assert!(
650 decode_ack(&pending).is_err(),
651 "0x0805 is not an acknowledgement command id"
652 );
653 }
654
655 #[test]
658 fn command_header_fields_sit_at_the_specified_offsets() {
659 let cmd = GenCpCmd {
660 header: CommandHeader {
661 flags: CommandFlags::ACK_REQUIRED,
662 opcode: OpCode::ReadMem,
663 length: 8,
664 request_id: 0x00AB,
665 },
666 payload: Bytes::from_static(&[0, 0, 0x0D, 0x04, 0, 0, 0, 4]),
667 };
668 let b = encode_cmd(&cmd);
669
670 assert_eq!(u16::from_be_bytes([b[0], b[1]]), 0x0001, "flags at 0");
671 assert_eq!(u16::from_be_bytes([b[2], b[3]]), 0x0084, "READMEM_CMD at 2");
672 assert_eq!(u16::from_be_bytes([b[4], b[5]]), 8, "payload length at 4");
673 assert_eq!(u16::from_be_bytes([b[6], b[7]]), 0x00AB, "request id at 6");
674 assert_eq!(b.len(), HEADER_SIZE + 8);
675 }
676}