Skip to content

Commit

Permalink
feat: support websocket connection
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Feb 2, 2025
1 parent 9912e92 commit 739f583
Show file tree
Hide file tree
Showing 24 changed files with 937 additions and 113 deletions.
24 changes: 20 additions & 4 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,19 @@ components:
ConfigData:
type: object
properties:
protocol:
type: string
enum: ['tcp', 'ws']
description: Protocol to use for the connection
host:
type: string
description: Address of the Companion server to connect to
description: Address of the Companion server to connect to, for TCP protocol
port:
type: number
description: Port number of the Companion server to connect to
description: Port number of the Companion server to connect to, for TCP protocol
wsAddress:
type: string
description: Address of the Companion server to connect to, for WS protocol

installationName:
type: string
Expand All @@ -217,8 +224,10 @@ components:
type: number
description: Port number of the HTTP api. This is readonly in the HTTP api
required:
- protocol
- host
- port
- wsAddress
- installationName
- mdnsEnabled
- httpEnabled
Expand All @@ -227,12 +236,19 @@ components:
ConfigDataUpdate:
type: object
properties:
protocol:
type: string
enum: ['tcp', 'ws']
description: Protocol to use for the connection
host:
type: string
description: Address of the Companion server to connect to
description: Address of the Companion server to connect to, for TCP protocol
port:
type: number
description: Port number of the Companion server to connect to
description: Port number of the Companion server to connect to, for TCP protocol
wsAddress:
type: string
description: Address of the Companion server to connect to, for WS protocol

