From a8cfaaf66ff1553e667c425c83dfe0b7182c24a0 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Thu, 29 Jan 2026 21:25:27 -0500 Subject: [PATCH] feat: add device configuration for HAL drivers (#20) - Create src/config/device.ts with machine presets (Sintra, Gaia) - Support environment variable overrides (VITE_LAMASSU_*) - Add initializeForProduction() convenience method to ATM store - Document configuration in docs/device-configuration.md Default Sintra configuration: - Validator: /dev/ttyJ5 (ID003 protocol) - Dispenser: /dev/ttyJ7 (F56) - Cassettes: [{ denomination: 20, count: 50 }] Co-Authored-By: Claude Opus 4.5 --- .../apps/machine/src/config/device.ts | 217 ++++++++++++++++++ lamassu-next/apps/machine/src/config/index.ts | 15 ++ lamassu-next/apps/machine/src/stores/atm.ts | 27 +++ lamassu-next/docs/device-configuration.md | 197 ++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 lamassu-next/apps/machine/src/config/device.ts create mode 100644 lamassu-next/apps/machine/src/config/index.ts create mode 100644 lamassu-next/docs/device-configuration.md diff --git a/lamassu-next/apps/machine/src/config/device.ts b/lamassu-next/apps/machine/src/config/device.ts new file mode 100644 index 0000000..6efcccf --- /dev/null +++ b/lamassu-next/apps/machine/src/config/device.ts @@ -0,0 +1,217 @@ +/** + * Device Configuration + * + * Defines hardware configuration for ATM devices (bill validator, dispenser). + * Supports multiple machine models with sensible defaults. + * + * Configuration priority (highest to lowest): + * 1. Runtime overrides (passed directly to functions) + * 2. Environment variables (LAMASSU_*) + * 3. Machine preset defaults + */ + +import type { HalConfig, CassetteConfig } from '@/services/hal' + +/** + * Supported machine models + */ +export type MachineModel = 'sintra' | 'gaia' | 'custom' + +/** + * Full device configuration + */ +export interface DeviceConfig { + /** Machine model name */ + model: MachineModel + /** Fiat currency code (ISO 4217) */ + fiatCode: string + /** Bill validator configuration */ + validator: { + /** Validator protocol type */ + type: 'id003' + /** Serial device path(s) */ + device: string | string[] + } + /** Bill dispenser configuration */ + dispenser: { + /** Dispenser model */ + type: 'f56' + /** Serial device path */ + device: string + /** Cassette configuration */ + cassettes: CassetteConfig[] + } +} + +/** + * Preset configurations for known machine models + */ +export const MACHINE_PRESETS: Record> = { + /** + * Lamassu Sintra (Gen 2) + * - Platform: Aaeon UP Board (Intel Atom x5-Z8350) + * - Validator: JCM iVIZION (ID003 protocol) + * - Dispenser: Fujitsu F56 + */ + sintra: { + model: 'sintra', + validator: { + type: 'id003', + device: '/dev/ttyJ5', + }, + dispenser: { + type: 'f56', + device: '/dev/ttyJ7', + cassettes: [{ denomination: 20, count: 50 }], + }, + }, + + /** + * Lamassu Gaia + * - Validator: JCM iVIZION (ID003 protocol) + * - Dispenser: Fujitsu F56 + * Note: Device paths may vary - verify on hardware + */ + gaia: { + model: 'gaia', + validator: { + type: 'id003', + device: '/dev/ttyUSB0', + }, + dispenser: { + type: 'f56', + device: '/dev/ttyUSB1', + cassettes: [{ denomination: 20, count: 50 }], + }, + }, + + /** + * Custom configuration - all values must be provided via env/runtime + */ + custom: { + model: 'custom', + validator: { + type: 'id003', + device: '/dev/ttyUSB0', + }, + dispenser: { + type: 'f56', + device: '/dev/ttyUSB1', + cassettes: [], + }, + }, +} + +/** + * Get device configuration for a machine model + * + * @param model - Machine model or 'custom' for manual config + * @param fiatCode - Fiat currency code (default: 'USD') + * @param overrides - Optional overrides for specific values + */ +export function getDeviceConfig( + model: MachineModel = 'sintra', + fiatCode: string = 'USD', + overrides?: Partial +): DeviceConfig { + const preset = MACHINE_PRESETS[model] + + const config: DeviceConfig = { + ...preset, + fiatCode, + validator: { ...preset.validator }, + dispenser: { ...preset.dispenser, cassettes: [...preset.dispenser.cassettes] }, + } + + // Apply overrides + if (overrides) { + if (overrides.fiatCode) config.fiatCode = overrides.fiatCode + if (overrides.validator?.device) config.validator.device = overrides.validator.device + if (overrides.dispenser?.device) config.dispenser.device = overrides.dispenser.device + if (overrides.dispenser?.cassettes) config.dispenser.cassettes = overrides.dispenser.cassettes + } + + return config +} + +/** + * Load device configuration from environment variables + * + * Supported variables: + * - LAMASSU_MACHINE_MODEL: Machine model preset (sintra, gaia, custom) + * - LAMASSU_FIAT_CODE: Fiat currency code (USD, EUR, etc.) + * - LAMASSU_VALIDATOR_DEVICE: Bill validator serial device path + * - LAMASSU_DISPENSER_DEVICE: Bill dispenser serial device path + * - LAMASSU_CASSETTES: JSON array of cassette configs + * + * @example + * LAMASSU_MACHINE_MODEL=sintra + * LAMASSU_FIAT_CODE=USD + * LAMASSU_CASSETTES='[{"denomination":20,"count":100},{"denomination":50,"count":50}]' + */ +export function loadDeviceConfigFromEnv(): DeviceConfig { + const model = (import.meta.env.VITE_LAMASSU_MACHINE_MODEL as MachineModel) || 'sintra' + const fiatCode = import.meta.env.VITE_LAMASSU_FIAT_CODE || 'USD' + + const overrides: Partial = {} + + // Validator device override + const validatorDevice = import.meta.env.VITE_LAMASSU_VALIDATOR_DEVICE + if (validatorDevice) { + overrides.validator = { type: 'id003', device: validatorDevice } + } + + // Dispenser device override + const dispenserDevice = import.meta.env.VITE_LAMASSU_DISPENSER_DEVICE + if (dispenserDevice) { + overrides.dispenser = { + type: 'f56', + device: dispenserDevice, + cassettes: [], + } + } + + // Cassettes override (JSON) + const cassettesJson = import.meta.env.VITE_LAMASSU_CASSETTES + if (cassettesJson) { + try { + const cassettes = JSON.parse(cassettesJson) as CassetteConfig[] + if (overrides.dispenser) { + overrides.dispenser.cassettes = cassettes + } else { + overrides.dispenser = { + type: 'f56', + device: MACHINE_PRESETS[model].dispenser.device, + cassettes, + } + } + } catch (e) { + console.error('[Config] Failed to parse LAMASSU_CASSETTES:', e) + } + } + + return getDeviceConfig(model, fiatCode, overrides) +} + +/** + * Convert DeviceConfig to HalConfig format + */ +export function toHalConfig(config: DeviceConfig): HalConfig { + return { + validator: { + type: config.validator.type, + device: config.validator.device, + fiatCode: config.fiatCode, + }, + dispenser: { + type: config.dispenser.type, + device: config.dispenser.device, + cassettes: config.dispenser.cassettes, + }, + } +} + +/** + * Default export: Load config from environment with Sintra defaults + */ +export const deviceConfig = loadDeviceConfigFromEnv() diff --git a/lamassu-next/apps/machine/src/config/index.ts b/lamassu-next/apps/machine/src/config/index.ts new file mode 100644 index 0000000..126eae1 --- /dev/null +++ b/lamassu-next/apps/machine/src/config/index.ts @@ -0,0 +1,15 @@ +/** + * Configuration module + * + * Exports device configuration and utilities. + */ + +export { + deviceConfig, + getDeviceConfig, + loadDeviceConfigFromEnv, + toHalConfig, + MACHINE_PRESETS, + type DeviceConfig, + type MachineModel, +} from './device' diff --git a/lamassu-next/apps/machine/src/stores/atm.ts b/lamassu-next/apps/machine/src/stores/atm.ts index c87587a..c52aadf 100644 --- a/lamassu-next/apps/machine/src/stores/atm.ts +++ b/lamassu-next/apps/machine/src/stores/atm.ts @@ -11,6 +11,7 @@ import { } from '@lamassu/state-machine' import { initializeLightningServices } from '@/services/lightning' import type { HalConfig, HalServices } from '@/services/hal' +import { deviceConfig, toHalConfig } from '@/config' import type { LightningPubClient } from '@lamassu/lightning' import type { CLINKClient } from '@lamassu/clink' @@ -389,6 +390,31 @@ export const useAtmStore = defineStore('atm', () => { } } + /** + * Initialize for production using device configuration + * + * Loads device config from environment variables (with Sintra defaults) + * and initializes HAL hardware + Lightning services. + * + * Environment variables (optional, have sensible defaults): + * - VITE_LAMASSU_MACHINE_MODEL: sintra | gaia | custom + * - VITE_LAMASSU_FIAT_CODE: USD, EUR, etc. + * - VITE_LAMASSU_VALIDATOR_DEVICE: /dev/ttyJ5 + * - VITE_LAMASSU_DISPENSER_DEVICE: /dev/ttyJ7 + * - VITE_LAMASSU_CASSETTES: JSON array of cassette configs + */ + async function initializeForProduction() { + console.log('[ATM] Initializing for production...') + console.log('[ATM] Machine model:', deviceConfig.model) + console.log('[ATM] Fiat currency:', deviceConfig.fiatCode) + console.log('[ATM] Validator device:', deviceConfig.validator.device) + console.log('[ATM] Dispenser device:', deviceConfig.dispenser.device) + console.log('[ATM] Cassettes:', deviceConfig.dispenser.cassettes) + + const halConfig = toHalConfig(deviceConfig) + await initializeWithHal(halConfig) + } + function send(event: Parameters['send']>[0]) { if (!actor.value) { console.error('[ATM] Cannot send event: machine not initialized') @@ -480,6 +506,7 @@ export const useAtmStore = defineStore('atm', () => { initialize, initializeWithLightning, initializeWithHal, + initializeForProduction, send, selectCashIn, selectCashOut, diff --git a/lamassu-next/docs/device-configuration.md b/lamassu-next/docs/device-configuration.md new file mode 100644 index 0000000..4d8f982 --- /dev/null +++ b/lamassu-next/docs/device-configuration.md @@ -0,0 +1,197 @@ +# Device Configuration + +This document describes how to configure the ATM hardware for different machine models. + +## Overview + +The ATM application supports multiple Lamassu machine models out of the box. Configuration is handled through: + +1. **Machine presets** - Built-in defaults for known hardware (Sintra, Gaia) +2. **Environment variables** - Override any setting at runtime +3. **Runtime overrides** - Programmatic configuration + +## Supported Machine Models + +### Sintra (Default) + +The Lamassu Sintra (Gen 2) uses: + +| Device | Protocol | Path | +| -------------- | -------- | ------------ | +| Bill Validator | ID003 | `/dev/ttyJ5` | +| Bill Dispenser | F56 | `/dev/ttyJ7` | +| Printer | Nippon | `/dev/ttyJ4` | + +**Hardware:** + +- Platform: Aaeon UP Board (Intel Atom x5-Z8350) +- Validator: JCM iVIZION +- Dispenser: Fujitsu F53/F56 + +### Gaia + +The Lamassu Gaia uses similar hardware with different device paths. Verify paths on your specific unit. + +## Environment Variables + +All configuration can be overridden via environment variables. For Vite/Electron, prefix with `VITE_`: + +| Variable | Description | Default | +| ------------------------------- | ---------------------------- | ------------- | +| `VITE_LAMASSU_MACHINE_MODEL` | Machine preset | `sintra` | +| `VITE_LAMASSU_FIAT_CODE` | Fiat currency (ISO 4217) | `USD` | +| `VITE_LAMASSU_VALIDATOR_DEVICE` | Validator serial device path | (from preset) | +| `VITE_LAMASSU_DISPENSER_DEVICE` | Dispenser serial device path | (from preset) | +| `VITE_LAMASSU_CASSETTES` | JSON array of cassettes | (from preset) | + +### Example: Custom Cassette Configuration + +```bash +# Two cassettes: $20 bills (100 count) and $50 bills (50 count) +export VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100},{"denomination":50,"count":50}]' +``` + +### Example: Custom Device Paths + +```bash +export VITE_LAMASSU_VALIDATOR_DEVICE="/dev/ttyUSB0" +export VITE_LAMASSU_DISPENSER_DEVICE="/dev/ttyUSB1" +``` + +## Cassette Configuration + +Each cassette is defined with: + +```typescript +interface CassetteConfig { + denomination: number // Bill denomination (e.g., 20 for $20) + count?: number // Number of bills loaded (optional, for inventory tracking) +} +``` + +### Default Sintra Configuration + +```json +[{ "denomination": 20, "count": 50 }] +``` + +This configures a single cassette with $20 bills, 50 bill capacity. + +### Multi-Cassette Example + +```json +[ + { "denomination": 20, "count": 100 }, + { "denomination": 50, "count": 50 }, + { "denomination": 100, "count": 25 } +] +``` + +## Programmatic Configuration + +You can also configure devices programmatically: + +```typescript +import { getDeviceConfig, toHalConfig } from '@/config' + +// Use preset with custom fiat code +const config = getDeviceConfig('sintra', 'EUR') + +// Or with overrides +const config = getDeviceConfig('sintra', 'USD', { + dispenser: { + type: 'f56', + device: '/dev/ttyUSB1', + cassettes: [ + { denomination: 20, count: 100 }, + { denomination: 50, count: 50 }, + ], + }, +}) + +// Convert to HAL config format +const halConfig = toHalConfig(config) +``` + +## Initialization + +### For Production (Recommended) + +The simplest way to initialize with hardware: + +```typescript +import { useAtmStore } from '@/stores/atm' + +const atm = useAtmStore() + +// Uses environment variables with Sintra defaults +await atm.initializeForProduction() +``` + +### With Custom Config + +```typescript +import { useAtmStore } from '@/stores/atm' +import { getDeviceConfig, toHalConfig } from '@/config' + +const atm = useAtmStore() + +const config = getDeviceConfig('sintra', 'USD', { + dispenser: { + type: 'f56', + device: '/dev/ttyJ7', + cassettes: [{ denomination: 20, count: 100 }], + }, +}) + +await atm.initializeWithHal(toHalConfig(config)) +``` + +## Verifying Device Paths + +On a Sintra running Linux, verify the serial devices exist: + +```bash +ls -la /dev/ttyJ* +``` + +Expected output: + +``` +lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ4 -> ttyS4 +lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ5 -> ttyS5 +lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ7 -> ttyS7 +``` + +If the symlinks don't exist, check the udev rules or use the underlying `/dev/ttyS*` devices directly. + +## Troubleshooting + +### Permission Denied on Serial Port + +Add your user to the `dialout` group: + +```bash +sudo usermod -a -G dialout $USER +# Log out and back in for changes to take effect +``` + +### Device Not Found + +1. Check the device exists: `ls -la /dev/ttyJ*` or `ls -la /dev/ttyS*` +2. Check dmesg for hardware detection: `dmesg | grep tty` +3. Verify udev rules are loaded: `udevadm info /dev/ttyS5` + +### Validator Not Responding + +1. Verify baud rate: ID003 uses 9600 baud, 8 data bits, even parity, 1 stop bit +2. Check cable connections +3. Power cycle the validator +4. Check validator is in "online" mode (not standalone) + +### Dispenser Not Dispensing + +1. Verify cassette is properly seated +2. Check for bill jams +3. Verify dispenser firmware is compatible with F56 protocol +4. Check the dispenser is powered and initialized