feat(hal): add EBDS bill validator driver for BATM3 support

Port MEI CashFlow SC / BNR Advance EBDS protocol from lamassu-machine
to TypeScript HAL. Adds 'batm3' machine model preset (EBDS validator +
F56 dispenser). Fixes hardcoded 'id003' validator type in device config
overrides so model presets correctly propagate their validator type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-20 18:06:54 -04:00
commit 71eff33f1e
11 changed files with 814 additions and 13 deletions

View file

@ -15,7 +15,7 @@ export interface CassetteConfig {
export interface HalConfig {
validator: {
type: 'id003'
type: 'id003' | 'ebds'
device: string | string[]
fiatCode: string
}

View file

@ -390,6 +390,10 @@ app.whenReady().then(() => {
{ denomination: 100, count: 500 },
],
gaia: [{ denomination: 20, count: 50 }],
batm3: [
{ denomination: 20, count: 500 },
{ denomination: 50, count: 500 },
],
}
seedCassettes = presets[model] || []
}

View file

@ -15,7 +15,7 @@ import type { HalConfig, CassetteConfig } from '@/services/hal'
/**
* Supported machine models
*/
export type MachineModel = 'sintra' | 'tejo' | 'douro' | 'gaia' | 'custom'
export type MachineModel = 'sintra' | 'tejo' | 'douro' | 'gaia' | 'batm3' | 'custom'
/**
* Full device configuration
@ -28,7 +28,7 @@ export interface DeviceConfig {
/** Bill validator configuration */
validator: {
/** Validator protocol type */
type: 'id003'
type: 'id003' | 'ebds'
/** Serial device path(s) */
device: string | string[]
}
@ -130,6 +130,27 @@ export const MACHINE_PRESETS: Record<MachineModel, Omit<DeviceConfig, 'fiatCode'
},
},
/**
* General Bytes BATM3 (XL variant)
* - Validator: MEI BNR Advance or CashFlow SC (EBDS protocol, USB CDC ACM)
* - Dispenser: Fujitsu F56 (RS232)
*/
batm3: {
model: 'batm3',
validator: {
type: 'ebds',
device: '/dev/ttyACM0',
},
dispenser: {
type: 'f56',
device: '/dev/ttyUSB0',
cassettes: [
{ denomination: 20, count: 500 },
{ denomination: 50, count: 500 },
],
},
},
/**
* Custom configuration - all values must be provided via env/runtime
*/
@ -200,10 +221,10 @@ export function loadDeviceConfigFromEnv(): DeviceConfig {
const overrides: Partial<DeviceConfig> = {}
// Validator device override
// Validator device override (preserve validator type from preset)
const validatorDevice = import.meta.env.VITE_LAMASSU_VALIDATOR_DEVICE
if (validatorDevice) {
overrides.validator = { type: 'id003', device: validatorDevice }
overrides.validator = { type: MACHINE_PRESETS[model].validator.type, device: validatorDevice }
}
// Dispenser device override

View file

@ -23,7 +23,7 @@ export interface CassetteConfig {
export interface HalConfig {
validator: {
type: 'id003'
type: 'id003' | 'ebds'
device: string | string[]
fiatCode: string
}

View file

@ -767,13 +767,18 @@ export const useAtmStore = defineStore('atm', () => {
fiatCode.value = runtimeFiatCode
// Build device config from runtime values
const { getDeviceConfig, toHalConfig } = await import('@/config')
const { getDeviceConfig, toHalConfig, MACHINE_PRESETS } = await import('@/config')
const preset = MACHINE_PRESETS[model] ?? MACHINE_PRESETS.sintra
const overrides: any = {}
if (runtimeConfig.validatorDevice) {
overrides.validator = { type: 'id003', device: runtimeConfig.validatorDevice }
overrides.validator = { type: preset.validator.type, device: runtimeConfig.validatorDevice }
}
if (runtimeConfig.dispenserDevice) {
overrides.dispenser = { type: 'f56', device: runtimeConfig.dispenserDevice, cassettes: [] }
overrides.dispenser = {
type: preset.dispenser.type,
device: runtimeConfig.dispenserDevice,
cassettes: [],
}
}
if (runtimeConfig.cassettes) {
try {
@ -781,7 +786,7 @@ export const useAtmStore = defineStore('atm', () => {
if (overrides.dispenser) {
overrides.dispenser.cassettes = cassettes
} else {
overrides.dispenser = { type: 'f56', device: '', cassettes }
overrides.dispenser = { type: preset.dispenser.type, device: '', cassettes }
}
} catch (e) {
console.error('[ATM] Failed to parse cassettes:', e)