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:
parent
f70105e8ff
commit
4d0e42f289
3 changed files with 46 additions and 9 deletions
|
|
@ -313,6 +313,13 @@ export class EbdsRs232 extends EventEmitter {
|
|||
private serial: SerialPort | null = null
|
||||
private ack: number = 0x0
|
||||
private enabledDenominations: number = 0x00
|
||||
// Latched escrow decision. In EBDS the stack/return choice is NOT a one-shot
|
||||
// message — it's carried as bits in the omnibus poll command, and the device
|
||||
// holds the escrowed note until a poll asserts stack or return. We keep the
|
||||
// action set and re-assert it on every poll until the device leaves escrow
|
||||
// (cleared in _process), so a single dropped/collided frame no longer strands
|
||||
// the note in escrow forever.
|
||||
private pendingAction: 'none' | 'stack' | 'return' = 'none'
|
||||
private lastStatusFlags: string | null = null
|
||||
private firmwareLogged: boolean = false
|
||||
|
||||
|
|
@ -405,23 +412,38 @@ export class EbdsRs232 extends EventEmitter {
|
|||
|
||||
// -- Commands (Appendix D, Controller Message) ---------------------------
|
||||
|
||||
/** Send an Omnibus poll command with current denomination mask */
|
||||
/**
|
||||
* Command byte 1 for the omnibus poll, encoding any latched escrow action.
|
||||
* `stack` (0x3f) and `return` (0x5f) differ from the plain poll (0x1b) only
|
||||
* in the stack/return bits; while an action is latched every poll re-asserts
|
||||
* it until the device acts.
|
||||
*/
|
||||
private commandByte(): number {
|
||||
if (this.pendingAction === 'stack') return 0x3f
|
||||
if (this.pendingAction === 'return') return 0x5f
|
||||
return 0x1b
|
||||
}
|
||||
|
||||
/** Send an Omnibus poll command with the current mask + latched action */
|
||||
poll(): void {
|
||||
this._dispatch([this.enabledDenominations, 0x1b, 0x10])
|
||||
this._dispatch([this.enabledDenominations, this.commandByte(), 0x10])
|
||||
}
|
||||
|
||||
/** Stack the bill currently in escrow */
|
||||
/** Latch "stack the escrowed note"; re-asserted each poll until it takes. */
|
||||
stack(): void {
|
||||
this._dispatch([this.enabledDenominations, 0x3f, 0x10])
|
||||
this.pendingAction = 'stack'
|
||||
this.poll()
|
||||
}
|
||||
|
||||
/** Reject/return the bill currently in escrow */
|
||||
/** Latch "return the escrowed note"; re-asserted each poll until it takes. */
|
||||
reject(): void {
|
||||
this._dispatch([this.enabledDenominations, 0x5f, 0x10])
|
||||
this.pendingAction = 'return'
|
||||
this.poll()
|
||||
}
|
||||
|
||||
/** Send initial setup command (disable all, reset state) */
|
||||
reset(): void {
|
||||
this.pendingAction = 'none'
|
||||
this._dispatch([0x00, 0x1b, 0x10])
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +492,13 @@ export class EbdsRs232 extends EventEmitter {
|
|||
validatePacket(packet)
|
||||
const result = interpret(packet)
|
||||
if (result) {
|
||||
if (result.destructedData) this._logStatusOnChange(result.destructedData)
|
||||
if (result.destructedData) {
|
||||
// Clear a latched stack/return once the device has left escrow — it
|
||||
// is now stacking/returning/idle, so we must stop asserting the
|
||||
// action or it would leak onto the next note.
|
||||
if (!result.destructedData[0].escrowed) this.pendingAction = 'none'
|
||||
this._logStatusOnChange(result.destructedData)
|
||||
}
|
||||
this.emit('message', result)
|
||||
}
|
||||
} catch (ex) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue