fix(hal): make validator optional for dispenser-only operation

The HAL init hung indefinitely when the validator device didn't exist
or failed to respond — blocking the entire init including the dispenser
and Lightning connection.

Now the validator is optional:
- Checks device exists (fs.existsSync) before attempting to open
- 15s timeout on validator.run() to prevent hanging
- On failure, logs warning and proceeds with dispenser-only mode
- All validator methods guarded with null checks

This enables the BATM3 to run cash-out only when the MEI validator
is not connected (e.g., during initial setup or testing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-25 16:20:51 -04:00
commit c8f76af113

View file

@ -68,11 +68,6 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
const { validator: valConfig, dispenser: dispConfig } = config
// Create hardware instances
const validator: BillValidator = hal.createValidator(valConfig.type, {
rs232: { device: valConfig.device, fiatCode: valConfig.fiatCode },
fiatCode: valConfig.fiatCode,
})
const dispenser: BillDispenser = hal.createDispenser(dispConfig.type, {
device: dispConfig.device,
})
@ -85,14 +80,35 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
await dispenser.init(dispenserInitData)
console.log('[HAL] Dispenser initialized')
// Start validator
await new Promise<void>((resolve, reject) => {
validator.run((err?: Error) => {
if (err) return reject(err)
resolve()
})
})
console.log('[HAL] Validator started')
// Start validator (optional — proceed without if device is missing or fails)
let validator: BillValidator | null = null
try {
const fs = await import('node:fs')
const device = Array.isArray(valConfig.device) ? valConfig.device[0] : valConfig.device
if (device && fs.existsSync(device)) {
validator = hal.createValidator(valConfig.type, {
rs232: { device: valConfig.device, fiatCode: valConfig.fiatCode },
fiatCode: valConfig.fiatCode,
})
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Validator init timed out'))
}, 15000)
validator!.run((err?: Error) => {
clearTimeout(timeout)
if (err) return reject(err)
resolve()
})
})
console.log('[HAL] Validator started')
} else {
console.log('[HAL] Validator device not found, running dispenser-only')
}
} catch (err) {
console.warn('[HAL] Validator failed to start, running dispenser-only:', err)
validator = null
}
// Track inventory
const inventory: Record<number, number> = {}
@ -104,6 +120,10 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
return {
connectValidator: (callbacks: ValidatorCallbacks) => {
if (!validator) {
console.log('[HAL] No validator — cash-in disabled')
return
}
validator.on('billsRead', (data: { denomination: number | null; code: number }) => {
if (data.denomination !== null) {
const decision = callbacks.shouldAcceptBill(data.denomination)
@ -142,17 +162,17 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
},
enableValidator: () => {
validator.enable()
validator.lightOn()
validator?.enable()
validator?.lightOn()
},
disableValidator: () => {
validator.disable()
validator.lightOff()
validator?.disable()
validator?.lightOff()
},
stackBill: () => validator.stack(),
rejectBill: () => validator.reject(),
stackBill: () => validator?.stack(),
rejectBill: () => validator?.reject(),
dispenseCash: async (amounts): Promise<DispenseResult> => {
console.log('[HAL] Dispensing:', amounts)
@ -229,13 +249,17 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
cleanup: async () => {
return new Promise<void>((resolve) => {
validator.disable()
validator.lightOff()
validator?.disable()
validator?.lightOff()
dispenser.close()
validator.close((err?: Error) => {
if (err) console.error('[HAL] Validator close error:', err)
if (validator) {
validator.close((err?: Error) => {
if (err) console.error('[HAL] Validator close error:', err)
resolve()
})
} else {
resolve()
})
}
})
},
}