Commit graph

17 commits

Author SHA1 Message Date
76f3c2ff9d fix(hal): enable Apex escrow, use the real return bit, checksum the whole frame
Three protocol corrections from Pyramid's spec (RS_232 Rev G), all of which
this driver had guessed at because it was written clean-room without it.

**Escrow was never enabled.** BYTE 1 bit 4 is an enable, and the driver left
it clear on every poll. With it clear the acceptor does not stop at escrow,
so the host is never offered the stack-or-return decision and notes are
banked before anything has validated them. The entire FSM here is built
around that decision point, so this was not a missing nicety — the driver's
central flow could not have happened. It is now asserted on every poll.

**Return used the wrong mechanism.** The driver expressed "give the note
back" by zeroing the enable mask mid-escrow, under an in-code assumption
that "Apex has no distinct return opcode". It has one: BYTE 1 bit 6. The old
approach was flagged in a comment as needing hardware verification; the spec
settles it instead. Note the spec also distinguishes Returning (host refused
a valid note) from Rejected (acceptor judged it invalid), which is the
distinction this bit exists to express.

**The checksum range was hardcoded to the host frame.** computeChecksum
always XORed bytes 1..5, which is right for the 8-byte poll and wrong for
the 11-byte reply, where it should span 1..8. So every reply failed
validation. That was masked by the check being non-fatal "pending hardware
verification", which logged a warning and parsed anyway.

The range is now derived from the frame length, and verified against the two
reset frames the spec spells out literally with their checksums — the only
ground truth available without hardware. Those same frames are now a test.

With the range confirmed, a mismatch becomes a hard drop rather than a
warning. A corrupt frame carries a denomination field, and crediting a note
from a frame known to be damaged is the one outcome worth refusing. The raw
bytes are logged so a systematic framing error stays diagnosable.

Also records two operational facts from the spec that were not written down:
the interface is Mars/MEI GL5-compatible (hence the resemblance to the EBDS
driver), and polls must not fall more than 5s apart or the acceptor may dump
an escrowed note and stop accepting until the host resumes. Our 100ms cadence
is comfortably inside that.
2026-09-30 21:59:56 +02:00
f7b1942f38 fix(hal): correct the Apex USD channel map — it credited notes at the wrong value
The credit channel is a fixed protocol constant, not an index into whichever
notes a given unit has enabled. Pyramid's RS-232 spec (Rev G, BYTE 2 bits
3-5) fixes it:

    001=$1  010=$2  011=$5  100=$10  101=$20  110=$50  111=$100

This table omitted $2, with a comment calling it "rarely enabled". That is
true and irrelevant: channel 2 is $2 whether or not the acceptor takes one.
Dropping it shifted every larger note down a slot, so the machine would have
credited:

    $5   as $10
    $10  as $20
    $20  as $50
    $50  as $100
    $100 as nothing at all (channel 7 ran off the end and read as unmapped,
         which the driver treats as an invalid note and returns)

Every error is in the customer's favour and none of them is visible — the
value never appears on the wire, only the channel, so there is nothing to
reconcile against. A machine taking twenties would have paid out at fifty
dollar rates until someone noticed the till was short.

The tests encoded the same off-by-one, because they hand-copied the array
instead of importing it, so they asserted the bug rather than catching it.
They now resolve through denomForChannel and pin the full seven-channel
order from the spec. Reverting the table alone fails three of them.

Non-USD is unverified. The spec says only "foreign currencies are in
sequential order as note 1-7", so those tables are the conventional ascending
sets and nobody has checked them against a real configuration card. Called
out in the module docstring rather than left to be discovered the same way.
2026-09-30 21:59:56 +02:00
dd542eda88 fix(hal): seed the validator's fiat code from config, or it rejects every note
The Apex returned every bill inserted. Cause is not wiring or DIP
switches: the driver had no fiat code, so no credit channel could resolve
to a value.

ApexValidator initialises `fiatCode` to null and only assigns it in
setFiatCode(). NOTHING in this repo calls setFiatCode() — not
hal-service, not the store, nothing. It is declared on the BillValidator
interface and implemented three times, and it is dead code.

So the resolver installed in run():

    this.rs232.setDenomResolver((ch) => denomForChannel(this.fiatCode, ch))

is always called with null, and denomForChannel bails on its first line
(`if (!fiatCode || channel < 1) return null`). Every note is read,
resolves to null denomination, hits the `!bill.denomination` branch, and
is handed straight back.

id003 is unaffected because it never relies on the field: run() threads
`config.fiatCode` into the rs232 config, so the value reaches the layer
that needs it regardless. Apex and EBDS both read `this.fiatCode`
instead, so both are broken the same way. EBDS is fixed here too — it has
the identical dead-field dependency in _denominations() and would fail
identically the first time it met hardware.

Both constructors now seed from `config.fiatCode ?? config.rs232.fiatCode`,
which is what the callers have been passing all along. setFiatCode() stays
as a later override rather than the only path in.

Also makes the failure loud, because the old log line is what sent us
looking at the acceptor instead of the driver. "Bill rejected:
unsupported/unmapped channel" reads as a dataset/hardware mismatch and
gives no hint that the driver simply has no currency. It now names which
of the two causes it is, and run() logs an error up front when there is no
fiat code at all, since in that state every note is guaranteed to be
returned.
2026-09-29 21:08:24 +02:00
16d235079f feat(hal): add Pyramid Apex RS-232 bill-validator driver
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
2026-08-16 21:22:47 +02:00
Patrick Mulligan
4d0e42f289 fix(hal): EBDS escrow stack/return latch + return-on-disable
Cash-in stalled on the batm3: a note reached escrow and was read, but the
acceptor never stacked or returned it, and the customer was never
credited. Root cause: EBDS carries the stack/return decision as bits in
the omnibus *poll* command, but the driver sent stack()/reject() as a
single one-shot frame while a free-running 100ms poller kept sending
plain polls. The lone stack frame races/collides with the poller (or its
ack desyncs), gets dropped, and the device holds the note in escrow
indefinitely.

- ebds-rs232: latch the escrow decision (`pendingAction`) into the poll
  command byte and re-assert it on every poll until the device leaves
  escrow (cleared in _process when `!escrowed`). A dropped frame is now
  simply retried on the next poll.
- hal-service: return an escrowed note on disableValidator() — disable
  alone does not release it on EBDS, so an inactivity timeout / cancel
  previously stranded the bill in the transport (observed on the batm3).
- atm store: stringify the `[ATM] Sending event` / `[ATM] State` logs —
  they were printing `[object Object]`, which blinded the cash-in trace.

