π³ ecr17-protocol
The Italian ECR17 payment protocol in pure Rust β drive Nexi Group POS terminals over LAN from any Rust app, plus a Tauri desktop control panel.
The most complete open-source ECR17 toolkit for Rust & the desktop.
π Using PHP / Laravel? β padosoft/laravel-ecr17 Β Β·Β π± Using React Native / mobile? β padosoft/react-native-ecr17-protocol β the same ECR17 protocol, ported to each ecosystem.
π Table of contents
- What is ECR17?
- Why this exists
- Highlights
- Screenshots
- Feature status
- Installation
- Quick start
- Money-safety
- Configuration
- API reference
- Events
- Tokenization & receipts
- Protocol cheat-sheet
- Architecture
- The Tauri control panel
- Testing
- Other ports
- Vibe-coding batteries included
- License
π§ What is ECR17?
ECR17 is the Italian standard protocol β supported by Nexi Group terminals β that integrates an Electronic Cash Register (ECR) with an EFT-POS payment terminal over a local LAN connection. The cash register sends a request (payment, reversal, statusβ¦), the terminal talks to the acquiring host, and replies synchronously.
This crate speaks that protocol from Rust: a pure, #![forbid(unsafe_code)]
protocol engine plus an async Ecr17Client and an optional tokio TCP
transport β no C, no FFI, no bridge.
π Official protocol reference (public): https://developer.nexigroup.com/traditionalpos/en-EU/docs/ β the authoritative source. Field positions, message codes and
lrcModemay vary by terminal/firmware; always check against the official docs.
π― Why this exists
Integrating Italian POS terminals has long been needlessly painful. The ECR17
protocol is not publicly documented β the specifications are shared under NDA,
mostly with established point-of-sale software vendors β so everyone else
reverse-engineers it by trial and error across terminals and firmware versions.
(The classic trap that blocks almost everyone: the LRC is computed over a base of
0x7F, not 0x00 β handled here, and configurable per terminal.)
A few community efforts exist for server-side languages, but there was nothing
idiomatic for Rust or the desktop. To our knowledge this is the most complete
open-source ECR17 toolkit for Rust: the full command set, response parsing, the
ACK/NAK + retransmit orchestration, configurable LRC modes, and payment-safety β
all tested, all async, all pure Rust.
The goal is simple: Rust and desktop developers should no longer struggle to
talk to Italian POS terminals. No NDA hunting, no guesswork β just
client.pay(&req).await.
π€ Compatibility notes (lrcMode, field quirks per terminal/firmware) are welcome as issues, so we can build, together, the reference the ecosystem never had.
β¨ Highlights
- π¦ Pure-Rust protocol core β framing / LRC / orchestration,
#![forbid(unsafe_code)], no FFI. - π Async,
Future-based API βclient.pay(&req).await, built on tokio. - π§± Full command set β payment, extended payment, reversal, pre-auth (request / incremental / closure), card verification, close session, totals, last result, ECR printing, reprint, VAS.
- π‘οΈ Robust by design β fixed-width field validation, defensive response parsing, ACK/NAK handshake with retransmit and timeouts.
- π° Money-safe β a financial command is never blindly re-sent after a reconnect (double-charge protection), locked by unit tests. Recover a lost response via
send_last_result()(specG). - π‘ Live events β progress messages, streamed receipt lines, connection state β via callbacks.
- π Transport-agnostic β I/O lives behind an async
Transporttrait; the real tokio TCP socket is one feature flag away, and the in-memoryFakeTransportmakes the whole stack testable with no hardware. - π₯οΈ Tauri control panel β a cross-platform desktop debug console that exercises every command and streams the behind-the-scenes log live.
- β Heavily tested β 108 Rust unit / flow / money-safety tests (LRC, codec, every builder, every parser, full session orchestration) plus 12 Vitest + 12 Playwright e2e for the UI, all green in CI.
- π€ Vibe-coding batteries included β ships first-class AI-agent context (
AGENTS.md,CLAUDE.md,docs/LESSON.md,PROGRESS.md). See below.
πΈ Screenshots
The repo ships a Tauri Control Panel app that exercises every ECR17 command against a real terminal and streams the behind-the-scenes log (sent / progress / receipt / result / error) live β with card PANs masked.
π Feature status
| Area | Status |
|---|---|
| Packet framing + LRC (4 modes) | β |
All request builders (P X p i c H U C T G E R K s S) |
β |
Response parsing (E/V/s/T/C/e/K, incl. DCC) |
β |
| Session orchestration (ACK/NAK, retransmit, timeout, progress/receipt) | β |
| Async client API + events | β |
Auto-reconnect, tokenization (U) flow, receipt streaming |
β |
| tokio TCP transport (non-destructive liveness probe) | β
(feature tokio-transport) |
| Tauri desktop control panel (Win / macOS / Linux) | β (CI-built installers) |
π¦ Installation
# Protocol core only (bring your own transport):
# With the real tokio TCP transport:
MSRV 1.85 (the TCP liveness probe uses std::task::Waker::noop).
π Quick start
use Duration;
use ;
use TcpTransport; // needs feature "tokio-transport"
async
Request structs are plain data β construct the fields you need and leave the
OptiononesNone. The optional..fields map straight onto the wire defaults.
π° Money-safety
This crate drives a terminal that charges real cards, so payment integrity is a first-class, tested invariant β not an afterthought:
- A financial command is never blindly re-sent.
pay,pay_extended,reverse,pre_auth,incremental_auth,pre_auth_closureare not replayed after a transport drop/reconnect (a blind retry can double-charge). The decision lives in one tiny, unit-locked place βshould_retry_after_reconnectβ which only ever allows retrying safe/idempotent ops (status,totals). - Recover a lost response, don't replay it. If the reply is lost mid-flight,
call
send_last_result()(the spec'sGcommand) to fetch the real outcome. - Proactive drop detection. Nexi terminals close the TCP socket between
transactions, so the transport does a non-destructive liveness probe
(
poll_peek) before sending β never writing bytes on the peer's protocol stream β so a financial command never even starts on a stale socket. - Reusable across reconnects. The session holds no sticky "disconnected" flag; it resets its per-transaction state, so a fresh transaction is never blocked by a previous drop.
These rules are guarded by regression tests (financial_command_not_replayed_on_drop,
safe_command_retried_after_reconnect, recovers_and_succeeds_after_reconnect, β¦)
that must stay green.
βοΈ Configuration
Ecr17Config: host (required), port?, terminal_id (required),
cash_register_id (required), lrc_mode?, keep_alive?, auto_reconnect?,
connection_timeout_ms?, response_timeout_ms?, ack_timeout_ms?,
receipt_drain_ms?, retry_count?, retry_delay_ms?, debug?.
All fields serde-camelCase on the wire (cash_register_id β cashRegisterId),
so the same config round-trips cleanly to the Tauri frontend / any JSON consumer.
π API reference
Every command is async and performs a full request/response exchange.
new / configuration are synchronous.
| Method | Command | Returns |
|---|---|---|
connect() / disconnect() / is_connected() |
β | Result<()> / () / bool |
status() |
s |
PosStatusResponse |
pay(&req) / pay_extended(&req) |
P / X |
PaymentResult |
reverse(&req) |
S |
ReversalResult |
pre_auth(&req) / incremental_auth(&req) / pre_auth_closure(&req) |
p / i / c |
PreAuthResult / PaymentResult |
verify_card(&req) |
H |
CardVerificationResult |
close_session() / totals() |
C / T |
CloseSessionResult / TotalsResult |
send_last_result() |
G |
PaymentResult |
enable_ecr_printing(bool) / reprint(bool) |
E / R |
Result<()> |
vas(&xml) |
K |
VasResult |
Commands require an open connection (connect() first) and error on
timeout / retransmission exhaustion / disconnect.
π‘ Events
client.set_on_progress;
client.set_on_receipt_line;
client.set_on_connection_state_change;
Callbacks are
Fn(..) + Send + Sync + 'static. They are invoked with no client lock held, so a callback may safely call back into the client.
π§Ύ Tokenization & receipts
// Tokenization: attach a contract to a payment / preAuth / verifyCard. The 'U'
// additional-data message is sent automatically (P -> ACK -> U -> ACK -> result).
use ;
let req = PaymentRequest ;
// Receipts printed by the ECR: enable printing, set receipt_drain_ms in the config,
// and receive lines via the event.
client.enable_ecr_printing.await?;
client.set_on_receipt_line;
π Protocol cheat-sheet
App frame: STX(0x02) Β· payload Β· ETX(0x03) Β· LRC. Progress: SOH(0x01) Β·
20 chars Β· EOT(0x04) (no LRC). Confirmation: ACK(0x06) / NAK(0x15) Β· ETX Β·
LRC. LRC = 0x7F XOR-folded; the framing bytes folded in are selectable via
lrc_mode (stx / std / noext / stx_noext). Status code is lowercase s;
a P payment request is 167 bytes; receipts arrive as one or more S
messages. Outcome map: 00βok, 01βko, 05βcardNotPresent, 09βunknownTag.
ποΈ Architecture
crates/ecr17-protocol/src/
βββ lrc.rs # LRC (4 modes, base 0x7F) + LrcMode
βββ codec.rs # framing: STXΒ·ETXΒ·SOHΒ·EOTΒ·ACKΒ·NAK + LRC (encode/decode)
βββ types.rs # request/result/enum data model (serde, camelCase)
βββ protocol.rs # request builders (all commands), fixed-width + validated
βββ response.rs # response field parsers -> plain structs (incl. DCC, PAN mask)
βββ retry.rs # π° RetryPolicy β a financial command is never replayed
βββ transport.rs # async Transport trait + in-memory FakeTransport (tests)
βββ session.rs # ACK/NAK + retransmit + timeout + receipt-drain orchestration
βββ client.rs # Ecr17Client async API + events + auto/proactive reconnect
βββ transport/tcp.rs # tokio TCP transport (feature "tokio-transport")
app/
βββ src-tauri/ # Rust backend: managed Ecr17Client, one #[tauri::command] per cmd, events
βββ src/ # React 19 + TypeScript + Vite control panel (typed IPC, useEcr17 hook)
Design. The codec / protocol / response layers are pure and sync β trivially
unit-testable with no runtime. I/O lives behind the async Transport trait, so the
session/client run against a scripted FakeTransport in tests (including simulated
mid-exchange drops) and a real tokio socket in production.
π₯οΈ The Tauri control panel
app/ is a cross-platform Tauri 2 desktop app (React 19 + TypeScript + Vite)
that holds an Ecr17Client in managed state and exposes one #[tauri::command]
per protocol command. Itβs the fastest way to poke a terminal: pick a command,
fill the dynamically-generated parameters sheet (money fields coerce β¬ β cents),
hit run, and watch the sent/progress/receipt/result log stream live β PANs masked.
Native installers (Windows .msi/NSIS, macOS .dmg, Linux .deb/AppImage) are
built in CI and attached to each GitHub Release.
π§ͺ Testing
# Exactly what CI (rust-tests) runs:
# Frontend (from app/):
&&
The Rust tests cover LRC, packet (de)framing edge cases, every builder's byte
layout, every response parser, and the documented payment / reversal / re-pay /
progress / receipt / NAK-retransmit / timeout / reconnect-recovery flows
(against an in-memory FakeTransport).
Against a real terminal (opt-in)
An #[ignore]d integration test runs the core over a real TCP socket. It is
skipped unless ECR17_TEST_HOST is set:
ECR17_TEST_HOST=192.168.1.50 ECR17_TEST_PORT=10000 \
π§© Other ports
The same ECR17 protocol, maintained across ecosystems:
| Port | Repo | Stack |
|---|---|---|
| Rust / Tauri (this) | padosoft/rust-ecr17-protocol | Rust core + tokio + Tauri desktop |
| React Native | padosoft/react-native-ecr17-protocol | C++ core + Nitro (iOS/Android) |
| Laravel | padosoft/laravel-ecr17 | PHP / Laravel package + console |
π€ Vibe-coding batteries included
Building on an undocumented payment protocol is exactly where AI assistants get things subtly wrong. This repo ships the context to prevent that, so an agent (or a new contributor) is productive and safe from minute one:
AGENTS.md/CLAUDE.mdβ project guide, the mandatory per-task workflow, CI strategy, and the money-critical rules (e.g. never blindly retry a payment).docs/LESSON.mdβ accumulated, verified engineering lessons (Rust/Tauri APIs, toolchain traps, protocol facts, money-safety) β the gotchas already solved.PROGRESS.mdβ crash-safe resume state across sessions.
The result: less hallucination, fewer footguns, and changes that respect the payment-safety invariants by default.
π License
Disclaimer: independent integration library. "ECR17", "Nexi" and related marks belong to their respective owners and are referenced for interoperability only.