feat: add @lamassu/hal package with id003 and f56 drivers

Extract bill validator and dispenser drivers from lamassu-machine:
- ID003 driver for JCM iVIZION bill validators (Sintra)
- F56 driver for Fujitsu F53/F56 bill dispensers (Sintra)

Converted to TypeScript with full type definitions. Uses existing
battle-tested protocol implementations from lamassu-machine.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-26 23:08:47 -05:00
commit 8f24e10299
16 changed files with 2741 additions and 0 deletions

View file

@ -0,0 +1,57 @@
{
"name": "@lamassu/hal",
"version": "0.1.0",
"description": "Hardware Abstraction Layer for Lamassu ATM devices",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./validators": {
"import": "./dist/validators/index.js",
"types": "./dist/validators/index.d.ts"
},
"./dispensers": {
"import": "./dist/dispensers/index.js",
"types": "./dist/dispensers/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"clean": "rm -rf dist"
},
"dependencies": {
"lodash-es": "^4.17.21",
"serialport": "^12.0.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.0",
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^2.0.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"files": [
"dist",
"src"
],
"keywords": [
"lamassu",
"atm",
"hardware",
"bill-validator",
"bill-dispenser",
"id003",
"f56",
"fujitsu"
],
"license": "MIT"
}

View file

@ -0,0 +1,116 @@
/**
* Bill dimension data for F56 dispenser
*
* Each currency has:
* - thickness: Paper thickness setting
* - lengths: Map of denomination -> [length1, length2] bytes
* - polymer: Whether bills are polymer (affects ODR setting)
*/
export interface BillConfig {
thickness: number
lengths: Record<number, [number, number]>
polymer: boolean
}
export const bills: Record<string, BillConfig> = {
AED: {
thickness: 0x0c,
lengths: {
5: [0x99, 0x85],
10: [0x9d, 0x89],
20: [0x9f, 0x8b],
50: [0xa1, 0x8d],
100: [0xa5, 0x91],
200: [0xa7, 0x93],
500: [0xa9, 0x95],
1000: [0xad, 0x99],
},
polymer: false,
},
AUD: {
thickness: 0x0d,
lengths: {
5: [0x8c, 0x78],
10: [0x93, 0x7f],
20: [0x9a, 0x86],
50: [0xa1, 0x8d],
100: [0xa8, 0x94],
},
polymer: true,
},
CAD: {
thickness: 0x0d,
lengths: {
5: [0xa2, 0x8e],
10: [0xa2, 0x8e],
20: [0xa2, 0x8e],
50: [0xa2, 0x8e],
100: [0xa2, 0x8e],
},
polymer: true,
},
CHF: {
thickness: 0x0d,
lengths: {
10: [0x85, 0x71],
20: [0x8c, 0x78],
50: [0x93, 0x7f],
100: [0x9a, 0x86],
200: [0xa1, 0x8d],
1000: [0xa8, 0x94],
},
polymer: false,
},
EUR: {
thickness: 0x0c,
lengths: {
5: [0x82, 0x6e],
10: [0x89, 0x75],
20: [0x8f, 0x7b],
50: [0x96, 0x82],
100: [0x9d, 0x89],
200: [0xa3, 0x8f],
500: [0xaa, 0x96],
},
polymer: false,
},
GBP: {
thickness: 0x0d,
lengths: {
5: [0x91, 0x7d],
10: [0x98, 0x84],
20: [0x95, 0x81],
50: [0xa6, 0x92],
},
polymer: true,
},
MXN: {
thickness: 0x0c,
lengths: {
20: [0x7d, 0x73],
50: [0x82, 0x78],
100: [0x89, 0x7f],
200: [0x90, 0x86],
500: [0x97, 0x8d],
1000: [0x9e, 0x94],
},
polymer: true,
},
USD: {
thickness: 0x0d,
lengths: {
1: [0xa6, 0x92],
2: [0xa6, 0x92],
5: [0xa6, 0x92],
10: [0xa6, 0x92],
20: [0xa6, 0x92],
50: [0xa6, 0x92],
100: [0xa6, 0x92],
},
polymer: false,
},
// Add more currencies as needed...
}
export default bills

View file

@ -0,0 +1,78 @@
/**
* F56 Data-Level State Machine
*
* Higher-level FSM that manages request/response flow for F56 commands.
*/
import { EventEmitter } from 'node:events'
const RESPONSE_TIMEOUT = 40000
class F56DLevelFsm extends EventEmitter {
private _state: string = 'Idle'
private timerId: ReturnType<typeof setTimeout> | null = null
get state(): string {
return this._state
}
private transition(to: string): void {
this.clearTimer()
this._state = to
this.onEnter(to)
}
private onEnter(state: string): void {
if (state === 'WaitForStatus' || state === 'WaitForResponse') {
this.startTimer()
}
}
private startTimer(): void {
this.clearTimer()
this.timerId = setTimeout(() => this.handle('timeout'), RESPONSE_TIMEOUT)
}
private clearTimer(): void {
if (this.timerId) {
clearTimeout(this.timerId)
this.timerId = null
}
}
private conclude(status: string, data?: Buffer): void {
this.emit('status', status, data)
this.transition('Idle')
}
handle(event: string, data?: Buffer): void {
switch (this._state) {
case 'Idle':
if (event === 'waitForResponse') {
this.transition('WaitForStatus')
}
break
case 'WaitForStatus':
if (event === 'timeout') {
this.conclude('Transmission Timeout')
} else if (event === 'transmissionError') {
this.conclude('TransmissionError')
} else if (event === 'transmissionComplete') {
this.transition('WaitForResponse')
}
break
case 'WaitForResponse':
if (event === 'timeout') {
this.conclude('Response Timeout')
} else if (event === 'frame') {
this.conclude('Response', data)
}
break
}
}
}
export const dLevelFsm = new F56DLevelFsm()
export default dLevelFsm

View file

