hdm-am
A Rust client for the Armenian fiscal cash register protocol — the spec published by the State Revenue Committee of Armenia (ՊԵԿ) for integrating external (commercial) software with fiscal cash registers (Հսկիչ Դրամարկղային Մեքենա — HDM, ՀԴՄ).
The workspace contains two crates:
hdm-am— the library: wire framing, encryption, and one typed request/response per operation.hdm-am-cli— a thin command-line tool (binaryhdm) that maps subcommands onto the library.
Scope
The crate speaks the HDM TCP protocol directly:
- 12-byte fixed header with
D5 80 D4 B4 D5 84magic ("ՀԴՄ" in UTF-8), 2-byte protocol version, 1-byte operation code, 2-byte big-endian payload length. - 3DES-ECB-PKCS7-encrypted JSON payloads.
- Two-key model: a SHA-256-derived password key for operator login (and the operator/department listing), a session key returned by login for everything after.
- All 16 operations from spec v0.7.3.
It does not handle the surrounding business logic — selecting an HDM device, persisting fiscal receipts, deciding what to print. That belongs to the consumer.
Operation coverage
All 16 operations are implemented and unit-tested. Hardware behaviour is recorded per device and firmware below — the protocol is the same across terminals, but what a given firmware actually does (logo rendering, returns, eMark validation) varies, so the status column is firmware-specific.
Tested devices
| Device | OS / build | Firmware | HDM protocol / software |
|---|---|---|---|
| Newland N950 | NDroid 6 / Android 12 (SKQ1.220119.001) |
D_03_51_00_01010000 |
0.7 / 1.1.0 |
The device reports protocol version 0.7 (matching spec v0.7.x) in its responses, yet accepts the 0.5-framed requests this crate sends — so hdm probe keys identity on the protocol major version, not an exact match.
Per-operation status
Each row is keyed by the protocol operation code (the byte sent on the wire, 1–16) and its function name.
| Code | Function | Newland N950 (D_03_51_00_01010000) |
|---|---|---|
| 1 | List operators & departments | OK |
| 2 | Operator login | OK |
| 3 | Operator logout | OK |
| 4 | Print receipt | OK — registered a real fiscal sale |
| 5 | Reprint last receipt | OK |
| 6 | Get returnable receipt (lookup) | Blocked — returns 503 for any input [1] |
| 7 | Header / footer config | OK |
| 8 | Header logo | Accepted (200) but not rendered [2] |
| 9 | Fiscal report (X / Z) | OK — both; a Z-report does not lock the device [3] |
| 10 | Print return | Blocked — returns 174 for any input [1] |
| 11 | Cash in / out | OK — both directions |
| 12 | Date / time | OK |
| 13 | Receipt sample | OK |
| 14 | Time sync | OK |
| 15 | Payment systems list | OK |
| 16 | Single eMark | Error path only [4] |
Notes:
- Returns (get returnable receipt and print return). Both are wire-correct but rejected server-side on this firmware. They key on a
Receipt_IDthat lives only in the receipt-print response'sqrfield — and this firmware omitsqrentirely (confirmed on the raw decrypted payload, not justnull). So the lookup returns vendor code503and the return returns174("receipt to return does not exist") for every identifier tried (rseq, fiscal number, zero-padded).crnis correct (a wrongcrngives175). A realReceipt_IDmust come out-of-band. The lookup response shape (ReturnableReceiptResponse) is therefore modelled from the spec alone and is unverified — see its doc comment. - Header logo. The protocol accepts a Base64 BMP and returns success, but no custom logo prints (tried 1-bit BMP/PNG at 384×4 and 384×64). The firmware appears to ignore custom header logos.
- Z-report. Verified that a Z-report closes the fiscal shift without locking the device — the next receipt opens a new fiscal day.
- Single eMark. Only the error path is verified: malformed codes return
195. The success path needs a real, registered GS1 Data Matrix code from a marked product.
Behaviour on other models or firmware versions is unknown — additions to this table are welcome.
Library usage
use TcpStream;
use Duration;
use ;
let tcp = connect?;
tcp.set_read_timeout?;
let mut client = new;
client.login?; // cashier id + PIN
let receipt = client.print_receipt?;
println!;
client.logout?;
All monetary and quantity fields use rust_decimal::Decimal (re-exported as hdm_am::Decimal); the wire encoding stays a JSON number.
CLI usage
Connection parameters come from flags or the HDM_* environment variables:
Irreversible operations (receipt, return, cash, Z-report) prompt for confirmation unless --yes is passed. -v/-vv raise log verbosity (logs go to stderr; -vv traces the raw decrypted payloads).
Source spec
The State Revenue Committee of Armenia publishes the integration manual as a PDF on src.am. This crate targets v0.7.3 (2025-04, 34 pages). The original and an unofficial English translation are checked in for offline reference:
docs/history/hdm-protocol-v0.7.3-2025.pdf— original Armenian spec from src.am, authoritative. It is the newest entry in the version archive below.docs/spec.md— English translation (unofficial; for developer convenience). Where the two disagree, trust the PDF — translator's notes inspec.mdflag the corrections.docs/history/— every published revision from v0.3 (2015) to v0.7.3, archived offline with a per-version index and a wire-protocol changelog. The transport envelope (framing, 3DES-ECB, SHA-256 keys) has been stable since v0.3; the header version byte has been05since v0.5.
Machine-readable schema
JSON Schema for every request/response payload lives in docs/schema/ — one file
per type, generated from the Rust types behind the schema feature so they can't drift from the
code. They cover the decrypted JSON bodies (not the binary framing / 3DES envelope); money fields are
JSON numbers and integer-coded enums are integers, matching the wire.
Design
Client<T: Read + Write, S: SequenceProvider>is generic over its transport and its sequence-number provider — pass aTcpStream+InMemorySeq/FileSeqin production, or any mock in tests.- Synchronous API. Consumers needing async should wrap calls in
tokio::task::spawn_blockingor similar. - No global state. Each
Clientowns its session key and sequence counter. - Sequence-counter persistence is the consumer's choice (
InMemorySeq,FileSeq, or a customSequenceProvider).
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.