Initial Commit

This commit is contained in:
2026-01-15 21:52:12 +01:00
committed by erik
parent 3ed42cdeb6
commit 5a70f775f1
6702 changed files with 1389544 additions and 0 deletions

104
node_modules/loupedeck/connections/serial.js generated vendored Normal file
View File

@@ -0,0 +1,104 @@
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)
}
}

161
node_modules/loupedeck/connections/web-serial.js generated vendored Normal file
View File

@@ -0,0 +1,161 @@
import { Emitter as EventEmitter } from 'strict-event-emitter'
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'
// TransformStream version of ../parser.js
class MagicByteLengthParser {
constructor({ magicByte }) {
this.buffer = Buffer.alloc(0)
this.delimiter = magicByte
}
transform(chunk, controller) {
let data = Buffer.concat([this.buffer, chunk])
let position
while ((position = data.indexOf(this.delimiter)) !== -1) {
// We need to at least be able to read the length byte
if (data.length < position + 2) break
const nextLength = data[position + 1]
// Make sure we have enough bytes to meet this length
const expectedEnd = position + nextLength + 2
if (data.length < expectedEnd) break
controller.enqueue(data.slice(position + 2, expectedEnd))
data = data.slice(expectedEnd)
}
this.buffer = data
}
flush(controller) {
controller.enqueue(this.buffer)
}
}
// Async pipeline transformer that splits a stream by 0x82 magic bytes
async function *read(port, { signal }) {
const transformer = new TransformStream(new MagicByteLengthParser({ magicByte: 0x82 }))
const transformed = port.readable.pipeThrough(transformer, { signal })
const reader = transformed.getReader()
try {
while (true) {
const { value, done } = await reader.read()
if (done) break
yield value
}
} catch (error) {
console.error('error', error)
} finally {
reader.releaseLock()
}
}
export default class LoupedeckWebSerialConnection extends EventEmitter {
constructor({ port } = {}) {
super()
this.port = port
navigator.serial.addEventListener('disconnect', this.onDisconnect.bind(this))
}
// Automatically find Loupedeck Serial device by scanning ports
static async discover() {
// Return authorized ports if available
const ports = await navigator.serial.getPorts()
if (!ports.length) {
// Request a new port
ports.push(await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x2ec2 }, { usbVendorId: 0x1532 }] }))
}
const connections = []
for (const port of ports) {
const info = await port.getInfo()
connections.push({
connectionType: this,
port,
productId: info.usbProductId,
vendorId: info.usbVendorId,
})
}
return connections
}
async close() {
this.aborter.abort('Manual close initiated')
await new Promise(res => this.on('readEnd', res))
this.writer.close()
this.writer.releaseLock()
// Without the line below, the serialport will refuse to close with
// "TypeError: Failed to execute 'close': Cannot cancel a locked stream"
//
// However, the port.readable stream should have been unlocked via reader.releaseLock() (see read() above)
//
// Apparently it unlocks after a one tick propagation (due to pipeThrough()), but there doesn't
// seem to be a promise anywhere to await this happening...
//
// Annoyingly, we have to wait one tick here which is really hacky,
// and we should find a way to do this more robustly
await new Promise(res => setTimeout(res, 10))
await this.port.close()
}
async connect() {
if (!this.isReady()) {
await this.port.open({ baudRate: 256000 })
}
const reader = this.port.readable.getReader()
this.writer = this.port.writable.getWriter()
// Wait for the "websocket" handshake over serial (...)
const nextMessage = reader.read()
this.send(Buffer.from(WS_UPGRADE_HEADER), true)
const { value: response } = await nextMessage
const stringResponse = new TextDecoder().decode(response)
if (!stringResponse.startsWith(WS_UPGRADE_RESPONSE)) throw new Error(`Invalid handshake response: ${stringResponse}`)
reader.releaseLock()
// Set up data pipeline
this.aborter = new AbortController()
this.readStream = read(this.port, this.aborter)
this.emit('connect', { address: 'web' })
this.read()
}
isReady() {
return this.port && this.port.readable
}
onDisconnect(err) {
// TODO
this.emit('disconnect', err)
}
onError(err) {
// TODO
console.error(`Loupedeck Serial Error: ${err.message}`)
this.onDisconnect(err)
}
async read() {
for await (const message of this.readStream) {
if (!this.port.readable) break
this.emit('message', message)
}
this.emit('readEnd')
}
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.writer.write(prep)
}
return this.writer.write(buff)
}
}

80
node_modules/loupedeck/connections/ws.js generated vendored Normal file
View File

@@ -0,0 +1,80 @@
import { Emitter as EventEmitter } from 'strict-event-emitter'
import { networkInterfaces } from 'node:os'
import WebSocket from 'ws'
import { CONNECTION_TIMEOUT } from '../constants.js'
const DISCONNECT_CODES = {
NORMAL: 1000,
TIMEOUT: 1006,
}
export default class LoupedeckWSConnection extends EventEmitter {
constructor({ host } = {}) {
super()
this.host = host
// Track last interaction time
this.lastTick = Date.now()
// How long until declaring a timed out connetion
this.connectionTimeout = CONNECTION_TIMEOUT
}
// Automatically find Loupedeck IP by scanning network interfaces
static discover() {
const results = []
const interfaces = Object.values(networkInterfaces()).flat()
for (const iface of interfaces) {
if (iface.address.startsWith('100.127')) {
results.push({
connectionType: this,
productId: 0x04,
host: iface.address.replace(/.2$/, '.1')
})
}
}
return results
}
checkConnected() {
this._keepAliveTimer = setTimeout(this.checkConnected.bind(this), this.connectionTimeout * 2)
if (Date.now() - this.lastTick > this.connectionTimeout) this.connection.terminate()
}
close() {
if (!this.connection) return
const closed = new Promise(res => this.connection.once('close', res))
this.connection.close()
return closed
}
connect() {
this.address = `ws://${this.host}`
this.connection = new WebSocket(this.address)
this.connection.on('open', this.onConnect.bind(this))
this.connection.on('message', this.onReceive.bind(this))
this.connection.on('close', this.onDisconnect.bind(this))
return new Promise(res => {
this._connectionResolver = res
})
}
isReady() {
return this.connection !== undefined && this.connection.readyState === this.connection.OPEN
}
onConnect() {
this.emit('connect', { address: this.address })
this._keepAliveTimer = setTimeout(this.checkConnected.bind(this), this.connectionTimeout * 2)
this._connectionResolver()
}
onDisconnect(errCode) {
let error = null
switch (errCode) {
case DISCONNECT_CODES.TIMEOUT:
error = new Error('Connection timeout - was the device disconnected?')
}
clearTimeout(this._keepAliveTimer)
this.emit('disconnect', error)
}
onReceive(buff) {
this.lastTick = Date.now()
this.emit('message', buff)
}
send(buff) {
this.connection.send(buff)
}
}