@ -0,0 +1,349 @@
/**
* F56 Protocol State Machine
*
* Handles the low-level DLE/STX/ETX framing for Fujitsu F53/F56 dispensers.
*/
import { EventEmitter } from 'node:events'
import { compute as computeCrc } from '../../utils/crc.js'
const STX = 0x02
const ETX = 0x03
const ENQ = 0x05
const ACK = 0x06
const NAK = 0x15
const DLE = 0x10
const DLE_STX = Buffer.from([DLE, STX])
const DLE_ACK = Buffer.from([DLE, ACK])
const DLE_NAK = Buffer.from([DLE, NAK])
const DLE_ETX = Buffer.from([DLE, ETX])
const DLE_ENQ = Buffer.from([DLE, ENQ])
const DATA_STATES = ['DataLength', 'DataBody', 'CRC']
const CONTROL_MAP: Record<number, string> = {
0x02: 'STX',
0x03: 'ETX',
0x05: 'ENQ',
0x06: 'ACK',
0x15: 'NAK',
0x10: 'DLE',
}
type StateHandler = string | ((byte?: number) => void)
class F56Fsm extends EventEmitter {
private _state: string = 'Idle'
private timerId: ReturnType<typeof setTimeout> | null = null
private retryDleAckCount: number = 0
private retryAckCount: number = 0
private transmitData: Buffer | null = null
private dataLengthBuf: Buffer = Buffer.alloc(2)
private dataLengthPointer: number = 0
private data: Buffer = Buffer.alloc(0)
private dataPointer: number = 0
private crc: Buffer = Buffer.alloc(2)
private crcPointer: number = 0
get state(): string {
return this._state
}
private transition(to: string): void {
this._state = to
this.onEnter(to)
}
private onEnter(state: string): void {
switch (state) {
case 'Idle':
this.retryDleAckCount = 0
this.retryAckCount = 0
this.transmitData = null
this.dataLengthBuf = Buffer.alloc(2)
break
case 'ENQ':
case 'DLE_STX':
case 'STX':
case 'DataLength':
case 'DataBody':
case 'DLE_ETX':
case 'ETX':
case 'CRC':
case 'DLE_ACK':
case 'ACK':
case 'DLE_ACK_2':
case 'ACK_2':
this.startTimer()
break
case 'DLE_ENQ_T':
this.emit('send', DLE_ENQ)
this.transition('DLE_ACK')
break
case 'Transmit':
this.resetRetry()
this.retryAckCount = 0
if (this.transmitData) {
this.emit('send', this.transmitData)
}
this.transition('DLE_ACK_2')
break
case 'CRC_Check':
this.checkCrc()
break
}
// Additional state-specific initialization
if (state === 'DLE_STX') {
this.dataLengthPointer = 0
}
if (state === 'ACK' || state === 'ACK_2') {
this.retryDleAckCount = 0
}
}
private onExit(): void {
this.clearTimer()
}
private startTimer(): void {
this.clearTimer()
this.timerId = setTimeout(() => this.handle('Timeout'), 5000)
}
private clearTimer(): void {
if (this.timerId) {
clearTimeout(this.timerId)
this.timerId = null
}
}
private resetRetry(): void {
this.retryDleAckCount = 0
}
private nakStx(): void {
this.emit('send', DLE_NAK)
this.transition('DLE_STX')
}
private nakEnq(): void {
this.emit('NAK')
this.transition('Idle')
}
private retryDleAck(): void {
this.retryDleAckCount++
if (this.retryDleAckCount < 3) {
this.transition('DLE_ENQ_T')
return
}
this.emit('status', 'transmissionFailure')
this.transition('Idle')
}
private retryAck(): void {
this.retryAckCount++
if (this.retryAckCount < 3) {
this.transition('DLE_ENQ_T')
return
}
this.emit('status', 'transmissionFailure')
this.transition('Idle')
}
private retryDleAck2(): void {
this.retryDleAckCount++
if (this.retryDleAckCount < 3) {
this.transition('Transmit')
return
}
this.emit('status', 'transmissionFailure')
this.transition('Idle')
}
private retryAck2(): void {
this.retryAckCount++
if (this.retryAckCount < 3) {
this.transition('Transmit')
return
}
this.emit('status', 'transmissionFailure')
this.transition('Idle')
}
private checkCrc(): void {
const buf = Buffer.concat([this.dataLengthBuf, this.data, DLE_ETX])
const computedCrc = computeCrc(buf)
if (this.crc.readUInt16LE(0) === computedCrc) {
this.emit('send', DLE_ACK)
this.emit('frame', this.data)
this.transition('Idle')
return
}
console.log('DEBUG2: CRC failure')
this.nakStx()
}
handle(event: string, byte?: number): void {
this.onExit()
switch (this._state) {
case 'Idle':
if (event === 'Send' && byte !== undefined) {
// byte is actually the data buffer index, handled in tx()
}
if (event === 'DLE') this.transition('ENQ')
if (event === 'LineError') this.nakEnq()
break
case 'ENQ':
if (event === 'ENQ') {
this.emit('send', DLE_ACK)
this.transition('DLE_STX')
} else if (event === 'Timeout' || event === 'LineError') {
this.nakEnq()
} else {
this.transition('Idle')
}
break
case 'DLE_STX':
if (event === 'DLE') this.transition('STX')
else if (event === 'Timeout') this.transition('Idle')
else if (event === 'LineError') this.nakEnq()
else this.transition('ENQ')
break
case 'STX':
if (event === 'DLE') this.transition('DLE_STX')
else if (event === 'ENQ') {
this.emit('send', DLE_ACK)
this.transition('DLE_STX')
} else if (event === 'STX') this.transition('DataLength')
else this.nakEnq()
break
case 'DataLength':
if (event === 'Timeout' || event === 'LineError') {
this.nakStx()
} else if (event === 'Data' && byte !== undefined) {
this.dataLengthBuf[this.dataLengthPointer++] = byte
if (this.dataLengthPointer === 2) {
const dataLength = this.dataLengthBuf.readUInt16BE(0)
this.data = Buffer.alloc(dataLength)
this.dataPointer = 0
this.crc = Buffer.alloc(2)
this.crcPointer = 0
this.transition('DataBody')
}
}
break
case 'DataBody':
if (event === 'Timeout' || event === 'LineError') {
this.nakStx()
} else if (event === 'Data' && byte !== undefined) {
this.data[this.dataPointer++] = byte
if (this.dataPointer === this.data.length) {
this.transition('DLE_ETX')
}
}
break
case 'DLE_ETX':
if (event === 'DLE') this.transition('ETX')
else this.nakStx()
break
case 'ETX':
if (event === 'ETX') this.transition('CRC')
else this.nakStx()
break
case 'CRC':
if (event === 'Timeout' || event === 'LineError') {
this.nakStx()
} else if (event === 'Data' && byte !== undefined) {
this.crc[this.crcPointer++] = byte
if (this.crcPointer === 2) {
this.transition('CRC_Check')
}
}
break
case 'DLE_ACK':
if (event === 'DLE') this.transition('ACK')
else if (event === 'Timeout' || event === 'LineError') {
this.retryDleAck()
}
break
case 'ACK':
if (event === 'ENQ') this.transition('DLE_ENQ_T')
else if (event === 'ACK') this.transition('Transmit')
else if (event === 'Timeout' || event === 'LineError') {
this.retryAck()
} else {
this.transition('DLE_ACK')
}
break
case 'DLE_ACK_2':
if (event === 'DLE') this.transition('ACK_2')
else if (event === 'Timeout' || event === 'LineError') {
this.retryDleAck2()
}
break
case 'ACK_2':
if (event === 'ENQ') this.transition('Idle')
else if (event === 'ACK') {
this.emit('status', 'transmissionComplete')
this.transition('Idle')
} else if (event === 'NAK' || event === 'Timeout' || event === 'LineError') {
this.retryAck2()
} else {
this.transition('DLE_ACK_2')
}
break
}
}
rx(byte: number): void {
if (DATA_STATES.includes(this._state)) {
this.handle('Data', byte)
return
}
const event = CONTROL_MAP[byte]
if (event) {
this.handle(event)
return
}
console.error('Unknown code: 0x%s', Buffer.from([byte]).toString('hex'))
}
tx(packet: Buffer): void {
this.transmitData = this.buildFrame(packet)
this.transition('DLE_ENQ_T')
}
private buildFrame(data: Buffer): Buffer {
const buf = Buffer.alloc(8 + data.length)
buf.writeUInt16BE(data.length, 2)
DLE_STX.copy(buf)
data.copy(buf, 4)
DLE_ETX.copy(buf, data.length + 4)
const crcInt = computeCrc(buf.subarray(2, data.length + 6))
buf.writeUInt16LE(crcInt, data.length + 6)
return buf
}
}
export const fsm = new F56Fsm()
export default fsm

View file

