fix(batm3): get cash-in working — EBDS escrow latch + correct serial device paths #79

Merged
padreug merged 3 commits from fix/ebds-escrow-stack-latch into dev 2026-07-29 23:56:24 +00:00
Owner

Summary

Cash-in ("Buy Bitcoin") did not work on the batm3: the bill validator refused notes, and once it did read one the note hung in escrow, was never credited, and was not returned. Diagnosed live on the machine — it was two stacked bugs, both fixed here and validated end-to-end on real hardware.

1. Wrong validator device path (fix(config), eafdce3)

The batm3 preset pointed the EBDS validator at /dev/ttyACM0 (assuming a CDC-ACM BNR Advance). The actual MEI acceptor enumerates as a USB-serial device and is exposed via the stable udev symlink /dev/ttyMEI (batm3.nix, serial A9YW78OC). Because /dev/ttyACM0 never existed, hal-service silently skipped the validator and logged [HAL] No validator — cash-in disabled, so inserted bills were ignored.

2. EBDS stack/return never latched (fix(hal), 4d0e42f)

In EBDS the stack/return decision is carried as bits in the omnibus poll command, but the driver sent stack()/reject() as a one-shot frame while a free-running 100 ms poller kept sending plain polls. The lone stack frame raced/collided and got dropped, so the device held the note in escrow indefinitely; disable() didn't release it either (inactivity timeout stranded the bill).

  • Latch the escrow decision (pendingAction) into the poll command byte and re-assert it every poll until the device leaves escrow (cleared in _process on !escrowed). A dropped frame is simply retried next poll.
  • Return an escrowed note on disableValidator() (cancel / inactivity timeout / leaving the insert screen).

3. Robustness: stable dispenser path (fix(config), 2c71d9d)

Dispenser used the raw /dev/ttyUSB0 (enumeration-order dependent). Switched to the stable /dev/ttyF56 symlink so both peripherals bind by identity and survive re-enumeration — including on an internal-SATA flash (the udev rules live in the shared batm3.nix, so both the USB and SATA images get them).

Also

  • Stringify the [ATM] Sending event / [ATM] State logs — they printed [object Object], which blinded the cash-in trace.

Validation (on the batm3)

escrowed → billsRead → BILL_PENDING
escrowed → stacking            (escrow → stacking in 80 ms)
stacking → billsValid → BILL_INSERTED (credited on screen)
→ standby (re-armed)

Return path also confirmed: cancel/timeout → Returning escrowed bill on disable → escrowed → returning → returned, bill physically ejected.

Verified: @bitSpire/hal builds, machine app vue-tsc typechecks clean. No unit tests exist for this serial driver; behaviour confirmed on hardware.

Follow-ups (not in this PR)

  • Boot-time escrow: a note already in escrow at HAL init is reported before the renderer subscribes, so it isn't auto-handled until the next enable/disable cycle.
  • The [EBDS] … autonomously returning — watch for torn bills heuristic false-positives when we command a return on disable.

🤖 Generated with Claude Code

