Compare commits
No commits in common. "47d3d0b237ca9b3be3d57392a0355a011733c969" and "7000720ae9217c204a54622cde456464699bfa13" have entirely different histories.
47d3d0b237
...
7000720ae9
6 changed files with 40 additions and 90 deletions
|
|
@ -376,10 +376,12 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
|
||||||
setCassettes: async (cassettes: CassetteConfig[]): Promise<void> => {
|
setCassettes: async (cassettes: CassetteConfig[]): Promise<void> => {
|
||||||
console.log(
|
console.log(
|
||||||
'[HAL] Hot-reloading cassette layout:',
|
'[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(', ')
|
||||||
)
|
)
|
||||||
const previousBays = bays
|
// Rebuild bays first so subsequent dispense calls see the new layout
|
||||||
const previousInitData = dispenserInitData
|
// even if the dispenser re-init is slow / fails.
|
||||||
bays = cassettes
|
bays = cassettes
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.position - b.position)
|
.sort((a, b) => a.position - b.position)
|
||||||
|
|
@ -389,40 +391,31 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
|
||||||
count: c.count ?? 0,
|
count: c.count ?? 0,
|
||||||
}))
|
}))
|
||||||
dispenserInitData = { fiatCode: valConfig.fiatCode, cassettes }
|
dispenserInitData = { fiatCode: valConfig.fiatCode, cassettes }
|
||||||
// Close + re-init the dispenser so its internal per-bay state matches the
|
// Close + re-init the dispenser so its internal per-bay state matches
|
||||||
// new layout. close() now resolves only once the port is really closed,
|
// the new layout. Errors here surface to the caller (operator-config
|
||||||
// so the re-open below cannot race it (aiolabs/bitspire#118).
|
// consumer) — the renderer can decide whether to retry.
|
||||||
//
|
|
||||||
// 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 {
|
try {
|
||||||
await dispenser.close()
|
dispenser.close()
|
||||||
await dispenser.init(dispenserInitData)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
bays = previousBays
|
console.warn('[HAL] Dispenser close during setCassettes raised:', err)
|
||||||
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')
|
console.log('[HAL] Dispenser re-initialized with new cassettes')
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
validator?.disable()
|
validator?.disable()
|
||||||
validator?.lightOff()
|
validator?.lightOff()
|
||||||
await dispenser.close()
|
dispenser.close()
|
||||||
if (!validator) return
|
if (validator) {
|
||||||
await new Promise<void>((resolve) => {
|
validator.close((err?: Error) => {
|
||||||
validator?.close((err?: Error) => {
|
|
||||||
if (err) console.error('[HAL] Validator close error:', err)
|
if (err) console.error('[HAL] Validator close error:', err)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,25 +240,9 @@ fsm.on('send', (data: Buffer) => {
|
||||||
serial?.write(data)
|
serial?.write(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
export function close(): void {
|
||||||
* Close the serial port, resolving once the OS handle is really gone.
|
serial?.close()
|
||||||
*
|
|
||||||
* serialport's close() is asynchronous, so returning before its callback fires
|
|
||||||
* let a close-then-reopen caller (setCassettes -> init) race the old handle and
|
|
||||||
* fail to take the exclusive lock. Narrower window than puloon's, which
|
|
||||||
* deferred the close behind a timer, but the same bug. See aiolabs/bitspire#118.
|
|
||||||
*/
|
|
||||||
export async function close(): Promise<void> {
|
|
||||||
const port = serial
|
|
||||||
if (!port) return
|
|
||||||
// Claim the handle up front so a concurrent close() can't double-close it.
|
|
||||||
serial = null
|
serial = null
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
port.close((err?: Error | null) => {
|
|
||||||
if (err) console.warn('F56 | serial close raised:', err.message)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -56,14 +56,14 @@ export class F56Dispenser implements BillDispenser {
|
||||||
const { bills, error } = await f56.billCount(notes)
|
const { bills, error } = await f56.billCount(notes)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
await this.close()
|
this.close()
|
||||||
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
|
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
|
||||||
;(error as Error & { statusCode: number }).statusCode = 570
|
;(error as Error & { statusCode: number }).statusCode = 570
|
||||||
}
|
}
|
||||||
|
|
||||||
return { value: bills, error }
|
return { value: bills, error }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await this.close()
|
this.close()
|
||||||
const error = err as Error
|
const error = err as Error
|
||||||
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
|
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
|
||||||
;(error as Error & { statusCode: number }).statusCode = 570
|
;(error as Error & { statusCode: number }).statusCode = 570
|
||||||
|
|
@ -71,8 +71,8 @@ export class F56Dispenser implements BillDispenser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
close(): void {
|
||||||
await f56.close()
|
f56.close()
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export class PuloonDispenser implements BillDispenser {
|
||||||
const { bills, error } = await this.device.dispense(notes)
|
const { bills, error } = await this.device.dispense(notes)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
await this.close()
|
this.close()
|
||||||
error.name = 'PuloonDispenseError'
|
error.name = 'PuloonDispenseError'
|
||||||
console.log('PULOON | dispense error', error)
|
console.log('PULOON | dispense error', error)
|
||||||
}
|
}
|
||||||
|
|
@ -58,8 +58,8 @@ export class PuloonDispenser implements BillDispenser {
|
||||||
return { value: bills, error }
|
return { value: bills, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
close(): void {
|
||||||
await this.device.close()
|
this.device.close()
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SerialPort } from 'serialport'
|
import { SerialPort } from 'serialport'
|
||||||
|
|
||||||
/** Grace period for an in-flight write to land before the port is closed. */
|
|
||||||
const SERIAL_DRAIN_MS = 100
|
|
||||||
import { billLengths, encodeBillLength } from './bills.js'
|
import { billLengths, encodeBillLength } from './bills.js'
|
||||||
import type { CassetteConfig, DispenseResult } from '../../types.js'
|
import type { CassetteConfig, DispenseResult } from '../../types.js'
|
||||||
|
|
||||||
|
|
@ -241,32 +238,13 @@ export class PuloonRs232 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Close the serial port */
|
/** Close the serial port */
|
||||||
/**
|
close(): void {
|
||||||
* Close the serial port, resolving only once the OS handle is really gone.
|
if (this.serial) {
|
||||||
*
|
setTimeout(() => {
|
||||||
* This used to defer `serial.close()` behind a 100 ms setTimeout and return
|
this.serial?.close()
|
||||||
* immediately, with no way for a caller to know when the port was free. A
|
|
||||||
* caller that closed and reopened — setCassettes -> init — therefore raced a
|
|
||||||
* handle that was still open and got EAGAIN "Cannot lock port" every time,
|
|
||||||
* since the overlap was the full 100 ms. And if the reopen ever won the
|
|
||||||
* race, the pending timer fired afterwards and closed the *new* handle,
|
|
||||||
* leaving a silently dead dispenser. See aiolabs/bitspire#118.
|
|
||||||
*
|
|
||||||
* The drain delay is kept — it lets an in-flight write land before the port
|
|
||||||
* goes away — but it is now awaited rather than fired and forgotten.
|
|
||||||
*/
|
|
||||||
async close(): Promise<void> {
|
|
||||||
const serial = this.serial
|
|
||||||
if (!serial) return
|
|
||||||
// Claim the handle up front so a concurrent close() can't double-close it.
|
|
||||||
this.serial = null
|
this.serial = null
|
||||||
await new Promise((resolve) => setTimeout(resolve, SERIAL_DRAIN_MS))
|
}, 100)
|
||||||
await new Promise<void>((resolve) => {
|
}
|
||||||
serial.close((err?: Error | null) => {
|
|
||||||
if (err) console.warn('PULOON | serial close raised:', err.message)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a command and wait for a single response */
|
/** Send a command and wait for a single response */
|
||||||
|
|
|
||||||
|
|
@ -179,13 +179,8 @@ export interface BillDispenser {
|
||||||
error?: Error
|
error?: Error
|
||||||
}>
|
}>
|
||||||
|
|
||||||
/**
|
/** Close the connection */
|
||||||
* Close the connection.
|
close(): void
|
||||||
*
|
|
||||||
* Resolves only once the underlying port is actually closed, so a caller may
|
|
||||||
* safely reopen it straight after awaiting this (aiolabs/bitspire#118).
|
|
||||||
*/
|
|
||||||
close(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if bills are present at the dispense outlet
|
* Check if bills are present at the dispense outlet
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue