feat(hal/ebds): surface accepting/stacking/returning transient states
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) <noreply@anthropic.com>
This commit is contained in:
parent
d4115a31e8
commit
fe2bc5d314
3 changed files with 57 additions and 14 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue