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>
239 lines
8.5 KiB
Markdown
239 lines
8.5 KiB
Markdown
# /hal-check — Hardware Abstraction Layer Agent
|
|
|
|
## Purpose
|
|
|
|
Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the v8.1.5 release line of `lamassu-machine` — which is the **last** lamassu-machine release published under a fully-open license.
|
|
|
|
> **Provenance boundary.** Drivers in `packages/hal/` derive from `lamassu-machine` at v8.1.5 and earlier (plus hardware-vendor protocol specs). Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 with v8.1.6+ gated behind a paid Operator Support Agreement. **Do not** reference, port, or diff against v8.1.6+ — the only safe upstream tree for porting is `v8.1.5` or earlier. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules.
|
|
|
|
> **Language note.** ADR-001 selected TypeScript-in-Electron over Rust-in-Tauri for the HAL. Earlier versions of this skill referenced Rust patterns; that's obsolete. All checks below are TypeScript-flavored.
|
|
|
|
## Invocation
|
|
|
|
```
|
|
/hal-check [command] [driver]
|
|
```
|
|
|
|
Commands:
|
|
|
|
- `port` — Validate that a driver matches its lamassu-machine v8.1.5 reference (where the driver was ported from one)
|
|
- `protocol` — Check protocol implementation against published vendor specs
|
|
- `safety` — Type safety, error handling, hardware safety review
|
|
- `mock` — Validate mock implementation completeness
|
|
|
|
Drivers:
|
|
|
|
- Validators: `id003`, `ccnet`, `cashflow_sc` (EBDS), `bnr_advance`, `genmega`, `hcm2`, `gsr50`
|
|
- Dispensers: `puloon`, `f56`, `genmega`, `hcm2`, `gsr50`
|
|
- Printers: `nippon`, `zebra`, `genmega`
|
|
|
|
## Porting validation (`port` command)
|
|
|
|
### Source reference
|
|
|
|
Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine v8.1.5 (the last fully-open release):
|
|
|
|
| TS Driver | JS Source (v8.1.5 release tree) |
|
|
|---|---|
|
|
| `validators/id003/*.ts` | `lib/id003/*.js` |
|
|
| `validators/ccnet/*.ts` | `lib/ccnet/*.js` |
|
|
| `validators/ebds/*.ts` | `lib/mei/cashflow_sc.js` + `lib/mei/*.js` |
|
|
| `dispensers/puloon/*.ts` | `lib/puloon/*.js` |
|
|
| `dispensers/f56/*.ts` | `lib/f56/*.js` |
|
|
|
|
If a driver in `packages/hal/` references a JS source NOT under v8.1.5 (or no JS source at all because it was re-derived from vendor specs), the attribution comment at the top of the TS file should say so.
|
|
|
|
### Porting checklist
|
|
|
|
```
|
|
/hal-check port id003
|
|
```
|
|
|
|
Validates:
|
|
|
|
- [ ] All protocol commands implemented (RESET, ENABLE, DISABLE, STACK, RETURN, STATUS — see ID003 spec or `lib/id003/id003rs232.js` from v8.1.5)
|
|
- [ ] State machine matches the JS FSM (`lib/id003/id003fsm.js`)
|
|
- [ ] CRC/checksum calculation byte-identical to the JS version
|
|
- [ ] Timeout values match the JS source
|
|
- [ ] Error codes mapped correctly to TypeScript enum/union
|
|
- [ ] Denomination tables match (currency-specific)
|
|
- [ ] Attribution comment at top of TS file points at the specific v8.1.5 file
|
|
|
|
### Protocol commands
|
|
|
|
#### ID003 (JCM)
|
|
|
|
| Command | Code | JS Reference (v8.1.5) |
|
|
|---|---|---|
|
|
| RESET | 0x40 | id003rs232.js:reset() |
|
|
| ENABLE | 0x13 | id003fsm.js:enable |
|
|
| DISABLE | 0x14 | id003fsm.js:disable |
|
|
| STACK | 0x15 | id003fsm.js:stack |
|
|
| RETURN | 0x16 | id003fsm.js:return |
|
|
| STATUS | 0x10 | id003fsm.js:poll |
|
|
|
|
#### Puloon LCDM
|
|
|
|
| Command | Code | JS Reference (v8.1.5) |
|
|
|---|---|---|
|
|
| RESET | 0x44 | puloonrs232.js |
|
|
| DISPENSE | 0x45 | puloonrs232.js |
|
|
| STATUS | 0x46 | puloonrs232.js |
|
|
|
|
## Safety review (`safety` command)
|
|
|
|
### Type safety
|
|
|
|
- [ ] No `any` types in driver public API
|
|
- [ ] Discriminated unions for state machine states (not just string literals)
|
|
- [ ] Error types are typed `Result<T, DriverError>` (or equivalent) at the driver boundary — never `throw` raw strings
|
|
- [ ] Buffer reads are bounds-checked; offsets validated before slicing
|
|
|
|
### Concurrency safety
|
|
|
|
- [ ] Serial port access is single-writer (TypeScript single-threaded helps, but ensure no two `await write()` calls can interleave with each other's expected response)
|
|
- [ ] Event emitters are bounded (max listeners set; back-pressure handled)
|
|
- [ ] Timeouts on all blocking I/O (`Promise.race` with `setTimeout`)
|
|
- [ ] No unbounded queues — a runaway validator polling loop shouldn't fill RAM
|
|
|
|
### Hardware safety
|
|
|
|
- [ ] Dispenser amounts validated against cassette inventory before dispensing
|
|
- [ ] Bill count cross-checked between requested and dispensed
|
|
- [ ] Error states trigger hardware reset (`disable + reset + enable` cycle for validators)
|
|
- [ ] Graceful degradation: partial dispense reports correct dispensed-vs-rejected counts
|
|
- [ ] Validator goes to `disable` on app shutdown — bills inserted while the app is dead get returned, not stacked uncounted
|
|
|
|
## Mock validation (`mock` command)
|
|
|
|
### Mock requirements
|
|
|
|
Mocks under `packages/hal/src/**/mock.ts` must simulate:
|
|
|
|
1. Normal operation flow
|
|
2. Error conditions (jam, empty cassette, comms failure)
|
|
3. Realistic timing (validator escrow takes ~600ms; dispenser cycle takes ~2s)
|
|
4. State persistence within a session
|
|
|
|
### Mock test coverage
|
|
|
|
```
|
|
/hal-check mock puloon
|
|
```
|
|
|
|
Validates mock implements:
|
|
|
|
- [ ] `connect()` — success and failure paths
|
|
- [ ] `dispense()` — full, partial, and over-requested paths
|
|
- [ ] `reset()` — error recovery
|
|
- [ ] Event emission timing roughly matches real hardware
|
|
- [ ] Cassette state tracking (count decrements properly)
|
|
|
|
## Protocol analysis (`protocol` command)
|
|
|
|
### Packet structure validation
|
|
|
|
```
|
|
/hal-check protocol id003
|
|
```
|
|
|
|
Compares TS packet building with the v8.1.5 JS reference:
|
|
|
|
```typescript
|
|
// TypeScript implementation
|
|
function buildPacket(data: Uint8Array): Uint8Array {
|
|
const packet = new Uint8Array(data.length + 4)
|
|
packet[0] = 0x02 // SYNC
|
|
packet[1] = data.length + 4 // length includes header + CRC
|
|
packet.set(data, 2)
|
|
const crc = calculateCrc(packet.slice(0, -2))
|
|
packet[packet.length - 2] = crc & 0xff
|
|
packet[packet.length - 1] = (crc >> 8) & 0xff
|
|
return packet
|
|
}
|
|
```
|
|
|
|
Against the v8.1.5 JS reference (line numbers may vary by tag):
|
|
|
|
```javascript
|
|
// lamassu-machine v8.1.5 — lib/id003/id003rs232.js
|
|
function buildPacket(data) {
|
|
const buf = Buffer.alloc(data.length + 4)
|
|
buf[0] = 0x02 // SYNC
|
|
buf[1] = data.length + 4
|
|
data.copy(buf, 2)
|
|
const crc = calculateCrc(buf.slice(0, -2))
|
|
buf.writeUInt16LE(crc, buf.length - 2)
|
|
return buf
|
|
}
|
|
```
|
|
|
|
A discrepancy here (different CRC polynomial, different framing, different endianness) is a serious port bug.
|
|
|
|
## Output format
|
|
|
|
### Port validation
|
|
|
|
```markdown
|
|
## HAL Port Validation: id003
|
|
|
|
### Command coverage
|
|
| Command | v8.1.5 JS | TS | Match |
|
|
|---|---|---|---|
|
|
| RESET | ✅ | ✅ | ✅ |
|
|
| ENABLE | ✅ | ✅ | ✅ |
|
|
| STATUS | ✅ | ⚠️ | Partial |
|
|
|
|
### Protocol differences
|
|
- [ ] `id003-rs232.ts:78` — CRC uses different polynomial than v8.1.5 JS
|
|
|
|
### Missing implementations
|
|
- [ ] HOLD command not implemented (v8.1.5 `id003fsm.js:holdBill`)
|
|
|
|
### Attribution
|
|
- [x] File header points at `lib/id003/id003rs232.js` (v8.1.5)
|
|
```
|
|
|
|
### Safety review
|
|
|
|
```markdown
|
|
## HAL Safety Review: puloon
|
|
|
|
### Type safety
|
|
- [x] No `any` types in public surface
|
|
- [x] Discriminated union for DispenserState
|
|
|
|
### Concurrency safety
|
|
- [x] Single-writer over serial port
|
|
- [x] Event emitter has bounded listener count
|
|
- [ ] `puloon-rs232.ts:142` — no timeout on response wait; could hang forever
|
|
|
|
### Hardware safety
|
|
- [x] Amount validated against cassette inventory in dispense()
|
|
- [ ] `puloon.ts:178` — partial dispense doesn't update cassette inventory
|
|
|
|
### Critical issues
|
|
1. `puloon-rs232.ts:142` — wrap response wait in `Promise.race([response, timeout(5000)])`
|
|
2. `puloon.ts:178` — decrement cassette count by actual dispensed, not requested
|
|
```
|
|
|
|
## Example usage
|
|
|
|
```
|
|
/hal-check port id003
|
|
/hal-check safety packages/hal/src/dispensers/puloon/
|
|
/hal-check mock --all
|
|
/hal-check protocol ebds # validates EBDS framing against v8.1.5 JS + MEI vendor spec
|
|
```
|
|
|
|
## Related skills
|
|
|
|
- `/security` — broader Bitcoin/Lightning/ATM vulnerability scan
|
|
- `/test` — runs HAL-specific tests, generates coverage reports
|
|
- `/lightning-check` — validates the Lightning backend side (LNbits transport)
|
|
|
|
## Forbidden operations
|
|
|
|
- Diff or read `lamassu-machine` source at v8.1.6 or later. Only `v8.1.5` (and the historical commit range leading up to it) is permissible to reference.
|
|
- "Backport" any fix or feature from v8.1.6+ JS sources into TypeScript. If a bug fix is needed, implement from the protocol spec or hardware traces.
|
|
- Include attribution comments pointing at v8.1.6+ files even if the implementation is your own — readers should be able to trust file-header attributions as accurate.
|