@ -0,0 +1,254 @@
/**
* F56 RS232 Communication Layer
*
* Handles serial communication for Fujitsu F53/F56 dispensers.
* Protocol: 9600 baud, 8 data bits, even parity, 1 stop bit
*/
import { EventEmitter } from 'node:events'
import { SerialPort } from 'serialport'
import { fsm } from './f56-fsm.js'
import { dLevelFsm } from './f56-dlevel-fsm.js'
import { bills, type BillConfig } from './bills.js'
const SERIAL_OPTIONS = {
baudRate: 9600,
parity: 'even' as const,
dataBits: 8 as const,
stopBits: 1 as const,
autoOpen: false,
}
const FS = 0x1c
const MAX_SUPPORTED_CASSETTES = 4
class F56Rs232Emitter extends EventEmitter {}
const emitter = new F56Rs232Emitter()
let serial: SerialPort | null = null
/**
* Convert number to parity-encoded decimal digits
*/
function parity(x: number): number {
let y = x ^ (x >> 1)
y = y ^ (y >> 2)
y = y ^ (y >> 4)
y = y ^ (y >> 8)
y = y ^ (y >> 16)
return x + (y & 1) * 0x80
}
/**
* Encode a number as two parity-encoded decimal digits
*/
function D(n: number): [number, number] {
let str = n.toString(10)
if (str.length === 1) str = '0' + str
return [parity(str.charCodeAt(0)), parity(str.charCodeAt(1))]
}
/**
* Decode two parity-encoded bytes to a number
*/
function DP(buf: Buffer): number {
const byte0 = buf[0] ?? 0
const byte1 = buf[1] ?? 0
const str = String.fromCharCode(byte0 & 0x7f, byte1 & 0x7f)
return parseInt(str, 10)
}
function prettyHex(buf: Buffer): string {
const pairs: string[] = []
for (let i = 0; i < buf.length; i++) {
pairs.push(buf.subarray(i, i + 1).toString('hex'))
}
return pairs.join(' ')
}
function parse(buf: Buffer): void {
for (const byte of buf) {
fsm.rx(byte)
}
}
export async function create(device: string): Promise<void> {
return new Promise((resolve, reject) => {
const opts = { ...SERIAL_OPTIONS, path: device }
serial = new SerialPort(opts)
serial.open((error) => {
if (error) return reject(error)
serial!.on('data', (data: Buffer) => parse(data))
serial!.on('close', () => emitter.emit('disconnected'))
resolve()
})
})
}
export async function initialize(currency: string, denominations: number[]): Promise<void> {
const billData = bills[currency]
if (!billData) {
throw new Error(`Unsupported currency: ${currency}`)
}
// Validate denominations
for (let i = 0; i < denominations.length; i++) {
const denom = denominations[i]
if (denom !== undefined && !billData.lengths[denom]) {
throw new Error(`Unsupported denomination: ${denom} for fiat code: ${currency}`)
}
}
const ODR = billData.polymer ? 0x40 : 0x00
// Build lengths array
const lengths: number[] = []
for (let i = 0; i < MAX_SUPPORTED_CASSETTES; i++) {
const denom = denominations[i]
if (denom !== undefined && billData.lengths[denom]) {
lengths.push(...billData.lengths[denom])
} else {
lengths.push(0x00, 0x00)
}
}
// Build thicknesses array
const thicknesses = Array(MAX_SUPPORTED_CASSETTES).fill(billData.thickness)
const command = Buffer.from([0x60, 0x02, 0x0d, ODR, ...lengths, ...thicknesses, FS])
const res = await request(command)
if (res[0] === 0xf0) {
const errorCode = res.subarray(3, 5)
throw new Error(`F56 error code: ${prettyHex(errorCode)}`)
}
if (res[1] !== 0x02 || res[2] !== 0x34) {
throw new Error('Invalid F56 response header')
}
}
export interface BillCountResult {
bills: Array<{ dispensed: number; rejected: number }>
error?: Error
}
export async function billCount(counts: number[]): Promise<BillCountResult> {
const actualCounts = Array(MAX_SUPPORTED_CASSETTES)
.fill(0)
.map((_, i) => counts[i] ?? 0)
const ODR = 0xe4
const billCounts = [
...D(actualCounts[0]!),
...D(actualCounts[1]!),
...D(actualCounts[2]!),
...D(actualCounts[3]!),
]
const rejects = [...D(4), ...D(4), ...D(4), ...D(4)]
const retries = [3, 3, 3, 3]
const command = Buffer.from([0x60, 0x03, 0x15, ODR, ...billCounts, ...rejects, ...retries, FS])
const res = await request(command)
if (res[1] !== 0x03 || res[2] !== 0x99) {
throw new Error('Invalid F56 response header')
}
const response: BillCountResult = {
bills: [],
}
for (let i = 0; i < counts.length; i++) {
response.bills.push({
dispensed: DP(res.subarray(0x27 + 2 * i, 0x29 + 2 * i)),
rejected: DP(res.subarray(0x2f + 2 * i, 0x31 + 2 * i)),
})
}
if (res[0] === 0xf0) {
console.log('response', res)
const errorCode = res.subarray(3, 5)
response.error = new Error(`Dispensing, code: ${prettyHex(errorCode)}`)
console.error(`found error code: ${prettyHex(errorCode)}`)
}
return response
}
export async function billsPresent(): Promise<boolean> {
const command = Buffer.from([0x00, 0x01, FS])
const res = await request(command)
if (res[0] === 0xf0) {
const errorCode = res.subarray(3, 5)
console.error(`F56 Error with code ${prettyHex(errorCode)}`)
console.error(prettyHex(res))
throw new Error('F56 Error')
}
const sensorRegister = res.subarray(0x0c, 0x12)
const byte2 = sensorRegister[2] ?? 0
return (byte2 & 0x10) > 0
}
async function request(command: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
if (dLevelFsm.state !== 'Idle') {
const error = new Error("Can't send in state: " + dLevelFsm.state)
;(error as Error & { code: string }).code = 'DLEVEL_FSM_ERROR'
return reject(error)
}
const rs232StatusHandler = (status: string) => dLevelFsm.handle(status)
const rs232FrameHandler = (frame: Buffer) => dLevelFsm.handle('frame', frame)
fsm.on('status', rs232StatusHandler)
fsm.on('frame', rs232FrameHandler)
const statusHandler = (status: string, frame?: Buffer) => {
fsm.off('status', rs232StatusHandler)
fsm.off('frame', rs232FrameHandler)
dLevelFsm.off('status', statusHandler)
if (status === 'Response' && frame) {
return resolve(frame)
}
if (status === 'Response Timeout') {
const error = new Error('Response Timeout')
;(error as Error & { code: string }).code = 'RESPONSE_TIMEOUT'
return reject(error)
}
return reject(new Error(status))
}
dLevelFsm.on('status', statusHandler)
fsm.tx(command)
dLevelFsm.handle('waitForResponse')
fsm.tx(command)
})
}
// Wire up fsm send event to serial write
fsm.on('send', (data: Buffer) => {
serial?.write(data)
})
export function close(): void {
serial?.close()
serial = null
}
export default {
create,
initialize,
billCount,
billsPresent,
close,
}

View file

@ -0,0 +1,106 @@
/**
* F56 Bill Dispenser Driver
*
* Supports Fujitsu F53/F56 bill dispensers.
* Used in: Lamassu Sintra, Sintra Forte, Tejo
*
* Protocol: RS-232, 9600 baud, 8 data bits, even parity, 1 stop bit
*/
import * as f56 from './f56-rs232.js'
import type {
BillDispenser,
DispenserConfig,
DispenserInitData,
DispenseResult,
} from '../../types.js'
export class F56Dispenser implements BillDispenser {
public type: string = 'F56'
public initialized: boolean = false
private initializing: boolean = false
private device: string
private fiatCode: string = ''
public dispenseLimit: number = 20
constructor(config: DispenserConfig) {
this.device = config.device
}
static factory(config: DispenserConfig): F56Dispenser {
return new F56Dispenser(config)
}
async init(data: DispenserInitData): Promise<void> {
if (this.initializing || this.initialized) return
this.initializing = true
this.fiatCode = data.fiatCode
const denominations = data.cassettes.map((c) => c.denomination)
try {
await f56.create(this.device)
await f56.initialize(this.fiatCode, denominations)
this.initialized = true
this.initializing = false
console.log('INFO F56 Connected')
} catch (err) {
this.initializing = false
throw err
}
}
async dispense(notes: number[]): Promise<{ value: DispenseResult[]; error?: Error }> {
try {
const { bills, error } = await f56.billCount(notes)
if (error) {
this.close()
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
;(error as Error & { statusCode: number }).statusCode = 570
}
return { value: bills, error }
} catch (err) {
this.close()
const error = err as Error
;(error as Error & { name: string; statusCode: number }).name = 'F56DispenseError'
;(error as Error & { statusCode: number }).statusCode = 570
return { value: [], error }
}
}
close(): void {
f56.close()
this.initialized = false
}
async billsPresent(): Promise<boolean> {
return f56.billsPresent()
}
async waitForBillsRemoved(): Promise<boolean> {
return new Promise((resolve, reject) => {
let retries = 3
const interval = setInterval(() => {
this.billsPresent()
.then((billsArePresent) => {
if (!billsArePresent) {
clearInterval(interval)
resolve(true)
}
})
.catch((err: Error & { code?: string }) => {
if (err.code === 'DLEVEL_FSM_ERROR') return
if (err.code === 'RESPONSE_TIMEOUT' && retries-- > 0) return
clearInterval(interval)
reject(err)
})
}, 1000)
})
}
}
export default F56Dispenser

View file

