feat(machine): add HAL IPC bridge for real hardware in Electron
HAL hardware drivers (serialport) run in the main process since they need Node.js. The renderer communicates via IPC for all hardware ops. - hal-service.ts: bridge between HAL drivers and Electron IPC - main.ts: HAL IPC handlers (init, dispense, validator stack/reject) - preload.ts: expose HAL API to renderer via contextBridge - atm.ts: IPC-based production init with validator event wiring - hal.ts: add 'hold' mode for escrow (async stack/reject decision) - electron.d.ts: HAL type declarations for window.electronAPI Bills go to escrow first; the renderer checks balance before accepting. Falls back to Lightning-only mock mode if HAL init fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb73e13e48
commit
2605c44ef8
7 changed files with 515 additions and 24 deletions
|
|
@ -20,6 +20,7 @@ import {
|
|||
emptyCashbox,
|
||||
getCashbox,
|
||||
} from './state-store.js'
|
||||
import { initializeHal, type HalInstance } from './hal-service.js'
|
||||
|
||||
// ESM equivalent of __dirname
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
|
|
@ -128,6 +129,98 @@ ipcMain.handle('state:get-cashbox', () => getCashbox())
|
|||
ipcMain.handle('state:record-transaction', (_event, tx) => recordTransaction(tx))
|
||||
ipcMain.handle('state:empty-cashbox', () => emptyCashbox())
|
||||
|
||||
// =============================================================================
|
||||
// HAL Hardware IPC Handlers
|
||||
// HAL runs in the main process (Node.js) because it needs serialport.
|
||||
// The renderer communicates via IPC for all hardware operations.
|
||||
// =============================================================================
|
||||
|
||||
let halInstance: HalInstance | null = null
|
||||
let pendingBillDenomination: number | null = null
|
||||
|
||||
ipcMain.handle('hal:init', async (_event, config) => {
|
||||
try {
|
||||
halInstance = await initializeHal(config)
|
||||
|
||||
// Wire validator events → forward to renderer via IPC
|
||||
// Bills go to escrow first ('hold' mode); the renderer decides to
|
||||
// stack or reject by calling hal:stack-bill or hal:reject-bill.
|
||||
halInstance.connectValidator({
|
||||
shouldAcceptBill: (_denomination: number) => {
|
||||
// Hold in escrow — renderer decides asynchronously
|
||||
return 'hold' as const
|
||||
},
|
||||
onBillRead: (denomination: number) => {
|
||||
// Bill is in escrow, notify renderer to decide
|
||||
pendingBillDenomination = denomination
|
||||
mainWindow?.webContents.send('hal:bill-read', denomination)
|
||||
},
|
||||
onBillInserted: (denomination: number) => {
|
||||
mainWindow?.webContents.send('hal:bill-inserted', denomination)
|
||||
},
|
||||
onBillRejected: (reason: string) => {
|
||||
mainWindow?.webContents.send('hal:bill-rejected', reason)
|
||||
},
|
||||
onError: (error: string) => {
|
||||
mainWindow?.webContents.send('hal:error', error)
|
||||
},
|
||||
})
|
||||
|
||||
console.log('[Electron] HAL initialized via IPC')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
console.error('[Electron] HAL init failed:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'hal:dispense',
|
||||
async (_event, amounts: { denomination: number; count: number }[]) => {
|
||||
if (!halInstance) throw new Error('HAL not initialized')
|
||||
await halInstance.dispenseCash(amounts)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('hal:wait-for-bills-removed', async () => {
|
||||
// This is handled inside dispenseCash already
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:enable-validator', () => {
|
||||
if (halInstance) halInstance.enableValidator()
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:disable-validator', () => {
|
||||
if (halInstance) halInstance.disableValidator()
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:stack-bill', () => {
|
||||
if (halInstance) {
|
||||
const denomination = pendingBillDenomination
|
||||
pendingBillDenomination = null
|
||||
halInstance.stackBill()
|
||||
// Notify renderer that the bill was accepted
|
||||
if (denomination !== null) {
|
||||
mainWindow?.webContents.send('hal:bill-inserted', denomination)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:reject-bill', () => {
|
||||
if (halInstance) {
|
||||
pendingBillDenomination = null
|
||||
halInstance.rejectBill()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:cleanup', async () => {
|
||||
if (halInstance) {
|
||||
await halInstance.cleanup()
|
||||
halInstance = null
|
||||
}
|
||||
})
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(() => {
|
||||
initDatabase()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue