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') })