@ -0,0 +1,27 @@
/**
* Bill Dispenser Drivers
*
* Factory for creating bill dispenser instances based on device type.
*/
export { F56Dispenser } from './f56/index.js'
export type { DispenserConfig, BillDispenser, DispenserInitData, DispenseResult } from '../types.js'
import { F56Dispenser } from './f56/index.js'
import type { DispenserConfig, BillDispenser } from '../types.js'
export type DispenserType = 'f56'
/**
* Create a bill dispenser instance
* @param type Dispenser type (e.g., 'f56')
* @param config Dispenser configuration
*/
export function createDispenser(type: DispenserType, config: DispenserConfig): BillDispenser {
switch (type) {
case 'f56':
return F56Dispenser.factory(config)
default:
throw new Error(`Unknown dispenser type: ${type}`)
}
}

View file

@ -0,0 +1,56 @@
/**
* @lamassu/hal - Hardware Abstraction Layer
*
* Provides drivers for Lamassu ATM hardware devices:
* - Bill validators (JCM iVIZION via ID003 protocol)
* - Bill dispensers (Fujitsu F53/F56)
*
* @example
* ```typescript
* import { createValidator, createDispenser } from '@lamassu/hal'
*
* // Create a bill validator
* const validator = createValidator('id003', {
* rs232: { device: '/dev/ttyUSB0' },
* fiatCode: 'USD'
* })
*
* // Create a bill dispenser
* const dispenser = createDispenser('f56', {
* device: '/dev/ttyUSB1'
* })
*
* // Initialize dispenser
* await dispenser.init({
* fiatCode: 'USD',
* cassettes: [
* { denomination: 20 },
* { denomination: 100 }
* ]
* })
* ```
*/
// Validators
export { Id003, createValidator, type ValidatorType } from './validators/index.js'
// Dispensers
export { F56Dispenser, createDispenser, type DispenserType } from './dispensers/index.js'
// Types
export type {
BillValidator,
BillDispenser,
ValidatorConfig,
DispenserConfig,
DispenserInitData,
CassetteConfig,
DispenseResult,
BillData,
RejectionData,
ValidatorFactory,
DispenserFactory,
} from './types.js'
// Utilities
export { compute as computeCrc } from './utils/crc.js'

View file

@ -0,0 +1,206 @@
import { EventEmitter } from 'node:events'
/**
* Bill denomination data returned when a bill is read
*/
export interface BillData {
/** Denomination value (e.g., 20 for $20) */
denomination: number | null
/** Raw escrow code from the validator */
code: number
}
/**
* Rejection reason data
*/
export interface RejectionData {
/** Human-readable rejection reason */
reason: string
/** Raw rejection code */
code: number | null
}
/**
* Bill validator configuration
*/
export interface ValidatorConfig {
rs232: {
/** Serial device path (e.g., '/dev/ttyUSB0') or array of paths to try */
device: string | string[]
/** Fiat currency code (e.g., 'USD', 'EUR') */
fiatCode?: string
}
/** Fiat currency code */
fiatCode?: string
}
/**
* Bill validator events
*/
export interface ValidatorEvents {
/** Emitted when validator is enabled and ready */
enabled: (data: unknown) => void
/** Emitted when validator enters standby mode */
standby: (data: unknown) => void
/** Emitted when a bill is being accepted (inserted) */
billsAccepted: () => void
/** Emitted when a bill has been read and is in escrow */
billsRead: (data: BillData) => void
/** Emitted when a bill has been validated and stacked */
billsValid: () => void
/** Emitted when a bill has been rejected */
billsRejected: (data?: RejectionData) => void
/** Emitted when a bill is refused (unsupported denomination) */
billRefused: () => void
/** Emitted when the stacker/cash box is opened */
stackerOpen: () => void
/** Emitted on error */
error: (err: Error) => void
/** Emitted when device is disconnected */
disconnected: () => void
}
/**
* Bill validator interface
* Implementations: id003 (JCM), ccnet, mei
*/
export interface BillValidator extends EventEmitter {
/**
* Initialize and start the validator
* @param cb Callback when initialization is complete
*/
run(cb: (err?: Error) => void): void
/**
* Close the connection and clean up
* @param cb Callback when closed
*/
close(cb: (err?: Error) => void): void
/** Enable bill acceptance */
enable(): void
/** Disable bill acceptance */
disable(): void
/** Stack the bill currently in escrow */
stack(): void
/** Reject/return the bill currently in escrow */
reject(): void
/** Turn on the bill insertion light */
lightOn(): void
/** Turn off the bill insertion light */
lightOff(): void
/** Set the fiat currency code */
setFiatCode(fiatCode: string): void
/**
* Get the lowest bill denomination that is >= fiat amount
* @param fiat Amount to compare
*/
lowestBill(fiat: { lte: (n: number) => boolean }): { toNumber: () => number }
/**
* Get the highest bill denomination that is <= fiat amount
* @param fiat Amount to compare
*/
highestBill(fiat: { gte: (n: number) => boolean }): { toNumber: () => number }
/** Check if denominations have been loaded from the device */
hasDenominations(): boolean
}
/**
* Bill dispenser configuration
*/
export interface DispenserConfig {
/** Serial device path */
device: string
}
/**
* Cassette configuration
*/
export interface CassetteConfig {
/** Bill denomination in this cassette */
denomination: number
/** Number of bills loaded (optional) */
count?: number
}
/**
* Dispense result for a single cassette
*/
export interface DispenseResult {
/** Number of bills dispensed */
dispensed: number
/** Number of bills rejected during dispense */
rejected: number
}
/**
* Bill dispenser initialization data
*/
export interface DispenserInitData {
/** Fiat currency code (e.g., 'USD') */
fiatCode: string
/** Cassette configuration */
cassettes: CassetteConfig[]
}
/**
* Bill dispenser interface
* Implementations: f56 (Fujitsu), puloon
*/
export interface BillDispenser {
/** Dispenser type identifier */
type: string
/** Whether the dispenser is initialized */
initialized: boolean
/**
* Initialize the dispenser
* @param data Initialization data including fiat code and cassettes
*/
init(data: DispenserInitData): Promise<void>
/**
* Dispense bills
* @param notes Array of bill counts per cassette [cassette1Count, cassette2Count, ...]
* @returns Dispense results per cassette and any error
*/
dispense(notes: number[]): Promise<{
value: DispenseResult[]
error?: Error
}>
/** Close the connection */
close(): void
/**
* Check if bills are present at the dispense outlet
* @returns true if bills are waiting to be taken
*/
billsPresent(): Promise<boolean>
/**
* Wait for customer to remove dispensed bills
* @returns Resolves when bills are removed
*/
waitForBillsRemoved(): Promise<boolean>
}
/**
* Factory function signature for validators
*/
export type ValidatorFactory = (config: ValidatorConfig) => BillValidator
/**
* Factory function signature for dispensers
*/
export type DispenserFactory = (config: DispenserConfig) => BillDispenser

View file

@ -0,0 +1,49 @@
/**
* CCITT-KERMIT CRC computation
* Used by ID003 and F56 protocols
*
* Based on: http://stackoverflow.com/questions/5059268/c-sharp-crc-implementation
*/
const TABLE: readonly number[] = [
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3,
0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399,
0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50,
0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5,
0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693,
0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a,
0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710,
0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df,
0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595,
0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
] as const
/**
* Compute CRC-16 CCITT-KERMIT checksum
* @param buf Array of bytes or Buffer
* @returns 16-bit CRC value
*/
export function compute(buf: ArrayLike<number>): number {
let crc = 0x00
const len = buf.length
for (let i = 0; i < len; i++) {
const byte = buf[i]
if (byte === undefined) continue
crc = (crc >> 8) ^ (TABLE[(crc ^ byte) & 0xff] ?? 0)
}
return crc
}

View file

