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 <noreply@anthropic.com>
This commit is contained in:
parent
c26b6f6169
commit
a8cfaaf66f
4 changed files with 456 additions and 0 deletions
217
lamassu-next/apps/machine/src/config/device.ts
Normal file
217
lamassu-next/apps/machine/src/config/device.ts
Normal file
|
|
@ -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<MachineModel, Omit<DeviceConfig, 'fiatCode'>> = {
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
): 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<DeviceConfig> = {}
|
||||||
|
|
||||||
|
// 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()
|
||||||
15
lamassu-next/apps/machine/src/config/index.ts
Normal file
15
lamassu-next/apps/machine/src/config/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Configuration module
|
||||||
|
*
|
||||||
|
* Exports device configuration and utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
deviceConfig,
|
||||||
|
getDeviceConfig,
|
||||||
|
loadDeviceConfigFromEnv,
|
||||||
|
toHalConfig,
|
||||||
|
MACHINE_PRESETS,
|
||||||
|
type DeviceConfig,
|
||||||
|
type MachineModel,
|
||||||
|
} from './device'
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '@lamassu/state-machine'
|
} from '@lamassu/state-machine'
|
||||||
import { initializeLightningServices } from '@/services/lightning'
|
import { initializeLightningServices } from '@/services/lightning'
|
||||||
import type { HalConfig, HalServices } from '@/services/hal'
|
import type { HalConfig, HalServices } from '@/services/hal'
|
||||||
|
import { deviceConfig, toHalConfig } from '@/config'
|
||||||
import type { LightningPubClient } from '@lamassu/lightning'
|
import type { LightningPubClient } from '@lamassu/lightning'
|
||||||
import type { CLINKClient } from '@lamassu/clink'
|
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<NonNullable<typeof actor.value>['send']>[0]) {
|
function send(event: Parameters<NonNullable<typeof actor.value>['send']>[0]) {
|
||||||
if (!actor.value) {
|
if (!actor.value) {
|
||||||
console.error('[ATM] Cannot send event: machine not initialized')
|
console.error('[ATM] Cannot send event: machine not initialized')
|
||||||
|
|
@ -480,6 +506,7 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
initialize,
|
initialize,
|
||||||
initializeWithLightning,
|
initializeWithLightning,
|
||||||
initializeWithHal,
|
initializeWithHal,
|
||||||
|
initializeForProduction,
|
||||||
send,
|
send,
|
||||||
selectCashIn,
|
selectCashIn,
|
||||||
selectCashOut,
|
selectCashOut,
|
||||||
|
|
|
||||||
197
lamassu-next/docs/device-configuration.md
Normal file
197
lamassu-next/docs/device-configuration.md
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue