bitspire/docs/device-configuration.md
Padreug 53b0d382e8 docs: finish the LNbits-era doc sweep across docs/ + .claude/skills/
Second + final batch of the doc refresh. README + CLAUDE went out in
8c9ae29; deploy/nixos/README + obsolete-flow flags in 924844f. This
commit covers everything left.

docs/machine-installation.md
  Was describing a manual AppImage scp deploy + a `lamassu-kiosk`
  systemd unit that hasn't been the deployment path for months.
  Replaced with a high-level "what the pipeline does and why"
  overview that points at deploy/nixos/README.md for the full
  command-by-command walkthrough. Includes the BATM3 chassis-mod
  note (custom Dell OptiPlex retrofit, not a stock Dell).

docs/architecture-comparison.md
  Rewrote the comparison to be lamassu-server (≤ v8.1.5) vs bitSpire
  (LNbits-backed) instead of the original lamassu-server vs LP-backed
  lamassu-next framing. Updated the cash-out + cash-in flow diagrams
  to show the actual nostr-transport path (LNbits-bundled nostrrelay
  extension at ws://<host>:5001/nostrrelay/test, no separate strfry
  container). Replaced the migration-path section with a softer
  "when to choose what" framing that includes Lamassu's current
  commercial offering as a legitimate third option. Added a header
  pointer to the Acknowledgements section.

docs/business-model.md
  Light touch-ups: Lightning.Pub → LNbits where it appeared, swapped
  the [[ndebit-cash-in-flow]] link for [[architecture-comparison]],
  noted the kind-30078 service beacon for availability broadcasts.