@ -0,0 +1,281 @@
/**
* ID003 Protocol State Machine
*
* Manages the state transitions for the JCM ID003 bill validator protocol.
* Based on the original lamassu-machine implementation.
*/
import { EventEmitter } from 'node:events'
// Using a simple state machine implementation instead of the contrib library
type StateHandler = (data?: unknown) => void
type TransitionMap = Record<string, string | StateHandler>
const TRANSIENT_TIMEOUT = 60000
interface Id003FsmEvents {
dispatch: (cmd: string, data?: unknown) => void
denominations: () => void
getEnabled: (data: unknown) => void
setEnabled: (data: unknown) => void
ready: () => void
stale: () => void
stuck: () => void
billsAccepted: () => void
billsRead: (data: unknown) => void
billsValid: () => void
billsRejected: (data?: unknown) => void
billRefused: () => void
standby: () => void
stackerOpen: () => void
failure: (args: unknown) => void
powerUp: () => void
error: (err: Error) => void
}
export class Id003Fsm extends EventEmitter {
private _state: string = 'Start'
public disableFlag: boolean = false
private stateTimeout: ReturnType<typeof setTimeout> | null = null
constructor() {
super()
}
static factory(): Id003Fsm {
return new Id003Fsm()
}
get state(): string {
return this._state
}
is(state: string): boolean {
return this._state === state
}
private transition(to: string, event?: string, data?: unknown): void {
const from = this._state
this.clearStateTimeout()
console.log('FSM: %s [ %s -> %s ]', event ?? 'transition', from, to)
this._state = to
this.onEnterState(to, from, data)
}
private clearStateTimeout(): void {
if (this.stateTimeout) {
clearTimeout(this.stateTimeout)
this.stateTimeout = null
}
}
private startTransientTimeout(): void {
this.stateTimeout = setTimeout(() => {
this.emit('stuck')
}, TRANSIENT_TIMEOUT)
}
private onEnterState(state: string, from: string, data?: unknown): void {
switch (state) {
case 'PowerUp':
this.emit('powerUp')
break
case 'Connected':
// Will emit 'ready' on leave
break
case 'Denominations':
this.emit('denominations')
break
case 'GetEnabled':
this.emit('getEnabled', data)
break
case 'SetEnabled':
this.emit('setEnabled', data)
break
case 'Enable':
if (this.disableFlag) {
console.trace('FSM: delayed disable')
this.disableFlag = false
this._dispatch('inhibit')
}
break
case 'Disable':
this.disableFlag = false
if (from === 'Initialize') {
this.emit('standby')
}
break
case 'Initialize':
// Enable interrupt mode after 500ms
setTimeout(() => {
this._dispatch('interruptMode')
}, 500)
break
case 'Accepting':
this.startTransientTimeout()
this.emit('billsAccepted')
break
case 'Rejecting':
this.startTransientTimeout()
console.log('Rejected bill: %s', (data as { reason?: string })?.reason)
this.emit('billsRejected', data)
break
case 'Returning':
this.startTransientTimeout()
this.emit('billsRejected', { reason: 'Returned', code: null })
break
case 'Escrow':
this.emit('billsRead', data)
break
case 'VendValid':
if (from === 'Connected') {
this._dispatch('reset')
return
}
this._dispatch('ack')
this.emit('billsValid')
break
case 'StackerOpen':
this.emit('stackerOpen')
break
case 'Failure':
this.emit('failure', data)
break
case 'Stacking':
case 'Stacked':
this.startTransientTimeout()
break
}
}
private _dispatch(cmd: string): void {
this.emit('dispatch', cmd)
}
// Event handlers called by the RS232 layer
connect(): void {
if (this._state === 'Start') {
this.transition('Connected', 'connect')
} else if (this._state === 'Refresh') {
this.transition('Disable', 'connect')
}
}
badFrame(): void {
this.transition('BadFrame', 'badFrame')
}
powerUp(): void {
this.transition('PowerUp', 'powerUp')
}
powerUpAcceptor(): void {
this.transition('PowerUp', 'powerUpAcceptor')
}
denominations(): void {
this.transition('Denominations', 'denominations')
}
getEnabled(data?: unknown): void {
this.transition('GetEnabled', 'getEnabled', data)
}
setEnabled(data?: unknown): void {
this.transition('SetEnabled', 'setEnabled', data)
}
initialize(): void {
this.transition('Initialize', 'initialize')
}
enable(): void {
this.transition('Enable', 'enable')
}
disable(): void {
this.transition('Disable', 'disable')
}
escrow(data?: unknown): void {
const validFrom = ['Paused', 'Enable', 'Accepting', 'Escrow']
if (validFrom.includes(this._state)) {
this.transition('Escrow', 'escrow', data)
}
}
returning(): void {
const validFrom = ['Escrow', 'Returning', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('Returning', 'returning')
}
}
stacking(): void {
const validFrom = ['Escrow', 'Stacking', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('Stacking', 'stacking')
}
}
vendValid(): void {
const validFrom = ['Connected', 'Escrow', 'Stacking', 'VendValid', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('VendValid', 'vendValid')
}
}
stacked(): void {
const validFrom = ['VendValid', 'Stacked', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('Stacked', 'stacked')
}
}
rejecting(data?: unknown): void {
const validFrom = ['Accepting', 'Rejecting', 'Escrow', 'Stacking', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('Rejecting', 'rejecting', data)
}
}
stackerOpen(): void {
this.transition('StackerOpen', 'stackerOpen')
}
stackerFull(): void {
const validFrom = ['StackerFull', 'Stacked', 'VendValid', 'Paused']
if (validFrom.includes(this._state)) {
this.transition('StackerFull', 'stackerFull')
}
}
accepting(): void {
const validFrom = ['Paused', 'Enable', 'Accepting']
if (validFrom.includes(this._state)) {
this.transition('Accepting', 'accepting')
}
}
failure(args?: unknown): void {
this.transition('Failure', 'failure', args)
}
acceptorJam(): void {
this.transition('AcceptorJam', 'acceptorJam')
}
stackerJam(): void {
this.transition('StackerJam', 'stackerJam')
}
cheated(): void {
this.transition('Cheated', 'cheated')
}
pause(): void {
this.transition('Paused', 'pause')
}
}
export default Id003Fsm

View file

