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:
parent
16f5fb3cbc
commit
8f24e10299
16 changed files with 2741 additions and 0 deletions
57
lamassu-next/packages/hal/package.json
Normal file
57
lamassu-next/packages/hal/package.json
Normal 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"
|
||||||
|
}
|
||||||
116
lamassu-next/packages/hal/src/dispensers/f56/bills.ts
Normal file
116
lamassu-next/packages/hal/src/dispensers/f56/bills.ts
Normal 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
|
||||||
|
|
@ -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
|
||||||
349
lamassu-next/packages/hal/src/dispensers/f56/f56-fsm.ts
Normal file
349
lamassu-next/packages/hal/src/dispensers/f56/f56-fsm.ts
Normal 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
|
||||||
254
lamassu-next/packages/hal/src/dispensers/f56/f56-rs232.ts
Normal file
254
lamassu-next/packages/hal/src/dispensers/f56/f56-rs232.ts
Normal 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,
|
||||||
|
}
|
||||||
106
lamassu-next/packages/hal/src/dispensers/f56/index.ts
Normal file
106
lamassu-next/packages/hal/src/dispensers/f56/index.ts
Normal 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
|
||||||
27
lamassu-next/packages/hal/src/dispensers/index.ts
Normal file
27
lamassu-next/packages/hal/src/dispensers/index.ts
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lamassu-next/packages/hal/src/index.ts
Normal file
56
lamassu-next/packages/hal/src/index.ts
Normal 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'
|
||||||
206
lamassu-next/packages/hal/src/types.ts
Normal file
206
lamassu-next/packages/hal/src/types.ts
Normal 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
|
||||||
49
lamassu-next/packages/hal/src/utils/crc.ts
Normal file
49
lamassu-next/packages/hal/src/utils/crc.ts
Normal 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
|
||||||
|
}
|
||||||
281
lamassu-next/packages/hal/src/validators/id003/id003-fsm.ts
Normal file
281
lamassu-next/packages/hal/src/validators/id003/id003-fsm.ts
Normal 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
|
||||||
359
lamassu-next/packages/hal/src/validators/id003/id003-rs232.ts
Normal file
359
lamassu-next/packages/hal/src/validators/id003/id003-rs232.ts
Normal 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
|
||||||
312
lamassu-next/packages/hal/src/validators/id003/index.ts
Normal file
312
lamassu-next/packages/hal/src/validators/id003/index.ts
Normal 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
|
||||||
27
lamassu-next/packages/hal/src/validators/index.ts
Normal file
27
lamassu-next/packages/hal/src/validators/index.ts
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
lamassu-next/packages/hal/tsconfig.json
Normal file
22
lamassu-next/packages/hal/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
442
lamassu-next/pnpm-lock.yaml
generated
442
lamassu-next/pnpm-lock.yaml
generated
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue