105 lines
3.3 KiB
JavaScript
105 lines
3.3 KiB
JavaScript
import { Emitter as EventEmitter } from 'strict-event-emitter'
|
|
import { SerialPort } from 'serialport'
|
|
|
|
import { MagicByteLengthParser } from '../parser.js'
|
|
|
|
const WS_UPGRADE_HEADER = `GET /index.html
|
|
HTTP/1.1
|
|
Connection: Upgrade
|
|
Upgrade: websocket
|
|
Sec-WebSocket-Key: 123abc
|
|
|
|
`
|
|
const WS_UPGRADE_RESPONSE = 'HTTP/1.1'
|
|
const WS_CLOSE_FRAME = [0x88, 0x80, 0x00, 0x00, 0x00, 0x00]
|
|
|
|
const VENDOR_IDS = [
|
|
0x2ec2, // Loupedeck
|
|
0x1532, // Razer
|
|
]
|
|
const MANUFACTURERS = [
|
|
'Loupedeck',
|
|
'Razer'
|
|
]
|
|
|
|
export default class LoupedeckSerialConnection extends EventEmitter {
|
|
constructor({ path } = {}) {
|
|
super()
|
|
this.path = path
|
|
}
|
|
// Automatically find Loupedeck Serial device by scanning ports
|
|
static async discover() {
|
|
const results = []
|
|
for (const info of await SerialPort.list()) {
|
|
const { manufacturer, path, serialNumber } = info
|
|
const vendorId = parseInt(info.vendorId, 16)
|
|
const productId = parseInt(info.productId, 16)
|
|
if (!VENDOR_IDS.includes(vendorId) && !MANUFACTURERS.includes(manufacturer)) continue
|
|
results.push({
|
|
connectionType: this,
|
|
path,
|
|
vendorId,
|
|
productId,
|
|
serialNumber
|
|
})
|
|
}
|
|
return results
|
|
}
|
|
close() {
|
|
if (!this.connection) return
|
|
this.send(Buffer.from(WS_CLOSE_FRAME), true)
|
|
return new Promise(res => this.connection.close(res))
|
|
}
|
|
async connect() {
|
|
this.connection = new SerialPort({ path: this.path, baudRate: 256000 })
|
|
this.connection.on('error', this.onError.bind(this))
|
|
this.connection.on('close', this.onDisconnect.bind(this))
|
|
await new Promise(res => this.connection.once('open', res))
|
|
// Wait for the "websocket" handshake over serial (...)
|
|
await new Promise((res, rej) => {
|
|
this.connection.once('data', buff => {
|
|
if (buff.toString().startsWith(WS_UPGRADE_RESPONSE)) res()
|
|
else rej('Invalid handshake response: ', buff)
|
|
})
|
|
this.send(Buffer.from(WS_UPGRADE_HEADER), true)
|
|
})
|
|
|
|
// Set up data pipeline
|
|
const parser = new MagicByteLengthParser({ magicByte: 0x82 })
|
|
this.connection.pipe(parser)
|
|
parser.on('data', this.emit.bind(this, 'message'))
|
|
|
|
this.emit('connect', { address: this.path })
|
|
}
|
|
isReady() {
|
|
return this.connection !== undefined && this.connection.isOpen
|
|
}
|
|
onDisconnect(err) {
|
|
this.emit('disconnect', err)
|
|
}
|
|
onError(err) {
|
|
console.error(`Loupedeck Serial Error: ${err.message}`)
|
|
this.onDisconnect(err)
|
|
}
|
|
send(buff, raw = false) {
|
|
if (!raw) {
|
|
let prep
|
|
// Large messages
|
|
if (buff.length > 0xff) {
|
|
prep = Buffer.alloc(14)
|
|
prep[0] = 0x82
|
|
prep[1] = 0xff
|
|
prep.writeUInt32BE(buff.length, 6)
|
|
// Small messages
|
|
} else {
|
|
// Prepend each message with a send indicating the length to come
|
|
prep = Buffer.alloc(6)
|
|
prep[0] = 0x82
|
|
prep[1] = 0x80 + buff.length
|
|
}
|
|
this.connection.write(prep)
|
|
}
|
|
this.connection.write(buff)
|
|
}
|
|
}
|