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
|
// Blank stacks happen on power up or cassette re-insertion — ignore
|
||||||
if (status === 'blankStack') return
|
if (status === 'blankStack') return
|
||||||
|
|
||||||
|
const prev = this.currentStatus
|
||||||
this.currentStatus = status
|
this.currentStatus = status
|
||||||
|
console.log('[EBDS] %d status: %s → %s', Date.now(), prev ?? '(init)', status)
|
||||||
|
|
||||||
switch (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':
|
case 'billsRead':
|
||||||
// Bill is in escrow — verify currency and emit
|
// Bill is in escrow — verify currency and emit
|
||||||
if (bill) {
|
if (bill) {
|
||||||
|
|
@ -51,6 +59,26 @@ export class EbdsFsm extends EventEmitter {
|
||||||
}
|
}
|
||||||
break
|
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':
|
case 'billsValid':
|
||||||
// Bill stacked successfully — but ignore if no denomination
|
// Bill stacked successfully — but ignore if no denomination
|
||||||
// (can happen when cashbox is re-inserted)
|
// (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 {
|
function parseStatus(data: DestructedData): string | null {
|
||||||
return data[0].stacked
|
// Terminal — bill has reached a resting position
|
||||||
? 'billsValid'
|
if (data[0].stacked) return 'billsValid'
|
||||||
: data[0].escrowed
|
if (data[0].returned || data[1].cheated || data[1].rejected) return 'billsRejected'
|
||||||
? 'billsRead'
|
if (data[1].jammed) return 'jam'
|
||||||
: data[0].returned || data[1].cheated || data[1].rejected
|
if (!data[1].cassetteAttached) return 'stackerOpen'
|
||||||
? 'billsRejected'
|
// Transient — bill is in motion. Surface before `escrowed` so we observe
|
||||||
: data[1].jammed
|
// the MEI moving the bill out of escrow autonomously.
|
||||||
? 'jam'
|
if (data[0].stacking) return 'stacking'
|
||||||
: !data[1].cassetteAttached
|
if (data[0].returning) return 'returning'
|
||||||
? 'stackerOpen'
|
if (data[0].escrowed) return 'billsRead'
|
||||||
: data[0].idling
|
if (data[0].accepting) return 'accepting'
|
||||||
? 'standby'
|
if (data[0].idling) return 'standby'
|
||||||
: null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Destruct the control byte (§6.4) */
|
/** Destruct the control byte (§6.4) */
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,13 @@ export class EbdsValidator extends EventEmitter implements BillValidator {
|
||||||
this.emit('billsRejected')
|
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.fsm.on('stackerOpen', () => {
|
||||||
this.emit('stackerOpen')
|
this.emit('stackerOpen')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue