Skip to content

Commit

Permalink
feat: WebsocketTcpClient
Browse files Browse the repository at this point in the history
  • Loading branch information
olzzon committed Feb 20, 2025
1 parent 9917b7f commit be244ed
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 0 deletions.
19 changes: 19 additions & 0 deletions packages/timeline-state-resolver-types/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
TriCasterOptions,
MultiOSCOptions,
ViscaOverIPOptions,
// currently hardcoded: WebSocketTcpClientOptions
} from '.'
import { DeviceCommonOptions } from './generated/common-options'

Expand Down Expand Up @@ -78,6 +79,7 @@ export type DeviceOptionsAny =
| DeviceOptionsTriCaster
| DeviceOptionsMultiOSC
| DeviceOptionsViscaOverIP
| DeviceOptionsWebSocketTcpClient

export interface DeviceOptionsAbstract extends DeviceOptionsBase<AbstractOptions> {
type: DeviceType.ABSTRACT
Expand Down Expand Up @@ -148,3 +150,20 @@ export interface DeviceOptionsMultiOSC extends DeviceOptionsBase<MultiOSCOptions
export interface DeviceOptionsViscaOverIP extends DeviceOptionsBase<ViscaOverIPOptions> {
type: DeviceType.VISCA_OVER_IP
}

// Move this to a $schema file:
export interface WebSocketTcpClientOptions {
webSocket: {
uri: string; // e.g., "ws://localhost:8080"
reconnectInterval?: number; // Optional, in ms
};
tcp: {
host: string; // e.g., "127.0.0.1"
port: number; // e.g., 12345
bufferEncoding?: string; // e.g., "utf8"
};
}

export interface DeviceOptionsWebSocketTcpClient extends DeviceOptionsBase<WebSocketTcpClientOptions> {
type: DeviceType.WEBSOCKET_TCP_CLIENT;
}
4 changes: 4 additions & 0 deletions packages/timeline-state-resolver-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { TimelineContentSingularLiveAny } from './integrations/singularLive'
import { TimelineContentVMixAny } from './integrations/vmix'
import { TimelineContentOBSAny } from './integrations/obs'
import { TimelineContentTriCasterAny } from './integrations/tricaster'
import { TimelineContentWebSocketTcpClientAny } from './integrations/websocketTcpClient'

export * from './integrations/abstract'
export * from './integrations/atem'
Expand All @@ -46,6 +47,7 @@ export * from './integrations/tricaster'
export * from './integrations/telemetrics'
export * from './integrations/multiOsc'
export * from './integrations/viscaOverIP'
export * from './integrations/websocketTcpClient'

export * from './device'
export * from './mapping'
Expand Down Expand Up @@ -88,6 +90,7 @@ export enum DeviceType {
TRICASTER = 'TRICASTER',
MULTI_OSC = 'MULTI_OSC',
VISCA_OVER_IP = 'VISCA_OVER_IP',
WEBSOCKET_TCP_CLIENT = 'WEBSOCKET_TCP_CLIENT',
}

export interface TSRTimelineKeyframe<TContent> extends Omit<Timeline.TimelineKeyframe, 'content'> {
Expand Down Expand Up @@ -149,6 +152,7 @@ export type TSRTimelineContent =
| TimelineContentVIZMSEAny
| TimelineContentTelemetricsAny
| TimelineContentTriCasterAny
| TimelineContentWebSocketTcpClientAny

/**
* A simple key value store that can be referred to from the timeline objects
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DeviceType } from 'timeline-state-resolver-types/src'

export enum TimelineContentTypeWebSocketTcpClient {
WEBSOCKET_MESSAGE = 'websocketMessage',
TCP_COMMAND = 'tcpCommand',
}

export interface TimelineContentWebSocketTcpClientBase {
deviceType: DeviceType.WEBSOCKET_TCP_CLIENT
type: TimelineContentTypeWebSocketTcpClient
}

// We might end up using only 1 datatype as it's the same data being sent over different channels:
export interface TimelineContentWebSocketMessage extends TimelineContentWebSocketTcpClientBase {
type: TimelineContentTypeWebSocketTcpClient.WEBSOCKET_MESSAGE
message: string | Uint8Array // Data to send over WebSocket
}

export interface TimelineContentTcpCommand extends TimelineContentWebSocketTcpClientBase {
type: TimelineContentTypeWebSocketTcpClient.TCP_COMMAND
command: string | Uint8Array // Data to send over TCP
}

export type TimelineContentWebSocketTcpClientAny = TimelineContentWebSocketMessage | TimelineContentTcpCommand
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as WebSocket from 'ws'
import { Socket } from 'net'
import { WebSocketTcpClientOptions } from 'timeline-state-resolver-types/src'

export class WebSocketTcpConnection {
private ws: WebSocket | null = null
private tcp: Socket | null = null
private options: WebSocketTcpClientOptions

constructor(options: WebSocketTcpClientOptions) {
this.options = options
}

async connect(): Promise<void> {
// WebSocket connection
this.ws = new WebSocket(this.options.webSocket.uri)
this.ws.on('open', () => console.log('WebSocket connected'))
this.ws.on('error', (err) => console.error('WebSocket error:', err))
this.ws.on('close', () => {
console.log('WebSocket closed')
if (this.options.webSocket.reconnectInterval) {
setTimeout(() => this.connect(), this.options.webSocket.reconnectInterval)
}
})

// TCP connection
this.tcp = new Socket()
this.tcp.connect(this.options.tcp.port, this.options.tcp.host, () => {
console.log('TCP connected')
})
this.tcp.on('error', (err) => console.error('TCP error:', err))
this.tcp.on('close', () => console.log('TCP closed'))
}

connected(): boolean {
return (this.ws?.readyState === WebSocket.OPEN && this.tcp?.writable) || false
}

sendWebSocketMessage(message: string | Uint8Array): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(message)
} else {
console.warn('WebSocket not connected')
}
}

sendTcpCommand(command: string | Uint8Array): void {
if (this.tcp?.writable) {
this.tcp.write(command)
} else {
console.warn('TCP not connected')
}
}

async disconnect(): Promise<void> {
if (this.ws) {
this.ws.close()
this.ws = null
}
if (this.tcp) {
this.tcp.destroy()
this.tcp = null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CommandWithContext, Device, DeviceContextAPI } from '../../service/device'
import { DeviceStatus, WebSocketTcpClientOptions } from 'timeline-state-resolver-types'
import { WebSocketTcpConnection } from './connection'

interface WebSocketTcpCommand extends CommandWithContext{
command: any // need to fix command structure
context: string
timelineObjId: string
value?: any //
}

export class WebSocketTcpClientDevice extends Device<
WebSocketTcpClientOptions,
any, //Add state later
WebSocketTcpCommand
> {
private connection: WebSocketTcpConnection

constructor(context: DeviceContextAPI<any>, _options: WebSocketTcpClientOptions) {
super(context)
this.connection = new WebSocketTcpConnection(_options)
}

public async init(): Promise<boolean> {
await this.connection.connect()
return true
}

public get actions(): any {
// Placeholder implementation
return {}
}

public get connected(): boolean {
return this.connection?.connected() ?? false
}

public convertTimelineStateToDeviceState(
state: any // ToDo
): any {
return state
}

public diffStates(oldState: any, newState: any): WebSocketTcpCommand[] {
// ToDo: Implement state diffing
const commands: WebSocketTcpCommand[] = []
if (oldState !== newState) {
commands.push({
command: 'update',
context: 'state_change',
timelineObjId: 'example_id',
value: newState,
})
}
return commands
}

public getStatus(): Omit<DeviceStatus, "active"> {
return {
statusCode: this.connected ? 0 : 1, // 0 = GOOD, 1 = BAD (based on StatusCode enum)
messages: this.connected ? ['Connected'] : ['Disconnected'],
}
}

public async sendCommand(command: WebSocketTcpCommand): Promise<void> {
// Send the command via the WebSocket connection
await this.connection.sendWebSocketMessage(command.value)
}
// We might end up using just one sendCommand() with a switch-case for the command type:
public async sendTcpCommand(command: WebSocketTcpCommand): Promise<void> {
// Send the command via the TCP connection
await this.connection.sendTcpCommand(command.value)
}

public async terminate(): Promise<void> {
await this.connection.disconnect()
// Perform any cleanup if needed
}
}
6 changes: 6 additions & 0 deletions packages/timeline-state-resolver/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,11 @@ export const manifest: TSRManifest = {
configSchema: JSON.stringify(VMixOptions),
mappingsSchemas: stringifyMappingSchema(VMixMappings),
},
[DeviceType.WEBSOCKET_TCP_CLIENT]: {
displayName: generateTranslation('Websocket+TCP Client'),
// $schema to be added currently hardcoded
configSchema: JSON.stringify({}),
mappingsSchemas: {},
}
},
}

0 comments on commit be244ed

Please sign in to comment.