bug: dispense result mapping uses wrong index (amounts order vs cassette position) #6

Closed
opened 2026-03-22 21:13:53 +00:00 by padreug · 1 comment
Owner

Summary

brain.js maps dispense results by iterating over the requested amounts array and indexing into the hardware result array by the same index. But these two arrays use different ordering:

  • amounts (the input): ordered by whatever the state machine sends (denomination order)
  • result.value (from hardware): always ordered by physical cassette position

If the amounts array and cassette positions don't align (e.g. cassette 0 = $5, cassette 1 = $20, but amounts requests $20 first), the dispensed/rejected counts get swapped between denominations.

Affected Code

lamassu-machine lib/brain.js_physicalDispense and _batchesFinished:

The dispense result from the hardware driver (e.g. F56) returns an array indexed by cassette position. When mapping back to denominations, the code assumes the input amounts array has the same ordering as the cassette positions — which is only true when denominations happen to be requested in cassette order.

Impact

  • Inventory tracking: cassette counts decremented against wrong denomination
  • Operator reporting: dispensed_1/dispensed_2 in cash_out_txs may show swapped counts
  • Error diagnosis: a jam on cassette 1 could appear as a jam on cassette 2 in the DB

In practice this is often masked because many deployments use a single denomination or cassettes happen to be loaded in the same order as the sort. But multi-denomination setups with non-ascending cassette loading will silently misattribute results.

Fix

Map results by cassette position (matching the hardware driver's indexing), then look up which denomination each cassette holds:

// WRONG: amounts[i] != cassette position i
const bills = amounts.map((a, i) => ({
  denomination: a.denomination,
  dispensed: result.value[i]?.dispensed ?? 0,
  rejected: result.value[i]?.rejected ?? 0,
}))

// CORRECT: iterate by cassette position, filter to requested
const cassetteResults = cassetteDenominations.map((denom, i) => ({
  denomination: denom,
  dispensed: result.value[i]?.dispensed ?? 0,
  rejected: result.value[i]?.rejected ?? 0,
}))
const bills = cassetteResults.filter(c => /* was requested */)

Context

Found and fixed in lamassu-next (feat/record-failed-dispense branch) in both apps/machine/src/services/hal.ts and apps/machine/electron/hal-service.ts. The same pattern exists in lamassu-machine's brain.js.

## Summary `brain.js` maps dispense results by iterating over the **requested amounts array** and indexing into the **hardware result array** by the same index. But these two arrays use different ordering: - `amounts` (the input): ordered by whatever the state machine sends (denomination order) - `result.value` (from hardware): always ordered by **physical cassette position** If the amounts array and cassette positions don't align (e.g. cassette 0 = $5, cassette 1 = $20, but amounts requests $20 first), the dispensed/rejected counts get swapped between denominations. ## Affected Code **lamassu-machine** `lib/brain.js` — `_physicalDispense` and `_batchesFinished`: The dispense result from the hardware driver (e.g. F56) returns an array indexed by cassette position. When mapping back to denominations, the code assumes the input amounts array has the same ordering as the cassette positions — which is only true when denominations happen to be requested in cassette order. ## Impact - **Inventory tracking**: cassette counts decremented against wrong denomination - **Operator reporting**: `dispensed_1`/`dispensed_2` in `cash_out_txs` may show swapped counts - **Error diagnosis**: a jam on cassette 1 could appear as a jam on cassette 2 in the DB In practice this is often masked because many deployments use a single denomination or cassettes happen to be loaded in the same order as the sort. But multi-denomination setups with non-ascending cassette loading will silently misattribute results. ## Fix Map results by cassette position (matching the hardware driver's indexing), then look up which denomination each cassette holds: ```js // WRONG: amounts[i] != cassette position i const bills = amounts.map((a, i) => ({ denomination: a.denomination, dispensed: result.value[i]?.dispensed ?? 0, rejected: result.value[i]?.rejected ?? 0, })) // CORRECT: iterate by cassette position, filter to requested const cassetteResults = cassetteDenominations.map((denom, i) => ({ denomination: denom, dispensed: result.value[i]?.dispensed ?? 0, rejected: result.value[i]?.rejected ?? 0, })) const bills = cassetteResults.filter(c => /* was requested */) ``` ## Context Found and fixed in lamassu-next (`feat/record-failed-dispense` branch) in both `apps/machine/src/services/hal.ts` and `apps/machine/electron/hal-service.ts`. The same pattern exists in lamassu-machine's brain.js.
Author
Owner

Closing as part of aiolabs/lamassu-server archival.

Already fixed in aiolabs/lamassu-next feat/record-failed-dispense branch per this issue's own body note. The cassette-position-vs-amounts-order indexing bug was caught and corrected in both apps/machine/src/services/hal.ts and apps/machine/electron/hal-service.ts. The same fix on the legacy lamassu-machine brain.js is moot under repo archival — that code path no longer runs anywhere in the Nostr-native stack.

Disposition tracked in aiolabs/satmachineadmin#40.

Closing as part of `aiolabs/lamassu-server` archival. **Already fixed in `aiolabs/lamassu-next`** `feat/record-failed-dispense` branch per this issue's own body note. The cassette-position-vs-amounts-order indexing bug was caught and corrected in both `apps/machine/src/services/hal.ts` and `apps/machine/electron/hal-service.ts`. The same fix on the legacy `lamassu-machine` `brain.js` is moot under repo archival — that code path no longer runs anywhere in the Nostr-native stack. Disposition tracked in `aiolabs/satmachineadmin#40`.
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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/lamassu-server#6
No description provided.