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'
|
||||
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<NonNullable<typeof actor.value>['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,
|
||||
|
|
|
|||
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