@ -0,0 +1,359 @@
/**
* ID003 RS232 Protocol Layer
*
* Handles serial communication for JCM ID003 bill validators.
* Protocol: 9600 baud, 8 data bits, even parity, 1 stop bit
*/
import { EventEmitter } from 'node:events'
import { SerialPort } from 'serialport'
import * as fs from 'node:fs'
import { compute as computeCrc } from '../../utils/crc.js'
const SYNC = 0xfc
// Command codes
const CMD: Record<string, number[]> = {
denominations: [0x8a],
status: [0x11],
stack: [0x41],
ack: [0x50],
inhibit: [0xc3, 0x01],
unInhibit: [0xc3, 0x00],
reset: [0x40],
reject: [0x43],
enableAll: [0xc0, 0x00, 0x00],
getEnabled: [0x80],
interruptMode: [0xc2, 0x01],
}
// Response codes
const RSP: Record<number, string> = {
0x05: 'enq',
0x40: 'powerUp', // Also powerUpAcceptor
0x1b: 'initialize',
0x1a: 'disable',
0x11: 'enable',
0x12: 'accepting',
0x13: 'escrow',
0x14: 'stacking',
0x15: 'vendValid',
0x16: 'stacked',
0x17: 'rejecting',
0x18: 'returning',
0x43: 'stackerFull',
0x44: 'stackerOpen',
0x45: 'acceptorJam',
0x46: 'stackerJam',
0x47: 'pause',
0x48: 'cheated',
0x49: 'failure',
0x4b: 'invalid',
0x50: 'ack',
0x80: 'getEnabled',
0x88: 'version',
0x8a: 'denominations',
0xc0: 'setEnabled',
0xc2: 'commMode',
0xc3: 'inhibit',
}
// Rejection reason codes
const REJECTION_REASONS: Record<number, string> = {
0x71: 'insertion',
0x72: 'mug',
0x73: 'head',
0x74: 'calibration',
0x75: 'conveying',
0x76: 'discrimination',
0x77: 'photoPattern',
0x78: 'photoLevel',
0x79: 'inhibit',
0x7a: 'unknown',
0x7b: 'operation',
0x7c: 'stacker',
0x7d: 'length',
0x7e: 'photoPattern',
0x7f: 'trueBill',
}
// Currency codes (for multi-currency support)
const CODES: Record<string, number> = {
USD: 0x01,
CAD: 0x08,
}
export interface Id003Rs232Config {
device: string | string[]
fiatCode?: string
}
export class Id003Rs232 extends EventEmitter {
private fiatCode: string | undefined
private buf: Buffer = Buffer.alloc(0)
private config: Id003Rs232Config
private serial: SerialPort | null = null
private _denominations: Record<number, number> | null = null
constructor(config: Id003Rs232Config) {
super()
this.fiatCode = config.fiatCode
this.config = config
}
static factory(config: Id003Rs232Config): Id003Rs232 {
return new Id003Rs232(config)
}
private async _open(device: string): Promise<void> {
return new Promise((resolve, reject) => {
const options = {
path: device,
baudRate: 9600,
parity: 'even' as const,
dataBits: 8 as const,
stopBits: 1 as const,
autoOpen: false,
rtscts: false,
}
const serial = new SerialPort(options)
this.serial = serial
serial.on('error', (err) => this.emit('error', err))
serial.on('open', (err?: Error | null) => {
if (err) return reject(err)
serial.on('readable', () => {
const data = serial.read() as Buffer | null
if (data) this._process(data)
})
serial.on('close', () => {
this.emit('disconnected')
})
this.lightOff()
resolve()
})
serial.open()
})
}
async open(cb: (err?: Error) => void): Promise<void> {
const devices = this.config.device
if (!devices) {
this.emit('error', 'No configured devices.')
return
}
if (typeof devices === 'string') {
try {
await this._open(devices)
cb()
} catch (err) {
cb(err as Error)
}
return
}
// Try each device in order
for (const device of devices) {
try {
const stats = fs.statSync(device)
if (!stats.isCharacterDevice()) continue
await this._open(device)
cb()
return
} catch {
continue
}
}
this.emit('error', 'No configured devices available.')
}
send(command: string): void {
const codes = CMD[command]
if (!codes) throw new Error('Invalid command: ' + command)
const length = codes.length + 4
const payload = [SYNC, length, ...codes]
const buf = Buffer.from(payload)
const crc = computeCrc(payload)
const crcBuf = Buffer.alloc(2)
crcBuf.writeUInt16LE(crc, 0)
const outBuf = Buffer.concat([buf, crcBuf], length)
this.serial?.write(outBuf)
}
close(cb: (err?: Error | null) => void): void {
this.serial?.close(cb)
}
lightOn(): void {
this.serial?.set({ rts: true }, (err) => {
if (err) console.log('lightOn failed: %s', err)
})
}
lightOff(): void {
this.serial?.set({ rts: false }, (err) => {
if (err) console.log('lightOff failed: %s', err)
})
}
denominations(): Record<number, number> | null {
return this._denominations
}
private _acquireSync(data: Buffer): Buffer {
for (let i = 0; i < data.length; i++) {
if (data[i] === SYNC) {
return data.subarray(i)
}
}
return Buffer.alloc(0)
}
private _crcVerify(payload: Buffer): void {
const payloadCrc = payload.readUInt16LE(payload.length - 2)
const verify = computeCrc(payload.subarray(0, -2)) === payloadCrc
if (!verify) throw new Error('CRC error')
}
private _parse(packet: Buffer): void {
this._crcVerify(packet)
const data = packet.length === 5 ? null : packet.subarray(3, -2)
const commandCode = packet[2]
if (commandCode !== undefined) {
this._interpret(commandCode, data)
}
}
private _interpret(commandCode: number, rawData: Buffer | null): void {
const command = RSP[commandCode]
if (!command) {
this.emit('unknownCommand', commandCode)
return
}
const data = this._parseData(command, rawData)
this.emit('message', command, data)
}
private _parseData(command: string, rawData: Buffer | null): unknown {
if (!rawData) return null
switch (command) {
case 'escrow':
return this._escrow(rawData)
case 'version':
return this._version(rawData)
case 'rejecting':
return this._rejecting(rawData)
case 'denominations':
return this._setDenominations(rawData)
case 'getEnabled':
case 'setEnabled':
return this._enabled(rawData)
default:
return null
}
}
private _escrow(rawData: Buffer): { denomination: number | null; code: number } {
const fiatCode = rawData[0]
if (fiatCode === undefined) {
return { denomination: null, code: 0 }
}
const denomination = this._denominations?.[fiatCode] ?? null
return { denomination, code: fiatCode }
}
private _rejecting(rawData: Buffer): { reason: string; code: number } {
const code = rawData[0] ?? 0
const reason = REJECTION_REASONS[code] ?? 'unknown'
return { reason, code }
}
private _setDenominations(rawData: Buffer): void {
// Last two bytes are boot version
if (this._denominations) return
const denominations: Record<number, number> = {}
const rawLength = rawData.length
for (let offset = 0; offset < rawLength; offset += 4) {
const escrowCode = rawData[offset]
const countryCode = rawData[offset + 1]
if (escrowCode === undefined || countryCode === undefined) continue
if (
this.fiatCode &&
Object.prototype.hasOwnProperty.call(CODES, this.fiatCode) &&
countryCode !== CODES[this.fiatCode]
) {
console.log('Found a bill not matching the defined fiat code, rejecting...')
this.emit('reject')
}
const denominationInteger = rawData[offset + 2]
if (denominationInteger === undefined || denominationInteger === 0x00) continue
const denominationExponent = rawData[offset + 3] ?? 0
const denomination = denominationInteger * Math.pow(10, denominationExponent)
denominations[escrowCode] = denomination
}
this._denominations = denominations
}
private _enabled(rawData: Buffer): { data1: number; data2: number } {
return { data1: rawData[0] ?? 0, data2: rawData[1] ?? 0 }
}
private _version(rawData: Buffer): { version: Buffer } {
this._crcVerify(rawData)
return { version: rawData.subarray(0, -2) }
}
private _process(data: Buffer): void {
this.buf = Buffer.concat([this.buf, data])
while (this._processPacket()) {
// Continue processing packets
}
}
private _processPacket(): boolean {
if (this.buf.length === 0) return false
this.buf = this._acquireSync(this.buf)
// Wait for size byte
if (this.buf.length < 2) return false
const responseSize = this.buf[1]
if (responseSize === undefined) return false
// Wait for whole packet
if (this.buf.length < responseSize) return false
const packet = this.buf.subarray(0, responseSize)
this.buf = this.buf.subarray(responseSize)
try {
this._parse(packet)
} catch (ex) {
console.dir(ex)
if (ex instanceof Error) {
console.log(ex.stack)
}
this.emit('badFrame')
return false
}
return true
}
}
export default Id003Rs232

View file