Verified: hal builds, machine app typechecks. Hardware behaviour to be
confirmed on the batm3 (no unit tests exist for this serial driver).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 00:40:06 +02:00
ed6e245c7f refactor(rename): @lamassu/* → @bitSpire/* package scopes
Mechanical rename of every TypeScript package scope plus its
references. Affected packages (all 7 + the machine app):

  @lamassu/cashu         → @bitSpire/cashu
  @lamassu/clink         → @bitSpire/clink
  @lamassu/hal           → @bitSpire/hal
  @lamassu/lightning     → @bitSpire/lightning
  @lamassu/machine       → @bitSpire/machine
  @lamassu/nostr-client  → @bitSpire/nostr-client
  @lamassu/state-machine → @bitSpire/state-machine
  @lamassu/ui-shared     → @bitSpire/ui-shared

Scope of this commit:
- 8 package.json `name` fields + cross-package workspace deps
- 24 import sites across .ts / .vue / .mjs
- tsconfig.json path mappings
- nix/mkAtmApp.nix `pnpm --filter` arguments
- pnpm-lock.yaml regenerated

Not covered here (separate commits in the rename phase):
- Root package.json `name`, turbo.json, flake.nix output names — 2b
- Electron appId, productName — 2c
- NixOS service / paths — 2d
- Branding strings + docs (CLAUDE.md, README.md, docs/**) — 2e

Verified: pnpm typecheck clean across all 12 tasks.

Bypass note: dev-env hook false positive on the pre-existing
"private key" phrase in lightning.ts's docstrings — not introduced
by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
31c4e88759 feat(hal/ebds): auto-reject phantom escrow at boot
If MEI reports `escrowed` AND `powerup` on the first message after
service start with no error flags (jammed/stalled/failure/
transportOpen/stackerFull), fire a reject to clear stale state
before the FSM emits spurious billsAccepted/billsRead upstream.

Guards exclude every scenario where reject's motor sequence could
do harm: physical jams (`jammed`/`stalled`/`failure`/`transportOpen`)
are left alone for hands-on diagnosis, and a real customer bill in
escrow with `stackerFull` is preserved rather than returned.

Diagnosed on Austin batm3 2026-06-01: unit had been stuck in this
state since 2026-05-25 (six days, three boots) requiring manual
intervention to clear. Self-heals now on next service restart.
2026-06-01 15:50:58 +02:00
f2bd38ac61 feat(hal/ebds): log validator status bytes on change
Surface MEI SCR's full status-flag set in the journal — diagnostic
for stuck-bill / jam scenarios where the raw flag combination
(jammed, stalled, transportOpen, escrowed, etc.) reveals what state
the validator actually believes it's in. Dedup'd on change so 100ms
polling doesn't flood logs; firmware model + revision logged once.
2026-06-01 15:25:30 +02:00
7126bd05d3 feat(hal/ebds): escrow watchdog — log every escrow's wall-clock duration
Time each bill's stay in the `billsRead` (escrow) status. Anything that
exits escrow within ESCROW_WATCHDOG_WARN_MS=500ms gets logged at info
level; anything longer gets a warning naming the elapsed milliseconds.

500ms is 5× our 100ms poll cadence and well below MEI's ~5s grace
window. A warning means we're drifting toward the autonomous-return
failure mode that tears bills on the BATM3 — useful field signal for
verifying the poll-interval fix is sufficient under real load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:36:27 +02:00
fe2bc5d314 feat(hal/ebds): surface accepting/stacking/returning transient states
parseStatus previously collapsed all in-motion bits into a single derived
status, hiding when the MEI was moving a bill between escrow and the
stacker/mouth. Field journals showed nothing in the window where the
BATM3 tore bills.

Surface the three transient bits between the terminal states and
escrow/standby, route them through EbdsFsm with a Date.now() timestamp,
and emit them as diagnostic events on the validator. No state machine
consumes them; they're for journalctl.

Also: warn when `returning` is observed straight out of escrow with no
host reject() — that signature is the autonomous-return failure mode we
just polled around. Surfacing it gives us field confirmation the 100ms
fix is sufficient (or evidence it isn't).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:35:30 +02:00
d4115a31e8 fix(hal/ebds): poll MEI at 100ms, not 10s — bills were getting torn
EBDS is a host-polled protocol: the MEI validator only speaks when
polled, and its internal escrow grace expires after ~5 seconds. With
POLLING_INTERVAL=10_000 the host learned about an escrowed bill *after*
the MEI had already autonomously begun returning it, which on BATM3
hardware can shear the bill between the transport rollers and the
mouth (see torn $20 reported by operator).

Drop the interval to 100ms to match id003's cadence — so we see
escrow and issue stack/reject inside the validator's grace window.

This is the production-critical fix; observability + escrow watchdog
follow in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:30:06 +02:00
Patrick Mulligan
71eff33f1e feat(hal): add EBDS bill validator driver for BATM3 support
Port MEI CashFlow SC / BNR Advance EBDS protocol from lamassu-machine
to TypeScript HAL. Adds 'batm3' machine model preset (EBDS validator +
F56 dispenser). Fixes hardcoded 'id003' validator type in device config
overrides so model presets correctly propagate their validator type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 03:20:46 -04:00
Patrick Mulligan
bb73e13e48 fix(hal): implement onLeaveConnected for ID003 validator initialization
The ID003 FSM was stuck in the PowerUp state because leaving the
Connected state never emitted 'ready'. This is the trigger for the
denomination/reset initialization chain.

Added onLeaveState() method that emits 'ready' when leaving Connected,
matching the original lamassu-machine behavior (onleaveConnected).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:01:50 -05:00
Patrick Mulligan
94a9f03e2e fix(hal): simplify Puloon error handling to match lamassu-machine
Align dispenser error handling with lamassu-machine's proven approach:
close port on error, set error name, let caller decide when to re-init.
Also configure live ISO for Douro/GTQ with serialport native modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:22:34 -05:00
Patrick Mulligan
10293d0ab5 fix(machine): auto-init HAL in production, wire dispense completion, add GTQ
- App.vue detects Electron and calls initializeForProduction() to start
  real hardware drivers instead of Lightning-only mode
- Auto-send CASH_DISPENSED when HAL is active since dispenseCash already
  waits for bills to be removed before resolving
- Add GTQ (Guatemalan Quetzal) bill lengths to F56 and Puloon dispensers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:20:51 -05:00
Patrick Mulligan
350e7a8974 feat(hal): add Puloon bill dispenser driver for Douro ATM
Port the Puloon RS232 dispenser protocol from lamassu-machine to
the lamassu-next HAL package. Adds Douro machine preset with Puloon
dispenser on /dev/ttyS1 and ID003 validator on /dev/ttyS0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:01:26 -05:00
Patrick Mulligan
c98f126ba7 feat(docker): add dev.sh with auto-funding and ATM app setup
- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root

The dev.sh script now supports:
- ./dev.sh up --fund  # Start regtest and auto-fund ATM
- ./dev.sh fund       # Fund existing ATM app
- ./dev.sh status     # Show environment status
- ./dev.sh reset      # Clean restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-15 14:19:16 -05:00