diff --git a/apps/machine/electron/hal-service.ts b/apps/machine/electron/hal-service.ts index 2ec863b..1259c77 100644 --- a/apps/machine/electron/hal-service.ts +++ b/apps/machine/electron/hal-service.ts @@ -376,12 +376,10 @@ export async function initializeHal(config: HalConfig): Promise { setCassettes: async (cassettes: CassetteConfig[]): Promise => { console.log( '[HAL] Hot-reloading cassette layout:', - cassettes - .map((c) => `bay${c.position}:${c.denomination}×${c.count ?? 0}`) - .join(', ') + cassettes.map((c) => `bay${c.position}:${c.denomination}×${c.count ?? 0}`).join(', ') ) - // Rebuild bays first so subsequent dispense calls see the new layout - // even if the dispenser re-init is slow / fails. + const previousBays = bays + const previousInitData = dispenserInitData bays = cassettes .slice() .sort((a, b) => a.position - b.position) @@ -391,31 +389,40 @@ export async function initializeHal(config: HalConfig): Promise { count: c.count ?? 0, })) dispenserInitData = { fiatCode: valConfig.fiatCode, cassettes } - // Close + re-init the dispenser so its internal per-bay state matches - // the new layout. Errors here surface to the caller (operator-config - // consumer) — the renderer can decide whether to retry. + // Close + re-init the dispenser so its internal per-bay state matches the + // new layout. close() now resolves only once the port is really closed, + // so the re-open below cannot race it (aiolabs/bitspire#118). + // + // On failure, roll the in-memory layout back. This reverses an earlier + // deliberate choice to keep the new bays "even if the dispenser re-init + // is slow / fails": with the re-init failing every time on douro, the app + // kept a layout the device had never taken and the operator-config + // consumer went on to publish a cassettes-state event advertising it. A + // subsequent dispense would then pick bays by a layout the hardware does + // not share. Better to surface the failure and stay truthful about what + // the device is actually running. try { - dispenser.close() + await dispenser.close() + await dispenser.init(dispenserInitData) } catch (err) { - console.warn('[HAL] Dispenser close during setCassettes raised:', err) + bays = previousBays + dispenserInitData = previousInitData + console.error('[HAL] Dispenser re-init failed; keeping the previous cassette layout:', err) + throw err } - await dispenser.init(dispenserInitData) console.log('[HAL] Dispenser re-initialized with new cassettes') }, cleanup: async () => { - return new Promise((resolve) => { - validator?.disable() - validator?.lightOff() - dispenser.close() - if (validator) { - validator.close((err?: Error) => { - if (err) console.error('[HAL] Validator close error:', err) - resolve() - }) - } else { + validator?.disable() + validator?.lightOff() + await dispenser.close() + if (!validator) return + await new Promise((resolve) => { + validator?.close((err?: Error) => { + if (err) console.error('[HAL] Validator close error:', err) resolve() - } + }) }) }, }