feat: Raspberry Pi 5 (aarch64) target + Pyramid Apex RS-232 bill validator #87

Open
padreug wants to merge 4 commits from feat/rpi5-apex-hal into dev
Owner

Adds a second reference hardware platform for bitSpire — a DIY Raspberry Pi 5 build — plus the bill-validator driver its parts need. Groundwork for the "install bitspire from these parts" build (Pi 5 + Apex 7600 acceptor + 7" touch + QR scanner + SATA SSD). Everything here is additive; the x86 fleet (sintra/douro/tejo/batm3) is byte-identically unaffected (verified sintra-installed toplevel unchanged).

Three commits, independently reviewable:

1. feat(hal) — Pyramid Apex RS-232 driver

Neither the Apex 7600 nor the NV10 USB+ speaks id003/ebds (the only validators we shipped). The Apex speaks Pyramid RS-232; this adds a clean-room driver written from Pyramid's public protocol spec + their published Python reference host — license-clean, and it respects the lamassu-machine provenance boundary (no code sourced from that tree).

  • packages/hal/src/validators/apex/apex-rs232.ts — protocol layer (8-byte poll frame, XOR checksum, ACK toggle, state/event/credit response bytes). Pure, testable fns.
  • apex-fsm.ts — status tracker → BillValidator events (dedupe + escrow watchdog), mirrors EbdsFsm.
  • denominations.ts — per-fiat channel→value table.
  • index.ts — ApexValidator implements BillValidator, 100ms poll, mirrors EbdsValidator.
  • validators/index.ts — wired into the factory (ValidatorType gains 'apex').
  • 13 unit tests, all passing.

Bench-verify before trusting on real hardware: the reply-checksum range and the return-by-disable (reject) behaviour are flagged in-code as needing confirmation on an actual 7600. The NV10 USB+ (SSP/eSSP) driver is deliberately deferred — one driver now, swap later.

2. feat(deploy) — aarch64 Pi 5 target

  • flake.nix — nixos-hardware input, aarch64 pkgs, and the Pi build machinery. Runtime mirrors mkInstalledConfig (bitspire service, env seed, electron override) on aarch64 + Pi hardware, dropping x86-only bits (determinate, atm-tui) for first bring-up.
  • deploy/nixos/hardware/raspberry-pi-5.nix — the aarch64 twin of upboard.nix: extlinux boot, console=tty0 (UART free for a GPIO-wired validator), vc4/v3d KMS for the kiosk display, no-suspend, and /dev/ttyValidator{0,1,2} udev symlinks for FTDI / CP210x / CH340 USB-serial bridges.

3. feat(deploy) — split rpi5 into installed + image variants

rpi5-installed originally bundled the sd-image module, so it could only build a flashable image — an in-place nixos-rebuild switch against it would drag in the image builder. Split it, mirroring the x86 mkLiveConfig/mkInstalledConfig separation:

  • rpi5-installed → in-place rebuild target. Declares the flashed media's own root fs (NIXOS_SD / FIRMWARE), nothing image-specific. Targeted by the aarch64 twin of the fleet deploy ritual:
    sudo nixos-rebuild switch --flake \
      "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#rpi5-installed"
    
  • rpi5-image → same runtime + the sd-image builder; packages.aarch64-linux.sd-image-rpi5 points here.

Also folds the aiolabs cachix substituter + trusted key into the Pi's nix.settings, so a remote rebuild substitutes the heavy aarch64 closure instead of compiling on the Pi.


Build note: any Pi target needs an aarch64 builder (native Pi / arm box / binfmt) — the app closure won't build on x86. First flash: nix build .#packages.aarch64-linux.sd-image-rpi5 → dd. Updates thereafter: the nixos-rebuild switch remote-flake command above.

🤖 Generated with Claude Code

Adds a second reference hardware platform for bitSpire — a DIY Raspberry Pi 5 build — plus the bill-validator driver its parts need. Groundwork for the "install bitspire from these parts" build (Pi 5 + Apex 7600 acceptor + 7" touch + QR scanner + SATA SSD). Everything here is **additive**; the x86 fleet (sintra/douro/tejo/batm3) is byte-identically unaffected (verified `sintra-installed` toplevel unchanged). Three commits, independently reviewable: ### 1. `feat(hal)` — Pyramid Apex RS-232 driver Neither the Apex 7600 nor the NV10 USB+ speaks id003/ebds (the only validators we shipped). The Apex speaks **Pyramid RS-232**; this adds a clean-room driver written from Pyramid's public protocol spec + their published Python reference host — license-clean, and it respects the lamassu-machine provenance boundary (no code sourced from that tree). - `packages/hal/src/validators/apex/apex-rs232.ts` — protocol layer (8-byte poll frame, XOR checksum, ACK toggle, state/event/credit response bytes). Pure, testable fns. - `apex-fsm.ts` — status tracker → `BillValidator` events (dedupe + escrow watchdog), mirrors `EbdsFsm`. - `denominations.ts` — per-fiat channel→value table. - `index.ts` — `ApexValidator implements BillValidator`, 100ms poll, mirrors `EbdsValidator`. - `validators/index.ts` — wired into the factory (`ValidatorType` gains `'apex'`). - 13 unit tests, all passing. **Bench-verify before trusting on real hardware:** the reply-checksum range and the return-by-disable (reject) behaviour are flagged in-code as needing confirmation on an actual 7600. The NV10 USB+ (SSP/eSSP) driver is deliberately deferred — one driver now, swap later. ### 2. `feat(deploy)` — aarch64 Pi 5 target - `flake.nix` — `nixos-hardware` input, aarch64 pkgs, and the Pi build machinery. Runtime mirrors `mkInstalledConfig` (bitspire service, env seed, electron override) on aarch64 + Pi hardware, dropping x86-only bits (determinate, atm-tui) for first bring-up. - `deploy/nixos/hardware/raspberry-pi-5.nix` — the aarch64 twin of `upboard.nix`: extlinux boot, `console=tty0` (UART free for a GPIO-wired validator), vc4/v3d KMS for the kiosk display, no-suspend, and `/dev/ttyValidator{0,1,2}` udev symlinks for FTDI / CP210x / CH340 USB-serial bridges. ### 3. `feat(deploy)` — split rpi5 into installed + image variants `rpi5-installed` originally bundled the sd-image module, so it could only build a flashable image — an in-place `nixos-rebuild switch` against it would drag in the image builder. Split it, mirroring the x86 `mkLiveConfig`/`mkInstalledConfig` separation: - **`rpi5-installed`** → in-place rebuild target. Declares the flashed media's own root fs (NIXOS_SD / FIRMWARE), nothing image-specific. Targeted by the aarch64 twin of the fleet deploy ritual: ``` sudo nixos-rebuild switch --flake \ "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#rpi5-installed" ``` - **`rpi5-image`** → same runtime + the sd-image builder; `packages.aarch64-linux.sd-image-rpi5` points here. Also folds the aiolabs cachix substituter + trusted key into the Pi's `nix.settings`, so a remote rebuild substitutes the heavy aarch64 closure instead of compiling on the Pi. --- **Build note:** any Pi target needs an aarch64 builder (native Pi / arm box / binfmt) — the app closure won't build on x86. First flash: `nix build .#packages.aarch64-linux.sd-image-rpi5` → `dd`. Updates thereafter: the `nixos-rebuild switch` remote-flake command above. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
New `apex` validator for Pyramid Technologies Apex-series acceptors
(Apex 5000/7000/7600) on their RS-232 interface, for the Raspberry Pi 5
build. Implemented from Pyramid's PUBLIC protocol facts (RS-232 Serial
Interface Specification + their published integrator samples) — not
ported from lamassu-machine or any licensed source, so it stays inside
this repo's provenance boundary and AGPL.

- apex-rs232.ts: 8-byte poll frame (STX/len/ctrl+ACK-toggle/enable/cmd/
  rsvd/ETX/XOR-checksum), reply parsing (state+event+credit bytes),
  escrow stack/return latching re-asserted until the note leaves escrow.
- apex-fsm.ts: status tracker → BillValidator events (mirrors the EBDS
  tracker; dedupes continuous polling; escrow-latency watchdog).
- denominations.ts: per-fiat channel→value table (channel order must
  match the acceptor's programmed dataset — verify on the unit).
- index.ts: ApexValidator implementing BillValidator; wired into the
  createValidator factory as ValidatorType 'apex'.
- 13 unit tests for checksum, frame build, status priority, denom map.

Bench-verify on real hardware before trusting: the reply checksum range
(parsed leniently for now) and return-by-disable escrow behaviour are
flagged in-code as needing confirmation on the 7600.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ivBosaWmv8vwFE7ejrdHW
Proper aarch64 NixOS target for the DIY Pi 5 build, wired additively so
the x86 fleet path is untouched (both the new Pi sd-image and the
existing sintra-installed still evaluate cleanly):

- nixos-hardware input (raspberry-pi-5 module) for Pi kernel/firmware/GPU.
- aarch64 pkgs + pkgs-unstable + mkAtmApp instances (parallel to x86).
- mkPiConfig: aarch64 nixosSystem reusing the shared configuration.nix +
  bitspire-atm service, replicating the installed-config runtime (bitspire
  service, first-boot env seed, electron service override, swap). Drops
  the x86 fleet machinery for a first bring-up: no determinate/autoUpgrade
  (not yet fleet-managed) and no atm-tui (needs an aarch64 package).
- raspberry-pi-5.nix hardware module: extlinux boot, vc4/v3d KMS for X,
  primary UART free for a GPIO-wired validator, no-suspend, and stable
  /dev/ttyValidator* udev symlinks for USB-serial validator adapters
  (Apex 7600 RS-232 via adapter, NV10 USB+).
- nixosConfigurations.rpi5-installed + packages.aarch64-linux.sd-image-rpi5.

BUILD NOTE: the app closure (aarch64 electron/native addons) needs an
aarch64 builder — a native Pi/arm box or `boot.binfmt` qemu emulation on
an x86 host. Config evaluates on x86; it just can't build there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ivBosaWmv8vwFE7ejrdHW
`rpi5-installed` previously bundled the sd-image-aarch64 module, so it
was only good for building a flashable image — a `nixos-rebuild switch`
against it (local or the git+ssh remote form) would drag in the image
builder and its image-specific fs wiring. Split it, mirroring the x86
fleet's mkLiveConfig/mkInstalledConfig separation:

- rpi5-installed → in-place rebuild target. Declares the flashed media's
  own root fs (NIXOS_SD / FIRMWARE labels), nothing image-specific. This
  is what
    sudo nixos-rebuild switch --flake \
      "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=<branch>#rpi5-installed"
  targets, the aarch64 equivalent of the sintra/douro deploy ritual.
- rpi5-image → same shared runtime + the sd-image builder. Its
  system.build.sdImage is the flashable artifact; packages.aarch64-linux
  .sd-image-rpi5 now points here.

Shared runtime extracted into piBaseModules/mkPiRuntime; folded the
aiolabs cachix substituter + trusted key into the Pi's nix.settings so a
remote rebuild substitutes the heavy aarch64 closure instead of building
it on the Pi (no max-jobs/timeout watchdog — the Pi 5 can build locally
if it must).

Verified: rpi5-installed evaluates to a valid system toplevel (root fs
present), rpi5-image/sd-image-rpi5 to the .img.zst builder, and x86
sintra-installed is byte-identically unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGUJJjBDuYwRaWSkFdK3bm
raspberry-pi-5.nix set `boot.kernelParams = lib.mkDefault [ "console=tty0" ]`
to keep the serial console off the GPIO UART so a validator can own it. It
never took effect: kernelParams is list-merged, and only definitions at the
highest priority survive — nixpkgs defines loglevel/lsm at normal priority,
so the mkDefault list was discarded wholesale. Effective params on
rpi5-installed were `[ "loglevel=4" "lsm=landlock,yama,bpf" ]`, no console=
at all, which makes the kernel fall back to the device tree's stdout-path:
that same UART.

Drop the mkDefault so the entry merges. Verified by evaluating
config.boot.kernelParams before/after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4
Author
Owner

Added one commit: 082f738 fix(deploy): console=tty0 was being dropped on the Pi 5.

It was sitting on feat/rpi4-target, so the fix for the bug this PR introduces only existed on an unproposed branch. If #87 had merged as it was, it would have merged with the bug. Cherry-picked here so the PR is correct on its own.

The Pi 4 work is now #110, stacked on this branch. Merge this one first; #110 retargets to dev afterwards.

Added one commit: `082f738 fix(deploy): console=tty0 was being dropped on the Pi 5`. It was sitting on `feat/rpi4-target`, so the fix for the bug this PR introduces only existed on an unproposed branch. If #87 had merged as it was, it would have merged with the bug. Cherry-picked here so the PR is correct on its own. The Pi 4 work is now #110, stacked on this branch. Merge this one first; #110 retargets to `dev` afterwards.
This pull request has changes conflicting with the target branch.
  • flake.nix
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/rpi5-apex-hal:feat/rpi5-apex-hal
git switch feat/rpi5-apex-hal

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch dev
git merge --no-ff feat/rpi5-apex-hal
git switch feat/rpi5-apex-hal
git rebase dev
git switch dev
git merge --ff-only feat/rpi5-apex-hal
git switch feat/rpi5-apex-hal
git rebase dev
git switch dev
git merge --no-ff feat/rpi5-apex-hal
git switch dev
git merge --squash feat/rpi5-apex-hal
git switch dev
git merge --ff-only feat/rpi5-apex-hal
git switch dev
git merge feat/rpi5-apex-hal
git push origin dev
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!87
No description provided.