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