installationName:
type: string
Expand Down
4 changes: 3 additions & 1 deletion satellite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/koa-static": "^4.0.4",
"@types/node": "^20.17.16",
"@types/semver": "^7.5.8",
"@types/ws": "^8.5.12",
"cross-env": "^7.0.3",
"electron": "34.0.2",
"electron-builder": "^26.0.1",
Expand Down Expand Up @@ -58,7 +59,8 @@
"node-hid": "^3.1.2",
"semver": "^7.7.0",
"tslib": "^2.8.1",
"usb": "^2.14.0"
"usb": "^2.14.0",
"ws": "^8.18.0"
},
"lint-staged": {
"*.{css,json,md,scss}": [
Expand Down
4 changes: 4 additions & 0 deletions satellite/src/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export function compileStatus(client: CompanionSatelliteClient): ApiStatusRespon

export function compileConfig(appConfig: Conf<SatelliteConfig>): ApiConfigData {
return {
protocol: appConfig.get('remoteProtocol'),
host: appConfig.get('remoteIp'),
port: appConfig.get('remotePort'),
wsAddress: appConfig.get('remoteWsAddress'),

installationName: appConfig.get('installationName'),

Expand All @@ -48,8 +50,10 @@ export function compileConfig(appConfig: Conf<SatelliteConfig>): ApiConfigData {
}

export function updateConfig(appConfig: SatelliteConfigInstance, newConfig: ApiConfigDataUpdateElectron): void {
if (newConfig.protocol !== undefined) appConfig.set('remoteProtocol', newConfig.protocol)
if (newConfig.host !== undefined) appConfig.set('remoteIp', newConfig.host)
if (newConfig.port !== undefined) appConfig.set('remotePort', newConfig.port)
if (newConfig.wsAddress !== undefined) appConfig.set('remoteWsAddress', newConfig.wsAddress)

if (newConfig.httpEnabled !== undefined) appConfig.set('restEnabled', newConfig.httpEnabled)
if (newConfig.httpPort !== undefined) appConfig.set('restPort', newConfig.httpPort)
Expand Down
180 changes: 110 additions & 70 deletions satellite/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { EventEmitter } from 'events'
import { Socket } from 'net'
import { ClientCapabilities, CompanionClient, DeviceDrawProps, DeviceRegisterProps } from './device-types/api.js'
import { DEFAULT_PORT } from './lib.js'
import { assertNever, DEFAULT_TCP_PORT } from './lib.js'
import * as semver from 'semver'
import {
CompanionSatelliteTcpClient,
CompanionSatelliteWsClient,
formatConnectionUrl,
ICompanionSatelliteClient,
ICompanionSatelliteClientOptions,
SomeConnectionDetails,
} 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 @@ -93,7 +100,7 @@ export type CompanionSatelliteClientEvents = {

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

private receiveBuffer = ''

Expand All @@ -103,8 +110,7 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
private _connected = false
private _connectionActive = false // True when connected/connecting/reconnecting
private _retryConnectTimeout: NodeJS.Timeout | undefined = undefined
private _host = ''
private _port = DEFAULT_PORT
private _connectionDetails: SomeConnectionDetails = { mode: 'tcp', host: '', port: DEFAULT_TCP_PORT }

private _registeredDevices = new Set<string>()
private _pendingDevices = new Map<string, number>() // Time submitted
Expand All @@ -113,11 +119,8 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
private _companionApiVersion: string | null = null
private _companionUnsupported = false

public get host(): string {
return this._host
}
public get port(): number {
return this._port
public get connectionDetails(): SomeConnectionDetails {
return this._connectionDetails
}
public get connected(): boolean {
return this._connected
Expand All @@ -132,6 +135,23 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
return this._companionUnsupported
}

public get displayHost(): string {
switch (this._connectionDetails.mode) {
case 'tcp':
return this._connectionDetails.host
case 'ws':
try {
const url = new URL(this._connectionDetails.url)
return url.hostname
} catch (_e) {
return this._connectionDetails.url
}
default:
assertNever(this._connectionDetails)
return ''
}
}

public get capabilities(): ClientCapabilities {
return {
// For future use
Expand All @@ -147,74 +167,95 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
}

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')
}
if (this.socket) {
this.socket.destroy()
this.socket = undefined
}

this._registeredDevices.clear()
this._pendingDevices.clear()
if (!this._connectionDetails) {
this.emit('log', `Missing connection details`)
return
}

if (this._connected) {
this.emit('disconnected')
} else {
this._companionUnsupported = false
}
this._connected = false
this.receiveBuffer = ''
const socketOptions: ICompanionSatelliteClientOptions = {
onError: (e) => {
this.emit('error', e)
},
onClose: () => {
if (this.debug) {
this.emit('log', 'Connection closed')
}

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()
},
this._companionUnsupported ? RECONNECT_DELAY_UNSUPPORTED : RECONNECT_DELAY,
)
}
})
if (this._connected) {
this.emit('disconnected')
} else {
this._companionUnsupported = false
}
this._connected = false
this.receiveBuffer = ''

this.socket.on('data', (d) => this._handleReceivedData(d))
if (this._pingInterval) {
clearInterval(this._pingInterval)
this._pingInterval = undefined
}

this.socket.on('connect', () => {
if (this.debug) {
this.emit('log', 'Connected')
}
if (!this._retryConnectTimeout && this.socket === socket) {
this._retryConnectTimeout = setTimeout(
() => {
this._retryConnectTimeout = undefined
this.emit('log', 'Trying reconnect')
this.initSocket()
},
this._companionUnsupported ? RECONNECT_DELAY_UNSUPPORTED : RECONNECT_DELAY,
)
}
},
onData: (d) => this._handleReceivedData(d),
onConnect: () => {
if (this.debug) {
this.emit('log', 'Connected')
}

this._registeredDevices.clear()
this._pendingDevices.clear()
this._registeredDevices.clear()
this._pendingDevices.clear()

this._connected = true
this._pingUnackedCount = 0
this.receiveBuffer = ''
this._connected = true
this._pingUnackedCount = 0
this.receiveBuffer = ''

if (!this._pingInterval) {
this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL)
}
if (!this._pingInterval) {
this._pingInterval = setInterval(() => this.sendPing(), PING_INTERVAL)
}

if (!this.socket) {
// should never hit, but just in case
this.disconnect()
return
}
if (!this.socket) {
// should never hit, but just in case
this.disconnect()
return
}

// 'connected' gets emitted once we receive 'Begin'
})
// 'connected' gets emitted once we receive 'Begin'
},
}

if (this._host) {
this.emit('log', `Connecting to ${this._host}:${this._port}`)
this.socket.connect(this._port, this._host)
let socket: ICompanionSatelliteClient
switch (this._connectionDetails.mode) {
case 'tcp':
socket = new CompanionSatelliteTcpClient(socketOptions, this._connectionDetails)
break
case 'ws':
socket = new CompanionSatelliteWsClient(socketOptions, this._connectionDetails)
break
default:
assertNever(this._connectionDetails)
this.socket = undefined
throw new Error('Invalid connection mode')
}
this.socket = socket

this.emit('log', `Connecting to ${formatConnectionUrl(this._connectionDetails)}`)
}

private sendPing(): void {
Expand All @@ -235,7 +276,7 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
}
}

public async connect(host: string, port: number): Promise<void> {
public async connect(details: SomeConnectionDetails): Promise<void> {
if (this._connected || this._connectionActive) {
this.disconnect()
}
Expand All @@ -245,8 +286,7 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
this.emit('connecting')
})

this._host = host
this._port = port
this._connectionDetails = { ...details }

this.initSocket()
}
Expand Down Expand Up @@ -276,9 +316,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
}
}

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
Loading

0 comments on commit 739f583

Please sign in to comment.