@ -0,0 +1,312 @@
/**
* ID003 Bill Validator Driver
*
* Supports JCM bill validators using the ID003 protocol.
* Used by: JCM iVIZION, JCM UBA, JCM iPRO
*
* Protocol: RS-232, 9600 baud, 8 data bits, even parity, 1 stop bit
*/
import { EventEmitter } from 'node:events'
import { throttle } from 'lodash-es'
import { Id003Rs232 } from './id003-rs232.js'
import { Id003Fsm } from './id003-fsm.js'
import type { BillValidator, ValidatorConfig, BillData } from '../../types.js'
const POLLING_INTERVAL = 100
const IGNORE_RESPONSES = ['ack', 'inhibit', 'commMode', 'enq']
/**
* BigNumber-like interface for bill comparisons
*/
interface BNLike {
lte: (n: number) => boolean
gte: (n: number) => boolean
toNumber: () => number
}
/**
* Simple BigNumber wrapper
*/
function BN(n: number): BNLike {
return {
lte: (other: number) => n <= other,
gte: (other: number) => n >= other,
toNumber: () => n,
}
}
export class Id003 extends EventEmitter implements BillValidator {
private initialized: boolean = false
private pollingInterval: ReturnType<typeof setInterval> | null = null
private config: ValidatorConfig
private fiatCode: string | null = null
private _throttledError: (err: Error) => void
private _startupCallback: ((err?: Error) => void) | null = null
private rs232: Id003Rs232 | null = null
private id003Fsm: Id003Fsm | null = null
private disablePolling: boolean = false
constructor(config: ValidatorConfig) {
super()
this.config = config
this._throttledError = throttle((err: Error) => this.emit('error', err), 2000)
}
static factory(config: ValidatorConfig): Id003 {
return new Id003(config)
}
setFiatCode(fiatCode: string): void {
this.fiatCode = fiatCode
}
lightOn(): void {
if (this.rs232) {
console.log('lightOn')
this.rs232.lightOn()
}
}
lightOff(): void {
if (this.rs232) {
console.log('lightOff')
this.rs232.lightOff()
}
}
run(cb: (err?: Error) => void): void {
this._startupCallback = cb
this.id003Fsm = Id003Fsm.factory()
const config = this.config
const rs232Config = {
...config.rs232,
fiatCode: config.fiatCode,
}
this.disablePolling = false
this.rs232 = Id003Rs232.factory(rs232Config)
// RS232 event handlers
this.rs232.on('message', (cmd: string, data: unknown) => {
if (cmd === 'invalid') {
console.log('ERROR: invalid command')
if (this.disablePolling) this._send('status')
return
}
if (!IGNORE_RESPONSES.includes(cmd) && this.id003Fsm) {
// Call the appropriate FSM method
const fsmMethod = (this.id003Fsm as unknown as Record<string, (data?: unknown) => void>)[
cmd
]
if (typeof fsmMethod === 'function') {
fsmMethod.call(this.id003Fsm, data)
}
}
if (cmd === 'commMode') {
this._disablePolling()
}
if (cmd === 'enq') {
this._send('status')
}
})
this.rs232.on('unknownCommand', (code: number) => {
throw new Error('unknown code: ' + code.toString(16))
})
this.rs232.on('error', (err: Error) => {
this._throttledError(err)
})
this.rs232.on('badFrame', () => {
this.id003Fsm?.badFrame()
if (this.disablePolling) this._send('status')
})
// FSM event handlers
this.id003Fsm.on('dispatch', (cmd: string) => {
this._send(cmd)
})
this.id003Fsm.on('denominations', () => {
this._send('reset')
})
this.id003Fsm.on('getEnabled', (data: unknown) => {
this._send('enableAll')
this.emit('enabled', data)
})
this.id003Fsm.on('setEnabled', (data: unknown) => {
if (this._startupCallback) {
this._startupCallback()
this._startupCallback = null
}
this.emit('enabled', data)
this.emit('standby', data)
})
this.id003Fsm.on('ready', () => {
this._send('denominations')
})
this.id003Fsm.on('stale', () => {
this._send('reset')
})
this.id003Fsm.on('stuck', () => {
this.emit('error', new Error('Bill validator stuck'))
})
this.id003Fsm.on('billsAccepted', () => {
this.emit('billsAccepted')
})
this.id003Fsm.on('billsRead', (data: unknown) => {
const billData = data as BillData
if (!billData.denomination) {
console.log(
'bill rejected: unsupported denomination. Code: 0x%s',
billData.code.toString(16)
)
this._send('reject')
return
}
this.emit('billsRead', data)
})
this.id003Fsm.on('billsValid', () => {
this.emit('billsValid')
})
this.id003Fsm.on('billsRejected', (data?: unknown) => {
this.emit('billsRejected', data)
})
this.id003Fsm.on('billRefused', () => {
this.emit('billRefused')
})
this.id003Fsm.on('standby', () => {
this._send('getEnabled')
})
this.id003Fsm.on('stackerOpen', () => {
this.emit('stackerOpen')
})
this.id003Fsm.on('failure', (args: unknown) => {
this.emit('error', args)
})
// Open serial connection
this.rs232.open((err?: Error) => {
if (err) {
if (this._startupCallback) {
this._startupCallback(err)
}
return
}
this._startPolling()
this.id003Fsm?.connect()
})
}
close(cb: (err?: Error) => void): void {
if (this.pollingInterval) {
clearInterval(this.pollingInterval)
}
this.rs232?.close((err) => {
if (err) console.log(err)
cb(err ?? undefined)
})
}
enable(): void {
if (this.id003Fsm) {
this.id003Fsm.disableFlag = false
}
this._send('unInhibit')
}
disable(): void {
// If the run command is not executed id003Fsm will be undefined
if (!this.id003Fsm || this.id003Fsm.is('Disable')) return
if (this.id003Fsm.is('Enable')) {
this._send('inhibit')
} else {
this.id003Fsm.disableFlag = true
}
}
stack(): void {
this._send('stack')
}
reject(): void {
this._send('reject')
}
lowestBill(fiat: BNLike): BNLike {
const bills = Object.values(this._denominations() ?? {})
const filtered = bills.filter((bill) => fiat.lte(bill))
if (filtered.length === 0) return BN(Math.min(...bills))
return BN(Math.min(...filtered))
}
highestBill(fiat: BNLike): BNLike {
const bills = Object.values(this._denominations() ?? {})
const filtered = bills.filter((bill) => fiat.gte(bill))
if (filtered.length === 0) return BN(-Infinity)
return BN(Math.max(...filtered))
}
hasDenominations(): boolean {
return this._denominations() !== null
}
private _denominations(): Record<number, number> | null {
return this.rs232?.denominations() ?? null
}
private _send(command: string): void {
if (this.disablePolling) {
this.rs232?.send(command)
return
}
this._stopPolling()
// Timeout to prevent interleaved commands
setTimeout(() => {
this.rs232?.send(command)
if (!this.disablePolling) {
this._startPolling()
}
}, POLLING_INTERVAL)
}
private _startPolling(): void {
this._stopPolling()
this.pollingInterval = setInterval(() => {
this.rs232?.send('status')
}, POLLING_INTERVAL)
}
private _stopPolling(): void {
if (this.pollingInterval) {
clearInterval(this.pollingInterval)
this.pollingInterval = null
}
}
private _disablePolling(): void {
this.disablePolling = true
this._stopPolling()
}
}
export default Id003

View file

@ -0,0 +1,27 @@
/**
* Bill Validator Drivers
*
* Factory for creating bill validator instances based on device type.
*/
export { Id003 } from './id003/index.js'
export type { ValidatorConfig, BillValidator, BillData } from '../types.js'
import { Id003 } from './id003/index.js'
import type { ValidatorConfig, BillValidator } from '../types.js'
export type ValidatorType = 'id003'
/**
* Create a bill validator instance
* @param type Validator type (e.g., 'id003')
* @param config Validator configuration
*/
export function createValidator(type: ValidatorType, config: ValidatorConfig): BillValidator {
switch (type) {
case 'id003':
return Id003.factory(config)
default:
throw new Error(`Unknown validator type: ${type}`)
}
}

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View file

