diff --git a/packages/timeline-state-resolver/src/conductor.ts b/packages/timeline-state-resolver/src/conductor.ts index fc289b856..c041844a7 100644 --- a/packages/timeline-state-resolver/src/conductor.ts +++ b/packages/timeline-state-resolver/src/conductor.ts @@ -49,7 +49,6 @@ import { SisyfosMessageDevice, DeviceOptionsSisyfosInternal } from './integratio import { SingularLiveDevice, DeviceOptionsSingularLiveInternal } from './integrations/singularLive' import { VMixDevice, DeviceOptionsVMixInternal } from './integrations/vmix' import { VizMSEDevice, DeviceOptionsVizMSEInternal } from './integrations/vizMSE' -import { TelemetricsDevice } from './integrations/telemetrics' import { TriCasterDevice, DeviceOptionsTriCasterInternal } from './integrations/tricaster' import { DeviceOptionsMultiOSCInternal, MultiOSCMessageDevice } from './integrations/multiOsc' import { BaseRemoteDeviceIntegration, RemoteDeviceInstance } from './service/remoteDeviceInstance' @@ -546,15 +545,6 @@ export class Conductor extends EventEmitter { getCurrentTime, threadedClassOptions ) - case DeviceType.TELEMETRICS: - return DeviceContainer.create( - '../../dist/integrations/telemetrics/index.js', - 'TelemetricsDevice', - deviceId, - deviceOptions, - getCurrentTime, - threadedClassOptions - ) case DeviceType.TRICASTER: return DeviceContainer.create( '../../dist/integrations/tricaster/index.js', @@ -586,6 +576,7 @@ export class Conductor extends EventEmitter { case DeviceType.SHOTOKU: case DeviceType.SOFIE_CHEF: case DeviceType.TCPSEND: + case DeviceType.TELEMETRICS: case DeviceType.QUANTEL: { ensureIsImplementedAsService(deviceOptions.type) diff --git a/packages/timeline-state-resolver/src/integrations/telemetrics/__tests__/telemetrics.spec.ts b/packages/timeline-state-resolver/src/integrations/telemetrics/__tests__/telemetrics.spec.ts index b0a6d644f..70aaca1bd 100644 --- a/packages/timeline-state-resolver/src/integrations/telemetrics/__tests__/telemetrics.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/telemetrics/__tests__/telemetrics.spec.ts @@ -1,7 +1,7 @@ import { TelemetricsDevice } from '..' import { - DeviceOptionsTelemetrics, DeviceType, + Mappings, StatusCode, Timeline, TimelineContentTelemetrics, @@ -10,6 +10,7 @@ import { import { Socket } from 'net' import { DoOrderFunctionNothing } from '../../../devices/doOnTime' import { literal } from '../../../lib' +import { getDeviceContext } from '../../__tests__/testlib' const SERVER_PORT = 5000 const SERVER_HOST = '1.1.1.1' @@ -68,17 +69,6 @@ describe('telemetrics', () => { jest.restoreAllMocks() }) - describe('deviceName', () => { - it('returns "Telemetrics" plus the device id', () => { - const deviceId = 'someId' - device = createTelemetricsDevice(deviceId) - - const result = device.deviceName - - expect(result).toBe(`Telemetrics ${deviceId}`) - }) - }) - describe('init', () => { it('has correct ip, connects to server', () => { device = createTelemetricsDevice() @@ -140,6 +130,18 @@ describe('telemetrics', () => { }) }) + function handleState( + device: TelemetricsDevice, + state: Timeline.TimelineState, + mappings: Mappings + ) { + const deviceState = device.convertTimelineStateToDeviceState(state, mappings) + const commands = device.diffStates(undefined, deviceState, mappings, 1) + for (const command of commands) { + void device.sendCommand(command) + } + } + describe('handleState', () => { it('has correctly formatted command', () => { device = createInitializedTelemetricsDevice() @@ -147,7 +149,7 @@ describe('telemetrics', () => { const commandPostFix = '\r' const presetNumber = 5 - device.handleState(createTimelineState(presetNumber), {}) + handleState(device, createTimelineState(presetNumber), {}) const expectedCommand = `${commandPrefix}${presetNumber}${commandPostFix}` expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledWith(expectedCommand) @@ -157,7 +159,7 @@ describe('telemetrics', () => { device = createInitializedTelemetricsDevice() const presetNumber = 1 - device.handleState(createTimelineState(presetNumber), {}) + handleState(device, createTimelineState(presetNumber), {}) const expectedResult = `P0C${presetNumber}\r` expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledWith(expectedResult) @@ -167,7 +169,7 @@ describe('telemetrics', () => { device = createInitializedTelemetricsDevice() const presetNumber = 2 - device.handleState(createTimelineState(presetNumber), {}) + handleState(device, createTimelineState(presetNumber), {}) const expectedResult = `P0C${presetNumber}\r` expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledWith(expectedResult) @@ -176,7 +178,7 @@ describe('telemetrics', () => { it('receives three presets, sends three commands', () => { device = createInitializedTelemetricsDevice() - device.handleState(createTimelineState([1, 2, 3]), {}) + handleState(device, createTimelineState([1, 2, 3]), {}) expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledTimes(3) }) @@ -193,7 +195,7 @@ describe('telemetrics', () => { }), } as unknown as Timeline.ResolvedTimelineObjectInstance - device.handleState(timelineState, {}) + handleState(device, timelineState, {}) expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledTimes(2) }) @@ -205,23 +207,16 @@ describe('telemetrics', () => { const laterTimelineState = createTimelineState(1) laterTimelineState.time = timelineState.time + 100 - device.handleState(timelineState, {}) - device.handleState(laterTimelineState, {}) + handleState(device, timelineState, {}) + handleState(device, laterTimelineState, {}) expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledTimes(2) }) }) }) -function createTelemetricsDevice(deviceId?: string): TelemetricsDevice { - const deviceOptions: DeviceOptionsTelemetrics = { - type: DeviceType.TELEMETRICS, - } - return new TelemetricsDevice(deviceId ?? '', deviceOptions, mockGetCurrentTime) -} - -async function mockGetCurrentTime(): Promise { - return new Promise((resolve) => resolve(1)) +function createTelemetricsDevice(): TelemetricsDevice { + return new TelemetricsDevice(getDeviceContext()) } function createInitializedTelemetricsDevice(): TelemetricsDevice { diff --git a/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts b/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts index 902bb5849..78210f4bd 100644 --- a/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts +++ b/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts @@ -1,6 +1,6 @@ import { - DeviceOptionsTelemetrics, - DeviceType, + ActionExecutionResult, + DeviceStatus, Mappings, StatusCode, TelemetricsOptions, @@ -8,13 +8,9 @@ import { TimelineContentTelemetrics, TSRTimelineContent, } from 'timeline-state-resolver-types' -import { DeviceStatus, DeviceWithState } from '../../devices/device' import { Socket } from 'net' -import * as _ from 'underscore' -import { DoOnTime } from '../../devices/doOnTime' -import Timer = NodeJS.Timer +import { CommandWithContext, Device } from '../../service/device' -const TELEMETRICS_NAME = 'Telemetrics' const TELEMETRICS_COMMAND_PREFIX = 'P0C' const DEFAULT_SOCKET_PORT = 5000 const TIMEOUT_IN_MS = 2000 @@ -23,47 +19,32 @@ interface TelemetricsState { presetShotIdentifiers: number[] } +interface TelemetricsCommandWithContext { + command: { presetShotIdentifier: number } + context: string + timelineObjId: string +} + /** * Connects to a Telemetrics Device on port 5000 using a TCP socket. * This class uses a fire and forget approach. */ -export class TelemetricsDevice extends DeviceWithState { - private doOnTime: DoOnTime +export class TelemetricsDevice extends Device { + readonly actions: { + [id: string]: (id: string, payload?: Record) => Promise + } = {} private socket: Socket | undefined private statusCode: StatusCode = StatusCode.UNKNOWN private errorMessage: string | undefined - private retryConnectionTimer: Timer | undefined - - constructor(deviceId: string, deviceOptions: DeviceOptionsTelemetrics, getCurrentTime: () => Promise) { - super(deviceId, deviceOptions, getCurrentTime) - - this.doOnTime = new DoOnTime(() => this.getCurrentTime()) - this.handleDoOnTime(this.doOnTime, 'telemetrics') - } - - get canConnect(): boolean { - return true - } - - clearFuture(_clearAfterTime: number): void { - // No state to handle - we use a fire and forget approach - } + private retryConnectionTimer: NodeJS.Timer | undefined get connected(): boolean { return this.statusCode === StatusCode.GOOD } - get deviceName(): string { - return `${TELEMETRICS_NAME} ${this.deviceId}` - } - - get deviceType() { - return DeviceType.TELEMETRICS - } - - getStatus(): DeviceStatus { + getStatus(): Omit { const messages: string[] = [] switch (this.statusCode) { @@ -87,42 +68,55 @@ export class TelemetricsDevice extends DeviceWithState, mappings: Mappings): void { - super.onHandleState(newState, mappings) - const previousStateTime: number = Math.max(this.getCurrentTime(), newState.time) - const oldState: TelemetricsState = this.getStateBefore(previousStateTime)?.state ?? { presetShotIdentifiers: [] } - const newTelemetricsState: TelemetricsState = this.findNewTelemetricsState(newState) - - this.doOnTime.clearQueueNowAndAfter(previousStateTime) - - this.setState(newTelemetricsState, newState.time) - const presetIdentifiersToSend: number[] = this.filterNewPresetIdentifiersFromOld(newTelemetricsState, oldState) - - presetIdentifiersToSend.forEach((presetShotIdentifier) => this.queueCommand(presetShotIdentifier, newState)) - } - - private findNewTelemetricsState(newState: Timeline.TimelineState): TelemetricsState { + diffStates( + oldState: TelemetricsState | undefined, + newState: TelemetricsState, + _mappings: Mappings, + _time: number + ): TelemetricsCommandWithContext[] { + return newState.presetShotIdentifiers + .filter((preset) => !oldState || !oldState.presetShotIdentifiers.includes(preset)) + .map((presetShotIdentifier) => { + return { + command: { presetShotIdentifier }, + context: '', + timelineObjId: '', + } + }) + } + + convertTimelineStateToDeviceState( + state: Timeline.TimelineState, + _newMappings: Mappings + ): TelemetricsState { const newTelemetricsState: TelemetricsState = { presetShotIdentifiers: [] } - newTelemetricsState.presetShotIdentifiers = _.map(newState.layers, (timelineObject, _layerName) => { - const telemetricsContent = timelineObject.content as TimelineContentTelemetrics - return telemetricsContent.presetShotIdentifiers - }).flat() + newTelemetricsState.presetShotIdentifiers = Object.entries(state.layers) + .map(([_layerName, timelineObject]) => { + const telemetricsContent = timelineObject.content as TimelineContentTelemetrics + return telemetricsContent.presetShotIdentifiers + }) + .flat() return newTelemetricsState } - private filterNewPresetIdentifiersFromOld(newState: TelemetricsState, oldState: TelemetricsState): number[] { - return newState.presetShotIdentifiers.filter((preset) => !oldState.presetShotIdentifiers.includes(preset)) - } + async sendCommand({ command, context, timelineObjId }: TelemetricsCommandWithContext): Promise { + const cwc: CommandWithContext = { + context, + command, + timelineObjId, + } + this.context.logger.debug(cwc) - private queueCommand(presetShotIdentifier: number, newState: Timeline.TimelineState) { - const command = `${TELEMETRICS_COMMAND_PREFIX}${presetShotIdentifier}\r` - this.doOnTime.queue(newState.time, undefined, () => this.socket && this.socket.write(command)) + // Skip attempting send if not connected + if (!this.socket) return + + const commandStr = `${TELEMETRICS_COMMAND_PREFIX}${command.presetShotIdentifier}\r` + this.socket.write(commandStr) } async init(options: TelemetricsOptions): Promise { @@ -141,7 +135,7 @@ export class TelemetricsDevice extends DeviceWithState { - this.emit('debug', `${this.deviceName} received data: ${data.toString()}`) + this.context.logger.debug(`received data: ${data.toString()}`) }) this.socket.on('error', (error: Error) => { @@ -149,7 +143,6 @@ export class TelemetricsDevice extends DeviceWithState { - this.doOnTime.dispose() if (hadError) { this.updateStatus(StatusCode.BAD) this.reconnect(host, port) @@ -159,7 +152,7 @@ export class TelemetricsDevice extends DeviceWithState { - this.emit('debug', 'Successfully connected to device') + this.context.logger.debug('Successfully connected to device') this.updateStatus(StatusCode.GOOD) }) } @@ -169,7 +162,7 @@ export class TelemetricsDevice extends DeviceWithState { - this.emit('debug', 'Reconnecting...') + this.context.logger.debug('Reconnecting...') clearTimeout(this.retryConnectionTimer) this.retryConnectionTimer = undefined this.connectToDevice(host, port) }, TIMEOUT_IN_MS) } - prepareForHandleState(newStateTime: number): void { - this.doOnTime.clearQueueNowAndAfter(newStateTime) - this.cleanUpStates(0, newStateTime) - } - async terminate(): Promise { - this.doOnTime.dispose() if (this.retryConnectionTimer) { clearTimeout(this.retryConnectionTimer) this.retryConnectionTimer = undefined diff --git a/packages/timeline-state-resolver/src/service/devices.ts b/packages/timeline-state-resolver/src/service/devices.ts index dc724b0ff..f84a72e1f 100644 --- a/packages/timeline-state-resolver/src/service/devices.ts +++ b/packages/timeline-state-resolver/src/service/devices.ts @@ -14,6 +14,7 @@ import { PanasonicPtzDevice } from '../integrations/panasonicPTZ' import { LawoDevice } from '../integrations/lawo' import { SofieChefDevice } from '../integrations/sofieChef' import { PharosDevice } from '../integrations/pharos' +import { TelemetricsDevice } from '../integrations/telemetrics' export interface DeviceEntry { deviceClass: new (context: DeviceContextAPI) => Device @@ -36,6 +37,7 @@ export type ImplementedServiceDeviceTypes = | DeviceType.SHOTOKU | DeviceType.SOFIE_CHEF | DeviceType.TCPSEND + | DeviceType.TELEMETRICS | DeviceType.QUANTEL // TODO - move all device implementations here and remove the old Device classes @@ -118,6 +120,12 @@ export const DevicesDict: Record = { deviceName: (deviceId: string) => 'TCP' + deviceId, executionMode: () => 'sequential', // todo: should this be configurable? }, + [DeviceType.TELEMETRICS]: { + deviceClass: TelemetricsDevice, + canConnect: true, + deviceName: (deviceId: string) => 'Telemetrics ' + deviceId, + executionMode: () => 'salvo', + }, [DeviceType.QUANTEL]: { deviceClass: QuantelDevice, canConnect: true,