## Summary Cash-in ("Buy Bitcoin") did not work on the batm3: the bill validator refused notes, and once it did read one the note hung in escrow, was never credited, and was not returned. Diagnosed live on the machine — it was **two stacked bugs**, both fixed here and validated end-to-end on real hardware. ### 1. Wrong validator device path (`fix(config)`, `eafdce3`) The batm3 preset pointed the EBDS validator at `/dev/ttyACM0` (assuming a CDC-ACM BNR Advance). The actual MEI acceptor enumerates as a USB-serial device and is exposed via the stable udev symlink **`/dev/ttyMEI`** (`batm3.nix`, serial `A9YW78OC`). Because `/dev/ttyACM0` never existed, `hal-service` silently skipped the validator and logged `[HAL] No validator — cash-in disabled`, so inserted bills were ignored. ### 2. EBDS stack/return never latched (`fix(hal)`, `4d0e42f`) In EBDS the stack/return decision is carried as bits in the **omnibus poll** command, but the driver sent `stack()`/`reject()` as a **one-shot** frame while a free-running 100 ms poller kept sending plain polls. The lone stack frame raced/collided and got dropped, so the device held the note in escrow indefinitely; `disable()` didn't release it either (inactivity timeout stranded the bill). - Latch the escrow decision (`pendingAction`) into the poll command byte and re-assert it every poll until the device leaves escrow (cleared in `_process` on `!escrowed`). A dropped frame is simply retried next poll. - Return an escrowed note on `disableValidator()` (cancel / inactivity timeout / leaving the insert screen). ### 3. Robustness: stable dispenser path (`fix(config)`, `2c71d9d`) Dispenser used the raw `/dev/ttyUSB0` (enumeration-order dependent). Switched to the stable `/dev/ttyF56` symlink so both peripherals bind by identity and survive re-enumeration — including on an internal-SATA flash (the udev rules live in the shared `batm3.nix`, so both the USB and SATA images get them). ### Also - Stringify the `[ATM] Sending event` / `[ATM] State` logs — they printed `[object Object]`, which blinded the cash-in trace. ## Validation (on the batm3) ``` escrowed → billsRead → BILL_PENDING escrowed → stacking (escrow → stacking in 80 ms) stacking → billsValid → BILL_INSERTED (credited on screen) → standby (re-armed) ``` Return path also confirmed: cancel/timeout → `Returning escrowed bill on disable` → `escrowed → returning → returned`, bill physically ejected. Verified: `@bitSpire/hal` builds, machine app `vue-tsc` typechecks clean. No unit tests exist for this serial driver; behaviour confirmed on hardware. ## Follow-ups (not in this PR) - Boot-time escrow: a note already in escrow at HAL init is reported before the renderer subscribes, so it isn't auto-handled until the next enable/disable cycle. - The `[EBDS] … autonomously returning — watch for torn bills` heuristic false-positives when we command a return on disable. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cash-in stalled on the batm3: a note reached escrow and was read, but the
acceptor never stacked or returned it, and the customer was never
credited. Root cause: EBDS carries the stack/return decision as bits in
the omnibus *poll* command, but the driver sent stack()/reject() as a
single one-shot frame while a free-running 100ms poller kept sending
plain polls. The lone stack frame races/collides with the poller (or its
ack desyncs), gets dropped, and the device holds the note in escrow
indefinitely.

- ebds-rs232: latch the escrow decision (`pendingAction`) into the poll
  command byte and re-assert it on every poll until the device leaves
  escrow (cleared in _process when `!escrowed`). A dropped frame is now
  simply retried on the next poll.
- hal-service: return an escrowed note on disableValidator() — disable
  alone does not release it on EBDS, so an inactivity timeout / cancel
  previously stranded the bill in the transport (observed on the batm3).
- atm store: stringify the `[ATM] Sending event` / `[ATM] State` logs —
  they were printing `[object Object]`, which blinded the cash-in trace.

Verified: hal builds, machine app typechecks. Hardware behaviour to be
confirmed on the batm3 (no unit tests exist for this serial driver).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The batm3 preset defaulted the EBDS validator to /dev/ttyACM0, assuming a
CDC-ACM BNR Advance. The actual MEI acceptor enumerates as a USB-serial
device (ttyUSB*), exposed via the stable udev symlink /dev/ttyMEI
(batm3.nix). Because /dev/ttyACM0 never existed, hal-service skipped the
validator entirely and logged "[HAL] No validator — cash-in disabled", so
Buy Bitcoin silently ignored inserted bills.

Verified on hardware: with the correct device the validator starts, and
(with the EBDS latch fix) a bill escrows → stacks → credits. Removes the
need for the VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyMEI per-box override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The batm3 dispenser used the raw /dev/ttyUSB0, which is
enumeration-order dependent — a re-plug or reboot could reassign
ttyUSB0 to a different adapter. Switch to the stable udev symlink
/dev/ttyF56 (batm3.nix, serial DDDLb103Y23), matching the validator's
/dev/ttyMEI, so both peripherals bind by identity and survive
re-enumeration (incl. on an internal-SATA flash). Per-box override:
VITE_LAMASSU_DISPENSER_DEVICE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
padreug deleted branch fix/ebds-escrow-stack-latch 2026-07-29 23:56:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!79
No description provided.