@ -138,6 +138,28 @@ importers:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2)
packages/hal:
dependencies:
lodash-es:
specifier: ^4.17.21
version: 4.17.23
serialport:
specifier: ^12.0.0
version: 12.0.0
devDependencies:
'@types/lodash-es':
specifier: ^4.17.0
version: 4.17.12
'@types/node':
specifier: ^22.0.0
version: 22.19.7
typescript:
specifier: ^5.7.0
version: 5.9.3
vitest:
specifier: ^2.0.0
version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2)
packages/lightning: packages/lightning:
dependencies: dependencies:
'@lamassu/clink': '@lamassu/clink':
@ -184,6 +206,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.19.7 specifier: ^22.19.7
version: 22.19.7 version: 22.19.7
qrcode:
specifier: ^1.5.4
version: 1.5.4
tsx: tsx:
specifier: ^4.19.0 specifier: ^4.19.0
version: 4.21.0 version: 4.21.0
@ -193,6 +218,9 @@ importers:
vitest: vitest:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2)
ws:
specifier: ^8.19.0
version: 8.19.0
packages/state-machine: packages/state-machine:
dependencies: dependencies:
@ -923,6 +951,70 @@ packages:
'@scure/bip39@1.2.1': '@scure/bip39@1.2.1':
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
'@serialport/binding-mock@10.2.2':
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
engines: {node: '>=12.0.0'}
'@serialport/bindings-cpp@12.0.1':
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
engines: {node: '>=16.0.0'}
'@serialport/bindings-interface@1.2.2':
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
engines: {node: ^12.22 || ^14.13 || >=16}
'@serialport/parser-byte-length@12.0.0':
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-cctalk@12.0.0':
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@11.0.0':
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@12.0.0':
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-inter-byte-timeout@12.0.0':
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-packet-length@12.0.0':
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
engines: {node: '>=8.6.0'}
'@serialport/parser-readline@11.0.0':
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-readline@12.0.0':
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-ready@12.0.0':
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-regex@12.0.0':
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-slip-encoder@12.0.0':
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-spacepacket@12.0.0':
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
engines: {node: '>=12.0.0'}
'@serialport/stream@12.0.0':
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
engines: {node: '>=12.0.0'}
'@shocknet/clink-sdk@1.5.4': '@shocknet/clink-sdk@1.5.4':
resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==} resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==}
@ -1119,6 +1211,12 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.23':
resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
'@types/node@22.19.7': '@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
@ -1238,6 +1336,14 @@ packages:
alien-signals@1.0.13: alien-signals@1.0.13:
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
aria-hidden@1.2.6: aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1256,6 +1362,10 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
chai@5.3.3: chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1267,16 +1377,35 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
de-indent@1.0.2: de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -1286,6 +1415,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
deep-eql@5.0.2: deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1297,6 +1430,12 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -1342,11 +1481,19 @@ packages:
picomatch: picomatch:
optional: true optional: true
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-tsconfig@4.13.0: get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
@ -1361,6 +1508,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true hasBin: true
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
jiti@2.6.1: jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
@ -1435,6 +1586,13 @@ packages:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
loupe@3.2.1: loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@ -1462,6 +1620,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1473,6 +1634,13 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
node-addon-api@7.0.0:
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
node-gyp-build@4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
nostr-tools@2.15.1: nostr-tools@2.15.1:
resolution: {integrity: sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==} resolution: {integrity: sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==}
peerDependencies: peerDependencies:
@ -1495,12 +1663,28 @@ packages:
ohash@2.0.11: ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-scurry@2.0.1: path-scurry@2.0.1:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@ -1528,6 +1712,10 @@ packages:
typescript: typescript:
optional: true optional: true
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss@8.5.6: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -1542,11 +1730,23 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
reka-ui@2.7.0: reka-ui@2.7.0:
resolution: {integrity: sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==} resolution: {integrity: sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==}
peerDependencies: peerDependencies:
vue: '>= 3.2.0' vue: '>= 3.2.0'
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-pkg-maps@1.0.0: resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@ -1560,6 +1760,13 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
serialport@12.0.0:
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
engines: {node: '>=16.0.0'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
siginfo@2.0.0: siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@ -1573,6 +1780,14 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
tailwind-merge@3.4.0: tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@ -1792,14 +2007,44 @@ packages:
typescript: typescript:
optional: true optional: true
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
why-is-node-running@2.3.0: why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xstate@5.25.1: xstate@5.25.1:
resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==} resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
snapshots: snapshots:
'@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.27.1': {}
@ -2221,6 +2466,60 @@ snapshots:
'@noble/hashes': 1.3.2 '@noble/hashes': 1.3.2
'@scure/base': 1.1.1 '@scure/base': 1.1.1
'@serialport/binding-mock@10.2.2':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@serialport/bindings-cpp@12.0.1':
dependencies:
'@serialport/bindings-interface': 1.2.2
'@serialport/parser-readline': 11.0.0
debug: 4.3.4
node-addon-api: 7.0.0
node-gyp-build: 4.6.0
transitivePeerDependencies:
- supports-color
'@serialport/bindings-interface@1.2.2': {}
'@serialport/parser-byte-length@12.0.0': {}
'@serialport/parser-cctalk@12.0.0': {}
'@serialport/parser-delimiter@11.0.0': {}
'@serialport/parser-delimiter@12.0.0': {}
'@serialport/parser-inter-byte-timeout@12.0.0': {}
'@serialport/parser-packet-length@12.0.0': {}
'@serialport/parser-readline@11.0.0':
dependencies:
'@serialport/parser-delimiter': 11.0.0
'@serialport/parser-readline@12.0.0':
dependencies:
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-ready@12.0.0': {}
'@serialport/parser-regex@12.0.0': {}
'@serialport/parser-slip-encoder@12.0.0': {}
'@serialport/parser-spacepacket@12.0.0': {}
'@serialport/stream@12.0.0':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
'@shocknet/clink-sdk@1.5.4': '@shocknet/clink-sdk@1.5.4':
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
@ -2378,6 +2677,12 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.23
'@types/lodash@4.17.23': {}
'@types/node@22.19.7': '@types/node@22.19.7':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@ -2547,6 +2852,12 @@ snapshots:
alien-signals@1.0.13: {} alien-signals@1.0.13: {}
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
aria-hidden@1.2.6: aria-hidden@1.2.6:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -2561,6 +2872,8 @@ snapshots:
cac@6.7.14: {} cac@6.7.14: {}
camelcase@5.3.1: {}
chai@5.3.3: chai@5.3.3:
dependencies: dependencies:
assertion-error: 2.0.1 assertion-error: 2.0.1
@ -2575,22 +2888,44 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
csstype@3.2.3: {} csstype@3.2.3: {}
de-indent@1.0.2: {} de-indent@1.0.2: {}
debug@4.3.4:
dependencies:
ms: 2.1.2
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
deep-eql@5.0.2: {} deep-eql@5.0.2: {}
defu@6.1.4: {} defu@6.1.4: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dijkstrajs@1.0.3: {}
emoji-regex@8.0.0: {}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@ -2696,9 +3031,16 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
get-caller-file@2.0.5: {}
get-tsconfig@4.13.0: get-tsconfig@4.13.0:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
@ -2713,6 +3055,8 @@ snapshots:
he@1.2.0: {} he@1.2.0: {}
is-fullwidth-code-point@3.0.0: {}
jiti@2.6.1: {} jiti@2.6.1: {}
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
@ -2764,6 +3108,12 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
lodash-es@4.17.23: {}
loupe@3.2.1: {} loupe@3.2.1: {}
lru-cache@11.2.4: {} lru-cache@11.2.4: {}
@ -2786,12 +3136,18 @@ snapshots:
minipass@7.1.2: {} minipass@7.1.2: {}
ms@2.1.2: {}
ms@2.1.3: {} ms@2.1.3: {}
muggle-string@0.4.1: {} muggle-string@0.4.1: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
node-addon-api@7.0.0: {}
node-gyp-build@4.6.0: {}
nostr-tools@2.15.1(typescript@5.9.3): nostr-tools@2.15.1(typescript@5.9.3):
dependencies: dependencies:
'@noble/ciphers': 0.5.3 '@noble/ciphers': 0.5.3
@ -2820,10 +3176,22 @@ snapshots:
ohash@2.0.11: {} ohash@2.0.11: {}
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-exists@4.0.0: {}
path-scurry@2.0.1: path-scurry@2.0.1:
dependencies: dependencies:
lru-cache: 11.2.4 lru-cache: 11.2.4
@ -2847,6 +3215,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
pngjs@5.0.0: {}
postcss@8.5.6: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@ -2859,6 +3229,12 @@ snapshots:
dependencies: dependencies:
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
reka-ui@2.7.0(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)): reka-ui@2.7.0(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)):
dependencies: dependencies:
'@floating-ui/dom': 1.7.4 '@floating-ui/dom': 1.7.4
@ -2876,6 +3252,10 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- typescript - typescript
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
rimraf@6.1.2: rimraf@6.1.2:
@ -2914,6 +3294,27 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.56.0 '@rollup/rollup-win32-x64-msvc': 4.56.0
fsevents: 2.3.3 fsevents: 2.3.3
serialport@12.0.0:
dependencies:
'@serialport/binding-mock': 10.2.2
'@serialport/bindings-cpp': 12.0.1
'@serialport/parser-byte-length': 12.0.0
'@serialport/parser-cctalk': 12.0.0
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-inter-byte-timeout': 12.0.0
'@serialport/parser-packet-length': 12.0.0
'@serialport/parser-readline': 12.0.0
'@serialport/parser-ready': 12.0.0
'@serialport/parser-regex': 12.0.0
'@serialport/parser-slip-encoder': 12.0.0
'@serialport/parser-spacepacket': 12.0.0
'@serialport/stream': 12.0.0
debug: 4.3.4
transitivePeerDependencies:
- supports-color
set-blocking@2.0.0: {}
siginfo@2.0.0: {} siginfo@2.0.0: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
@ -2922,6 +3323,16 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
tailwind-merge@3.4.0: {} tailwind-merge@3.4.0: {}
tailwindcss@4.1.18: {} tailwindcss@4.1.18: {}
@ -3090,9 +3501,40 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
which-module@2.0.1: {}
why-is-node-running@2.3.0: why-is-node-running@2.3.0:
dependencies: dependencies:
siginfo: 2.0.0 siginfo: 2.0.0
stackback: 0.0.2 stackback: 0.0.2
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
ws@8.19.0: {}
xstate@5.25.1: {} xstate@5.25.1: {}
y18n@4.0.3: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3