fix(hal): EBDS escrow stack/return latch + return-on-disable

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>
This commit is contained in:
Patrick Mulligan 2026-07-30 00:40:06 +02:00
commit 4d0e42f289
3 changed files with 46 additions and 9 deletions

View file

@ -458,7 +458,7 @@ export const useAtmStore = defineStore('atm', () => {
actor.value.subscribe((newSnapshot: SnapshotFrom<ATMMachine>) => {
const prevSnapshot = snapshot.value
snapshot.value = newSnapshot
console.log('[ATM] State:', newSnapshot.value)
console.log('[ATM] State:', JSON.stringify(newSnapshot.value))
// Detect transition into a complete state
const state = newSnapshot.value
@ -1374,7 +1374,7 @@ export const useAtmStore = defineStore('atm', () => {
console.error('[ATM] Cannot send event: machine not initialized')
return
}
console.log('[ATM] Sending event:', event)
console.log('[ATM] Sending event:', event.type, JSON.stringify(event))
actor.value.send(event)
}