Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: websocket connection #159

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion satellite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@types/koa-static": "^4.0.4",
"@types/node": "^20.17.1",
"@types/semver": "^7.5.8",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"cross-env": "^7.0.3",
Expand Down Expand Up @@ -70,7 +71,8 @@
"node-hid": "^3.1.2",
"semver": "^7.6.3",
"tslib": "^2.8.0",
"usb": "^2.14.0"
"usb": "^2.14.0",
"ws": "^8.18.0"
},
"lint-staged": {
"*.{css,json,md,scss}": [
Expand Down
125 changes: 70 additions & 55 deletions satellite/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { EventEmitter } from 'eventemitter3'
import { Socket } from 'net'
import { ClientCapabilities, CompanionClient, DeviceDrawProps, DeviceRegisterProps } from './device-types/api.js'
import { DEFAULT_PORT } from './lib.js'
import * as semver from 'semver'
import {
CompanionSatelliteTcpClient,

Check failure on line 6 in satellite/src/client.ts

View workflow job for this annotation

GitHub Actions / lint

'CompanionSatelliteTcpClient' is defined but never used. Allowed unused vars must match /^_(.+)/u
CompanionSatelliteWsClient,
ICompanionSatelliteClient,
ICompanionSatelliteClientOptions,
} from './clientImplementations.js'

const PING_UNACKED_LIMIT = 15 // Arbitrary number
const PING_IDLE_TIMEOUT = 1000 // Pings are allowed to be late if another packet has been received recently
Expand Down Expand Up @@ -89,7 +94,7 @@

export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteClientEvents> implements CompanionClient {
private readonly debug: boolean
private socket: Socket | undefined
private socket: ICompanionSatelliteClient | undefined

private receiveBuffer = ''

Expand Down Expand Up @@ -144,69 +149,79 @@
}

private initSocket(): void {
const socket = (this.socket = new Socket())
this.socket.on('error', (e) => {
this.emit('error', e)
})
this.socket.on('close', () => {
if (this.debug) {
this.emit('log', 'Connection closed')
}

this._registeredDevices.clear()
this._pendingDevices.clear()
if (this.socket) {
this.socket.destroy()
this.socket = undefined
}

if (this._connected) {
this.emit('disconnected')
}
this._connected = false
this.receiveBuffer = ''
if (!this._host) {
this.emit('log', `Missing host for connection`)
return
}

if (this._pingInterval) {
clearInterval(this._pingInterval)
this._pingInterval = undefined
}
const socketOptions: ICompanionSatelliteClientOptions = {
onError: (e) => {
this.emit('error', e)
},
onClose: () => {
if (this.debug) {
this.emit('log', 'Connection closed')
}

if (!this._retryConnectTimeout && this.socket === socket) {
this._retryConnectTimeout = setTimeout(() => {
this._retryConnectTimeout = undefined
this.emit('log', 'Trying reconnect')
this.initSocket()
}, RECONNECT_DELAY)
}
})
this._registeredDevices.clear()
this._pendingDevices.clear()

this.socket.on('data', (d) => this._handleReceivedData(d))
if (this._connected) {
this.emit('disconnected')
}
this._connected = false
this.receiveBuffer = ''

this.socket.on('connect', () => {
if (this.debug) {
this.emit('log', 'Connected')
}
if (this._pingInterval) {
clearInterval(this._pingInterval)
this._pingInterval = undefined
}

this._registeredDevices.clear()
this._pendingDevices.clear()
if (!this._retryConnectTimeout && this.socket === socket) {
this._retryConnectTimeout = setTimeout(() => {
this._retryConnectTimeout = undefined
this.emit('log', 'Trying reconnect')
this.initSocket()
}, RECONNECT_DELAY)
}
},
onData: (d) => this._handleReceivedData(d),
onConnect: () => {
if (this.debug) {
this.emit('log', 'Connected')
}

this._connected = true
this._pingUnackedCount = 0
this.receiveBuffer = ''
this._registeredDevices.clear()
this._pendingDevices.clear()

if (!this._pingInterval) {
this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL)
}
this._connected = true
this._pingUnackedCount = 0
this.receiveBuffer = ''

if (!this.socket) {
// should never hit, but just in case
this.disconnect()
return
}
if (!this._pingInterval) {
this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL)
}

// 'connected' gets emitted once we receive 'Begin'
})
if (!this.socket) {
// should never hit, but just in case
this.disconnect()
return
}

if (this._host) {
this.emit('log', `Connecting to ${this._host}:${this._port}`)
this.socket.connect(this._port, this._host)
// 'connected' gets emitted once we receive 'Begin'
},
}

// const socket = new CompanionSatelliteTcpClient(socketOptions, this._host, this._port)
const socket = new CompanionSatelliteWsClient(socketOptions, `ws://${this._host}:${this._port}`)
this.socket = socket

this.emit('log', `Connecting to ${this._host}:${this._port}`)
}

private sendPing(): void {
Expand Down Expand Up @@ -268,9 +283,9 @@
}
}

private _handleReceivedData(data: Buffer): void {
private _handleReceivedData(data: string): void {
this._lastReceivedAt = Date.now()
this.receiveBuffer += data.toString()
this.receiveBuffer += data

let i = -1
let offset = 0
Expand Down
72 changes: 72 additions & 0 deletions satellite/src/clientImplementations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Socket } from 'net'
import { WebSocket } from 'ws'

export interface ICompanionSatelliteClientEvents {
error: [Error]
close: []
data: [Buffer]
connect: []
}

export interface ICompanionSatelliteClientOptions {
onError: (error: Error) => void
onClose: () => void
onData: (data: string) => void
onConnect: () => void
}

export interface ICompanionSatelliteClient {
write(data: string): void
end(): void
destroy(): void
}

export class CompanionSatelliteTcpClient implements ICompanionSatelliteClient {
#socket: Socket

constructor(options: ICompanionSatelliteClientOptions, host: string, port: number) {
this.#socket = new Socket()

this.#socket.on('error', (err) => options.onError(err))
this.#socket.on('close', () => options.onClose())
this.#socket.on('data', (data) => options.onData(data.toString()))
this.#socket.on('connect', () => options.onConnect())

this.#socket.connect(port, host)
}

write(data: string): void {
this.#socket.write(data)
}
end(): void {
this.#socket.end()
}
destroy(): void {
this.#socket.destroy()
}
}

export class CompanionSatelliteWsClient implements ICompanionSatelliteClient {
#socket: WebSocket

constructor(options: ICompanionSatelliteClientOptions, address: string) {
this.#socket = new WebSocket(address, {
timeout: 5000,
})

this.#socket.on('error', (err) => options.onError(err))
this.#socket.on('close', () => options.onClose())
this.#socket.on('message', (data) => options.onData(data.toString()))

Check failure on line 59 in satellite/src/clientImplementations.ts

View workflow job for this annotation

GitHub Actions / lint

'data' may use Object's default stringification format ('[object Object]') when stringified
this.#socket.on('open', () => options.onConnect())
}

write(data: string): void {
this.#socket.send(data)
}
end(): void {
this.#socket.terminate()
}
destroy(): void {
this.#socket.close()
}
}
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2001,6 +2001,15 @@ __metadata:
languageName: node
linkType: hard

"@types/ws@npm:^8.5.12":
version: 8.5.12
resolution: "@types/ws@npm:8.5.12"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9
languageName: node
linkType: hard

"@types/yauzl@npm:^2.9.1":
version: 2.10.0
resolution: "@types/yauzl@npm:2.10.0"
Expand Down Expand Up @@ -6776,6 +6785,7 @@ __metadata:
"@types/koa-static": "npm:^4.0.4"
"@types/node": "npm:^20.17.1"
"@types/semver": "npm:^7.5.8"
"@types/ws": "npm:^8.5.12"
"@typescript-eslint/eslint-plugin": "npm:^7.18.0"
"@typescript-eslint/parser": "npm:^7.18.0"
"@xencelabs-quick-keys/node": "npm:^1.0.0"
Expand Down Expand Up @@ -6805,6 +6815,7 @@ __metadata:
tslib: "npm:^2.8.0"
tsx: "npm:^4.19.1"
usb: "npm:^2.14.0"
ws: "npm:^8.18.0"
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -7789,6 +7800,21 @@ __metadata:
languageName: node
linkType: hard

"ws@npm:^8.18.0":
version: 8.18.0
resolution: "ws@npm:8.18.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06
languageName: node
linkType: hard

"xmlbuilder@npm:>=11.0.1, xmlbuilder@npm:^15.1.1":
version: 15.1.1
resolution: "xmlbuilder@npm:15.1.1"
Expand Down
Loading