hdm_am/wire.rs
1//! Wire framing: magic, operation codes, request/response envelopes.
2
3use crate::error::Error;
4use std::io::{Read, Write};
5
6/// 6-byte magic prefix: UTF-8 encoding of "ՀԴՄ" (HDM).
7pub const MAGIC: [u8; 6] = [0xD5, 0x80, 0xD4, 0xB4, 0xD5, 0x84];
8
9/// Wire framing version advertised in the request header (big-endian).
10///
11/// This is the *framing* version, not the document version ([`crate::SPEC_VERSION`]): it went
12/// `03→04→05` across early manuals and has been frozen at `05` since spec v0.5 (2017), through
13/// v0.7.3. Spec v0.7.3 still documents `05`, so this is correct, not stale.
14///
15/// A device reports its own protocol version in the *response* header, which may differ from what
16/// we send — documented behaviour since v0.3. [`crate::identify`] therefore matches on the major
17/// version only.
18pub const PROTOCOL_VERSION: [u8; 2] = [0x00, 0x05];
19
20/// Length of the response header preceding the encrypted payload.
21pub const RESPONSE_HEADER_LEN: usize = 2 + 3 + 2 + 2 + 2;
22
23/// Successful response code (200) per spec §4.10.
24pub const RESPONSE_CODE_OK: u16 = 200;
25
26/// HDM protocol operation codes. Codes 1-10 are v0.5; 11-16 were added through v0.7.3.
27#[repr(u8)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum OperationCode {
30 /// List active operators and departments.
31 ListOpsAndDeps = 1,
32 /// Operator login. Returns the session key used for all subsequent ops.
33 OperatorLogin = 2,
34 /// Operator logout.
35 OperatorLogout = 3,
36 /// Print a fiscal receipt.
37 PrintReceipt = 4,
38 /// Reprint a copy of the last receipt.
39 PrintLastReceipt = 5,
40 /// Print a return / refund receipt (full, by-amount, or per-item). Registers the return.
41 PrintReturnReceipt = 6,
42 /// Configure receipt header and footer text lines.
43 SetupHeaderFooter = 7,
44 /// Upload the receipt header logo image (Base64 BMP, colour depth ≤4 bits).
45 SetupHeaderLogo = 8,
46 /// Print a fiscal report (1 = X-report, 2 = Z-report).
47 PrintFiscalReport = 9,
48 /// Get (look up) the contents of a receipt you intend to return. Read-only.
49 GetReturnableReceipt = 10,
50 /// Cash drawer in/out adjustment.
51 CashInOut = 11,
52 /// Query device date and time.
53 DateTime = 12,
54 /// Receipt sample (test print).
55 ReceiptSample = 13,
56 /// Synchronise HDM with tax authority.
57 HdmTimeSync = 14,
58 /// List available payment systems (returns codes 1-18, see spec §4.8).
59 PaymentSystemsList = 15,
60 /// Submit a single eMark traceability code.
61 SingleEmark = 16,
62}
63
64/// Encoded request to be written to the wire.
65#[derive(Debug)]
66pub struct Request {
67 /// Operation code.
68 pub op: OperationCode,
69 /// Encrypted payload (ciphertext only — the header is added on encode).
70 pub payload: Vec<u8>,
71}
72
73impl Request {
74 /// Encode into the on-the-wire byte stream.
75 ///
76 /// # Errors
77 /// Returns [`Error::Transport`] if `payload` exceeds `u16::MAX` bytes (HDM frames have a 2-byte length field).
78 pub fn encode(&self, writer: &mut impl Write) -> Result<(), Error> {
79 let len = u16::try_from(self.payload.len()).map_err(|_| {
80 Error::Transport(std::io::Error::new(
81 std::io::ErrorKind::InvalidInput,
82 "payload exceeds u16::MAX",
83 ))
84 })?;
85 writer.write_all(&MAGIC)?;
86 writer.write_all(&PROTOCOL_VERSION)?;
87 writer.write_all(&[self.op as u8, 0])?;
88 writer.write_all(&len.to_be_bytes())?;
89 writer.write_all(&self.payload)?;
90 Ok(())
91 }
92}
93
94/// Decoded response header. The encrypted payload follows immediately on the wire.
95#[derive(Debug, Clone, Copy)]
96pub struct ResponseHeader {
97 /// HDM protocol version reported by the device (major, minor).
98 pub protocol_version: (u8, u8),
99 /// HDM firmware version (major, minor, patch).
100 pub software_version: (u8, u8, u8),
101 /// Response code per spec §4.10. 200 = success.
102 pub code: u16,
103 /// Length of the encrypted payload that follows.
104 pub payload_len: u16,
105}
106
107impl ResponseHeader {
108 /// Read and parse the 11-byte response header from a transport.
109 ///
110 /// # Errors
111 /// Returns [`Error::Transport`] on read failure.
112 pub fn read(reader: &mut impl Read) -> Result<Self, Error> {
113 let mut buf = [0u8; RESPONSE_HEADER_LEN];
114 reader.read_exact(&mut buf)?;
115 let [
116 pv_major,
117 pv_minor,
118 sw_major,
119 sw_minor,
120 sw_patch,
121 code_hi,
122 code_lo,
123 len_hi,
124 len_lo,
125 _r0,
126 _r1,
127 ] = buf;
128 Ok(Self {
129 protocol_version: (pv_major, pv_minor),
130 software_version: (sw_major, sw_minor, sw_patch),
131 code: u16::from_be_bytes([code_hi, code_lo]),
132 payload_len: u16::from_be_bytes([len_hi, len_lo]),
133 })
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::io::Cursor;
141
142 #[test]
143 fn magic_is_hdm_in_utf8() {
144 assert_eq!(std::str::from_utf8(&MAGIC).unwrap(), "ՀԴՄ");
145 }
146
147 #[test]
148 fn request_encode_round_trips_through_header() {
149 let req = Request {
150 op: OperationCode::OperatorLogin,
151 payload: vec![0xAA; 32],
152 };
153 let mut buf = Vec::new();
154 req.encode(&mut buf).unwrap();
155
156 let [
157 m0,
158 m1,
159 m2,
160 m3,
161 m4,
162 m5,
163 pv0,
164 pv1,
165 op,
166 reserved,
167 len_hi,
168 len_lo,
169 ..,
170 ] = buf.as_slice()
171 else {
172 panic!("encoded buffer too short");
173 };
174 assert_eq!([*m0, *m1, *m2, *m3, *m4, *m5], MAGIC);
175 assert_eq!([*pv0, *pv1], PROTOCOL_VERSION);
176 assert_eq!(*op, OperationCode::OperatorLogin as u8);
177 assert_eq!(*reserved, 0);
178 assert_eq!(u16::from_be_bytes([*len_hi, *len_lo]), 32);
179 }
180
181 #[test]
182 fn response_header_decodes() {
183 let raw = [
184 0x00, 0x05, 0x02, 0x02, 0x10, 0x00, 0xC8, 0x00, 0x10, 0x00, 0x00,
185 ];
186 let mut cursor = Cursor::new(raw);
187 let hdr = ResponseHeader::read(&mut cursor).unwrap();
188 assert_eq!(hdr.protocol_version, (0, 5));
189 assert_eq!(hdr.software_version, (2, 2, 16));
190 assert_eq!(hdr.code, RESPONSE_CODE_OK);
191 assert_eq!(hdr.payload_len, 16);
192 }
193}