From fe2bc5d314a5f8cd618f4acccbd8d8dfc8572488 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 19 May 2026 22:35:30 +0200 Subject: [PATCH] feat(hal/ebds): surface accepting/stacking/returning transient states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseStatus previously collapsed all in-motion bits into a single derived status, hiding when the MEI was moving a bill between escrow and the stacker/mouth. Field journals showed nothing in the window where the BATM3 tore bills. Surface the three transient bits between the terminal states and escrow/standby, route them through EbdsFsm with a Date.now() timestamp, and emit them as diagnostic events on the validator. No state machine consumes them; they're for journalctl. Also: warn when `returning` is observed straight out of escrow with no host reject() — that signature is the autonomous-return failure mode we just polled around. Surfacing it gives us field confirmation the 100ms fix is sufficient (or evidence it isn't). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/hal/src/validators/ebds/ebds-fsm.ts | 28 +++++++++++++++ .../hal/src/validators/ebds/ebds-rs232.ts | 36 +++++++++++-------- packages/hal/src/validators/ebds/index.ts | 7 ++++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/packages/hal/src/validators/ebds/ebds-fsm.ts b/packages/hal/src/validators/ebds/ebds-fsm.ts index 2813329..1373427 100644 --- a/packages/hal/src/validators/ebds/ebds-fsm.ts +++ b/packages/hal/src/validators/ebds/ebds-fsm.ts @@ -34,9 +34,17 @@ export class EbdsFsm extends EventEmitter { // Blank stacks happen on power up or cassette re-insertion — ignore if (status === 'blankStack') return + const prev = this.currentStatus this.currentStatus = status + console.log('[EBDS] %d status: %s → %s', Date.now(), prev ?? '(init)', status) switch (status) { + case 'accepting': + // Transient: bill is being read by the validator head. Diagnostic + // only — useful for measuring time-to-escrow under field load. + this.emit('accepting') + break + case 'billsRead': // Bill is in escrow — verify currency and emit if (bill) { @@ -51,6 +59,26 @@ export class EbdsFsm extends EventEmitter { } break + case 'stacking': + // Transient: bill is moving from escrow to stacker. Should follow + // a host stack() command — if it precedes one, MEI is moving the + // bill on its own and we should investigate. + this.emit('stacking') + break + + case 'returning': + // Transient: bill is moving from escrow back to mouth. Should + // follow a host reject() command — if it precedes one, MEI is + // autonomously returning, which is the BATM3 tearing failure mode. + if (prev === 'billsRead') { + console.warn( + '[EBDS] returning observed straight from escrow with no host reject — ' + + 'MEI may be autonomously returning. Watch for torn bills.' + ) + } + this.emit('returning') + break + case 'billsValid': // Bill stacked successfully — but ignore if no denomination // (can happen when cashbox is re-inserted) diff --git a/packages/hal/src/validators/ebds/ebds-rs232.ts b/packages/hal/src/validators/ebds/ebds-rs232.ts index d94750f..f626c30 100644 --- a/packages/hal/src/validators/ebds/ebds-rs232.ts +++ b/packages/hal/src/validators/ebds/ebds-rs232.ts @@ -186,21 +186,29 @@ function destructData(data: Buffer): DestructedData | null { ] } -/** Derive high-level status string from destructed status bytes */ +/** + * Derive high-level status string from destructed status bytes. + * + * Terminal states (stacked / returned / cheated / rejected / jammed / + * stackerOpen) take priority over transient states. Transient states + * (`stacking`, `returning`, `accepting`) are surfaced so the FSM can + * time the escrow grace window and so field logs reveal when MEI moves + * a bill on its own (the BATM3 tearing failure mode). + */ function parseStatus(data: DestructedData): string | null { - return data[0].stacked - ? 'billsValid' - : data[0].escrowed - ? 'billsRead' - : data[0].returned || data[1].cheated || data[1].rejected - ? 'billsRejected' - : data[1].jammed - ? 'jam' - : !data[1].cassetteAttached - ? 'stackerOpen' - : data[0].idling - ? 'standby' - : null + // Terminal — bill has reached a resting position + if (data[0].stacked) return 'billsValid' + if (data[0].returned || data[1].cheated || data[1].rejected) return 'billsRejected' + if (data[1].jammed) return 'jam' + if (!data[1].cassetteAttached) return 'stackerOpen' + // Transient — bill is in motion. Surface before `escrowed` so we observe + // the MEI moving the bill out of escrow autonomously. + if (data[0].stacking) return 'stacking' + if (data[0].returning) return 'returning' + if (data[0].escrowed) return 'billsRead' + if (data[0].accepting) return 'accepting' + if (data[0].idling) return 'standby' + return null } /** Destruct the control byte (§6.4) */ diff --git a/packages/hal/src/validators/ebds/index.ts b/packages/hal/src/validators/ebds/index.ts index cedbedf..44dd540 100644 --- a/packages/hal/src/validators/ebds/index.ts +++ b/packages/hal/src/validators/ebds/index.ts @@ -118,6 +118,13 @@ export class EbdsValidator extends EventEmitter implements BillValidator { this.emit('billsRejected') }) + // Diagnostic events — surface MEI transport motion so field logs can + // distinguish host-driven movement from autonomous (timeout-induced) + // movement. No state machine consumes these; they're for journals. + this.fsm.on('accepting', () => this.emit('accepting')) + this.fsm.on('stacking', () => this.emit('stacking')) + this.fsm.on('returning', () => this.emit('returning')) + this.fsm.on('stackerOpen', () => { this.emit('stackerOpen') })