docs/device-configuration.md
  Dropped "Lamassu" branding from the machine-model headings
  (Sintra / tejo / douro / batm3 are referenced by hardware identity
  here, not by Lamassu's product line). Added the Sintra-specific
  ttyS4-vs-placeholder-ttyS1..3 gotcha we hard-learned during the
  first flash. Corrected the BATM3 entry: stock GeneralBytes chassis
  with a Dell OptiPlex 9030 AIO motherboard physically grafted in,
  NOT a Dell out of the box. Updated the example /dev/ttyJ* symlink
  output to match what a healthy Sintra actually shows.

docs/adr/001-hal-architecture.md
  ADRs are historical artifacts — kept the original decision text
  intact. Added a postscript noting:
    - The package rename @lamassu/hal → @bitSpire/hal
    - The v8.1.5 boundary on any lamassu-machine source-tree
      references (Lamassu's 2024-01-26 license transition)
    - That the "Remaining Work" list is complete and the first
      successful Sintra hardware integration ran on 2026-05-13

.claude/skills/lightning-check.md
  Rewrote end-to-end. Was Lightning.Pub-flavoured with CLINK kinds
  21001/21002 as the primary flows; now validates the LNbits nostr-
  transport surface (kind-21000 envelope, NIP-44 v2 encryption,
  subscribe_payments filter discipline, lnurlw link composition).
  Preserved a --clink mode for the still-live kind-21003 operator-
  management surface. Includes a "what to check" rubric for cash-out
  vs cash-in flows that mirrors the actual code in
  apps/machine/src/services/lightning.ts.

.claude/skills/hal-check.md
  Two pivots: (1) acknowledge ADR-001's TypeScript-not-Rust choice
  and reframe all the safety checklists in TS-flavour (type safety,
  discriminated unions, single-writer serial, bounded emitters)
  instead of Rust-flavour (unsafe, borrow checker). (2) Add explicit
  v8.1.5 provenance boundary plus a "forbidden operations" section
  that prohibits diffing or porting from v8.1.6+ lamassu-machine
  source. Updated the port-validation source-reference table to
  list TS file paths under packages/hal/ instead of Rust paths.

.claude/skills/docs.md
  @lamassu/* → @bitSpire/*. Replaced the Lightning.Pub mermaid
  diagram with a current cash-out flow showing the nostr-transport
  RPC + subscribe_payments push path. Left the createOffer noffer
  example in the API-docs template section since it's illustrative
  ("here's what a good TSDoc block looks like") rather than current
  reference documentation.

.claude/skills/test.md
  One-line: @lamassu/nostr-client → @bitSpire/nostr-client in the
  pnpm-filter example.

deploy/nixos/README.md
  Single touch-up: clarified the douro/batm3 hardware-module comments
  to reflect that BATM3 is a custom-installed Dell board in a
  GeneralBytes BATM3 chassis (not a Dell OEM).

Files NOT touched in this sweep (intentionally):
  - packages/hal/src/**/*.ts attribution comments — those reference
    "lamassu-machine" in their port-source headers. Those are
    factually accurate (the drivers ARE ported from there, up to
    v8.1.5) and constitute necessary license/attribution metadata.
    Editing them would erase the provenance trail.
  - .claude/skills/{nostr-check,security}.md — already protocol-
    neutral, no LP/lamassu references to clean up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00

6.9 KiB

Device Configuration

This document describes how to configure the ATM hardware for different machine models.

Overview

bitSpire ships built-in presets for the hardware platforms we've tested on. Configuration is handled through:

  1. Machine presets — built-in defaults for known hardware (Sintra, tejo, douro, batm3)
  2. Environment variables — override any setting at runtime
  3. Runtime overrides — programmatic configuration

Supported machine models

Sintra

The Sintra (Aaeon UP Board-based, Intel Atom x5-Z8350) uses:

Device Protocol Path Underlying device
Bill Validator ID003 /dev/ttyJ5 FTDI USB-serial → /dev/ttyUSB1
Bill Dispenser F56 /dev/ttyJ7 SoC MMIO UART → /dev/ttyS4
Printer Nippon /dev/ttyJ4 FTDI USB-serial → /dev/ttyUSB0

Hardware:

  • Platform: Aaeon UP Board (Intel Atom x5-Z8350)
  • Validator: JCM iVIZION
  • Dispenser: Fujitsu F53/F56

Sintra-specific gotcha: the kernel allocates ttyS0..ttyS3 as placeholder serial nodes that error on any I/O. Only ttyS0 (legacy 8250 at I/O 0x3f8) and ttyS4 (SoC MMIO 16550A at 0xa171b000) are real on this hardware. The bitspire-atm NixOS module's upboard.nix keeps the kernel console on tty0 only (not ttyS4) so userspace can claim ttyS4 for the F56 dispenser.

tejo

The Tejo runs on the same Aaeon UP Board family as Sintra; same validator + dispenser layout. Uses the same upboard.nix hardware module.

Douro

Runs on a Dell OptiPlex 9030 AIO (Intel i7-4790S, Intel HD 4600) — Lamassu's stock Douro motherboard. SATA SSD instead of eMMC, eGalax touchscreen, dispenser/validator on USB-attached FTDI bridges. See deploy/nixos/hardware/douro.nix for the specifics.

BATM3

The "batm3" target in this repo is not the stock GeneralBytes BATM3 — it's a custom modification where the original ARM/Android board has been physically replaced with a Dell OptiPlex 9030 AIO (same hardware family as the Douro). The chassis, cash-handling units (MEI SCR validator/recycler + Puloon F56 dispenser), and screen are stock BATM3; everything compute-side is grafted in. See deploy/nixos/hardware/batm3.nix for the boot configuration; functionally it shares the Dell-board layout with Douro but with different cash hardware on the inside.

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

# 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

export VITE_LAMASSU_VALIDATOR_DEVICE="/dev/ttyUSB0"
export VITE_LAMASSU_DISPENSER_DEVICE="/dev/ttyUSB1"

Cassette Configuration

Each cassette is defined with:

interface CassetteConfig {
  denomination: number // Bill denomination (e.g., 20 for $20)
  count?: number // Number of bills loaded (optional, for inventory tracking)
}

Default Sintra Configuration

[{ "denomination": 20, "count": 50 }]

This configures a single cassette with $20 bills, 50 bill capacity.

Multi-Cassette Example

[
  { "denomination": 20, "count": 100 },
  { "denomination": 50, "count": 50 },
  { "denomination": 100, "count": 25 }
]

Programmatic Configuration

You can also configure devices programmatically:

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

The simplest way to initialize with hardware:

import { useAtmStore } from '@/stores/atm'

const atm = useAtmStore()

// Uses environment variables with Sintra defaults
await atm.initializeForProduction()

With Custom Config

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:

ls -la /dev/ttyJ*

Expected output on a working Sintra:

lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ4 -> ttyUSB0
lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ5 -> ttyUSB1
lrwxrwxrwx 1 root root 5 May 13 09:22 /dev/ttyJ7 -> ttyS4

(ttyUSB0/ttyUSB1 are the two FTDI USB-serial bridges — printer + validator. ttyS4 is the SoC's on-carrier MMIO UART used for the F56 dispenser.)

If the symlinks don't exist, check the udev rules in deploy/nixos/hardware/upboard.nix, then run mdev -s (or udevadm trigger on a non-Alpine system) to repopulate /dev.

If you see a symlink pointing at ttyS1/ttyS2/ttyS3 instead, those are kernel-allocated placeholder nodes that always error on I/O — the udev rule needs to map to ttyS4 for Sintra. The current upboard.nix covers all the cases (ttyS1, ttyS4, ttyS5) so whichever real device shows up gets the ttyJ7 alias.

Troubleshooting

Permission Denied on Serial Port

Add your user to the dialout group:

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