Skip to content

Commit

Permalink
feat: refactor telemtrics device SOFIE-2496
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Jun 25, 2024
1 parent 1d433f3 commit 30e0ab2
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 109 deletions.
11 changes: 1 addition & 10 deletions packages/timeline-state-resolver/src/conductor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -546,15 +545,6 @@ export class Conductor extends EventEmitter<ConductorEvents> {
getCurrentTime,
threadedClassOptions
)
case DeviceType.TELEMETRICS:
return DeviceContainer.create<DeviceOptionsTelemetrics, typeof TelemetricsDevice>(
'../../dist/integrations/telemetrics/index.js',
'TelemetricsDevice',
deviceId,
deviceOptions,
getCurrentTime,
threadedClassOptions
)
case DeviceType.TRICASTER:
return DeviceContainer.create<DeviceOptionsTriCasterInternal, typeof TriCasterDevice>(
'../../dist/integrations/tricaster/index.js',
Expand Down Expand Up @@ -586,6 +576,7 @@ export class Conductor extends EventEmitter<ConductorEvents> {
case DeviceType.SHOTOKU:
case DeviceType.SOFIE_CHEF:
case DeviceType.TCPSEND:
case DeviceType.TELEMETRICS:
case DeviceType.QUANTEL: {
ensureIsImplementedAsService(deviceOptions.type)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TelemetricsDevice } from '..'
import {
DeviceOptionsTelemetrics,
DeviceType,
Mappings,
StatusCode,
Timeline,
TimelineContentTelemetrics,
Expand All @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -140,14 +130,26 @@ describe('telemetrics', () => {
})
})

function handleState(
device: TelemetricsDevice,
state: Timeline.TimelineState<TSRTimelineContent>,
mappings: Mappings<unknown>
) {
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()
const commandPrefix = 'P0C'
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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
})
Expand All @@ -193,7 +195,7 @@ describe('telemetrics', () => {
}),
} as unknown as Timeline.ResolvedTimelineObjectInstance<any>

device.handleState(timelineState, {})
handleState(device, timelineState, {})

expect(MOCKED_SOCKET_WRITE).toHaveBeenCalledTimes(2)
})
Expand All @@ -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<number> {
return new Promise<number>((resolve) => resolve(1))
function createTelemetricsDevice(): TelemetricsDevice {
return new TelemetricsDevice(getDeviceContext())
}

function createInitializedTelemetricsDevice(): TelemetricsDevice {
Expand Down
129 changes: 58 additions & 71 deletions packages/timeline-state-resolver/src/integrations/telemetrics/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import {
DeviceOptionsTelemetrics,
DeviceType,
ActionExecutionResult,
DeviceStatus,
Mappings,
StatusCode,
TelemetricsOptions,
Timeline,
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
Expand All @@ -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<TelemetricsState, DeviceOptionsTelemetrics> {
private doOnTime: DoOnTime
export class TelemetricsDevice extends Device<TelemetricsOptions, TelemetricsState, TelemetricsCommandWithContext> {
readonly actions: {
[id: string]: (id: string, payload?: Record<string, any>) => Promise<ActionExecutionResult>
} = {}

private socket: Socket | undefined
private statusCode: StatusCode = StatusCode.UNKNOWN
private errorMessage: string | undefined

private retryConnectionTimer: Timer | undefined

constructor(deviceId: string, deviceOptions: DeviceOptionsTelemetrics, getCurrentTime: () => Promise<number>) {
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<DeviceStatus, 'active'> {
const messages: string[] = []

switch (this.statusCode) {
Expand All @@ -87,42 +68,55 @@ export class TelemetricsDevice extends DeviceWithState<TelemetricsState, DeviceO
return {
statusCode: this.statusCode,
messages,
active: this.isActive,
}
}

handleState(newState: Timeline.TimelineState<TSRTimelineContent>, 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<TSRTimelineContent>): TelemetricsState {
diffStates(
oldState: TelemetricsState | undefined,
newState: TelemetricsState,
_mappings: Mappings<unknown>,
_time: number
): TelemetricsCommandWithContext[] {
return newState.presetShotIdentifiers
.filter((preset) => !oldState || !oldState.presetShotIdentifiers.includes(preset))
.map((presetShotIdentifier) => {
return {
command: { presetShotIdentifier },
context: '',
timelineObjId: '',
}
})
}

convertTimelineStateToDeviceState(
state: Timeline.TimelineState<TSRTimelineContent>,
_newMappings: Mappings<unknown>
): 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<Timeline.ResolvedTimelineObjectInstance>(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<void> {
const cwc: CommandWithContext = {
context,
command,
timelineObjId,
}
this.context.logger.debug(cwc)

private queueCommand(presetShotIdentifier: number, newState: Timeline.TimelineState<TSRTimelineContent>) {
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<boolean> {
Expand All @@ -141,15 +135,14 @@ export class TelemetricsDevice extends DeviceWithState<TelemetricsState, DeviceO
this.socket = new Socket()

this.socket.on('data', (data: Buffer) => {
this.emit('debug', `${this.deviceName} received data: ${data.toString()}`)
this.context.logger.debug(`received data: ${data.toString()}`)
})

this.socket.on('error', (error: Error) => {
this.updateStatus(StatusCode.BAD, error)
})

this.socket.on('close', (hadError: boolean) => {
this.doOnTime.dispose()
if (hadError) {
this.updateStatus(StatusCode.BAD)
this.reconnect(host, port)
Expand All @@ -159,7 +152,7 @@ export class TelemetricsDevice extends DeviceWithState<TelemetricsState, DeviceO
})

this.socket.on('connect', () => {
this.emit('debug', 'Successfully connected to device')
this.context.logger.debug('Successfully connected to device')
this.updateStatus(StatusCode.GOOD)
})
}
Expand All @@ -169,28 +162,22 @@ export class TelemetricsDevice extends DeviceWithState<TelemetricsState, DeviceO
if (error) {
this.errorMessage = error.message
}
this.emit('connectionChanged', this.getStatus())
this.context.connectionChanged(this.getStatus())
}

private reconnect(host: string, port: number): void {
if (this.retryConnectionTimer) {
return
}
this.retryConnectionTimer = setTimeout(() => {
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<void> {
this.doOnTime.dispose()
if (this.retryConnectionTimer) {
clearTimeout(this.retryConnectionTimer)
this.retryConnectionTimer = undefined
Expand Down
Loading

0 comments on commit 30e0ab2

Please sign in to comment.