From c8f76af113bcb08cfaff404573404f95389ef400 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Wed, 25 Mar 2026 16:20:51 -0400 Subject: [PATCH] fix(hal): make validator optional for dispenser-only operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/machine/electron/hal-service.ts | 72 ++++++++++++++++++---------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/apps/machine/electron/hal-service.ts b/apps/machine/electron/hal-service.ts index 6d5ed79..d4c6ccb 100644 --- a/apps/machine/electron/hal-service.ts +++ b/apps/machine/electron/hal-service.ts @@ -68,11 +68,6 @@ export async function initializeHal(config: HalConfig): Promise { 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 { await dispenser.init(dispenserInitData) console.log('[HAL] Dispenser initialized') - // Start validator - await new Promise((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((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 = {} @@ -104,6 +120,10 @@ export async function initializeHal(config: HalConfig): Promise { 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 { }, 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 => { console.log('[HAL] Dispensing:', amounts) @@ -229,13 +249,17 @@ export async function initializeHal(config: HalConfig): Promise { cleanup: async () => { return new Promise((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() - }) + } }) }, }