diff --git a/package.json b/package.json index 96329c1d8..9a498f05a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:main": "cd packages/timeline-state-resolver && yarn build", "lint": "lerna exec yarn lint -- --", "test": "lerna exec yarn test", + "test:changed": "lerna run --since origin/master --include-dependents test", "unit": "lerna exec yarn unit", "unitci": "lerna exec yarn unitci", "watch": "lerna run --parallel build:main -- --watch --preserveWatchOutput", diff --git a/packages/quick-tsr/src/index.ts b/packages/quick-tsr/src/index.ts index 22a485892..d8ad78b59 100644 --- a/packages/quick-tsr/src/index.ts +++ b/packages/quick-tsr/src/index.ts @@ -144,6 +144,12 @@ function reloadInput(changed?: { path: string; stats: fs.Stats }) { currentInput.mappings = clone(newInput.mappings) currentInput.timeline = clone(newInput.timeline) + // Check that layers are correct. + newInput.timeline.forEach((obj) => { + if (!newInput.mappings[obj.layer]) + console.error(`Object ${obj.id} refers to a layer/mapping that does not exist: "${obj.layer}"`) + }) + tsr.setTimelineAndMappings(newInput.timeline, newInput.mappings) } if (!_.isEqual(newInput.datastore, currentInput.datastore)) { diff --git a/packages/timeline-state-resolver-types/src/generated/sisyfos.ts b/packages/timeline-state-resolver-types/src/generated/sisyfos.ts index 33bd7a92c..2fee893d5 100644 --- a/packages/timeline-state-resolver-types/src/generated/sisyfos.ts +++ b/packages/timeline-state-resolver-types/src/generated/sisyfos.ts @@ -34,11 +34,17 @@ export enum MappingSisyfosType { export type SomeMappingSisyfos = MappingSisyfosChannel | MappingSisyfosChannelByLabel | MappingSisyfosChannels +export interface SetSisyfosChannelStatePayload { + channel: number +} + export enum SisyfosActions { - Reinit = 'reinit' + Reinit = 'reinit', + SetSisyfosChannelState = 'setSisyfosChannelState' } export interface SisyfosActionExecutionResults { - reinit: () => void + reinit: () => void, + setSisyfosChannelState: (payload: SetSisyfosChannelStatePayload) => void } export type SisyfosActionExecutionPayload = Parameters< SisyfosActionExecutionResults[A] diff --git a/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts b/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts index f7bd1404a..156920711 100644 --- a/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts +++ b/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts @@ -1,5 +1,10 @@ import { DeviceType } from '..' +/* + * TRIGGERVALUE is used to SET_CHANNEL in Sisyfos + * When value is changed to a new value (e.g. Date.now()) Sisyfos will set the channel to + * the Current TSR State using setSisyfosChannel() + */ export enum TimelineContentTypeSisyfos { CHANNEL = 'channel', CHANNELS = 'channels', @@ -22,6 +27,9 @@ export interface SisyfosChannelOptions { label?: string visible?: boolean fadeTime?: number + muteOn?: boolean + inputGain?: number + inputSelector?: number } export interface TimelineContentSisyfosTriggerValue extends TimelineContentSisyfos { @@ -33,6 +41,7 @@ export interface TimelineContentSisyfosChannel extends TimelineContentSisyfos, S type: TimelineContentTypeSisyfos.CHANNEL resync?: boolean overridePriority?: number // defaults to 0 + triggerValue?: string } export interface TimelineContentSisyfosChannels extends TimelineContentSisyfos { type: TimelineContentTypeSisyfos.CHANNELS diff --git a/packages/timeline-state-resolver/src/integrations/sisyfos/$schemas/actions.json b/packages/timeline-state-resolver/src/integrations/sisyfos/$schemas/actions.json index 7cfce0a4d..5c3c73ac7 100644 --- a/packages/timeline-state-resolver/src/integrations/sisyfos/$schemas/actions.json +++ b/packages/timeline-state-resolver/src/integrations/sisyfos/$schemas/actions.json @@ -6,6 +6,22 @@ "name": "Reinitialize", "destructive": false, "timeout": 5000 + }, + { + "id": "setSisyfosChannelState", + "name": "SetSisyfosChannelState", + "payload": { + "type": "object", + "properties": { + "channel": { + "type": "number" + } + }, + "additionalProperties": false, + "required": ["channel"] + }, + "destructive": false, + "timeout": 5000 } ] -} +} \ No newline at end of file diff --git a/packages/timeline-state-resolver/src/integrations/sisyfos/connection.ts b/packages/timeline-state-resolver/src/integrations/sisyfos/connection.ts index a869a06d3..e3eb8d552 100644 --- a/packages/timeline-state-resolver/src/integrations/sisyfos/connection.ts +++ b/packages/timeline-state-resolver/src/integrations/sisyfos/connection.ts @@ -1,12 +1,20 @@ import * as osc from 'osc' -import { EventEmitter } from 'events' +import { EventEmitter } from 'eventemitter3' import { MetaArgument } from 'osc' /** How often to check connection status */ const CONNECTIVITY_INTERVAL = 3000 // ms const CONNECTIVITY_TIMEOUT = 1000 // ms -export class SisyfosApi extends EventEmitter { +interface SisyfosApiEvents { + error: [Error] + initialized: [] + mixerOnline: [boolean] + connected: [] + disconnected: [] +} + +export class SisyfosApi extends EventEmitter { private _oscClient: osc.UDPPort | undefined private _state?: SisyfosState private _labelToChannel: Map = new Map() @@ -70,6 +78,16 @@ export class SisyfosApi extends EventEmitter { this._oscClient.send({ address: '/take', args: [] }) } else if (command.type === SisyfosCommandType.CLEAR_PST_ROW) { this._oscClient.send({ address: '/clearpst', args: [] }) + } else if (command.type === SisyfosCommandType.RESYNC_CHANNEL) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/state`, + args: [ + { + type: 'i', + value: command.value, + }, + ], + }) } else if (command.type === SisyfosCommandType.LABEL) { this._oscClient.send({ address: `/ch/${command.channel + 1}/label`, @@ -122,6 +140,36 @@ export class SisyfosApi extends EventEmitter { }, ], }) + } else if (command.type === SisyfosCommandType.SET_MUTE) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/mute`, + args: [ + { + type: 'i', + value: command.value === true ? 1 : 0, + }, + ], + }) + } else if (command.type === SisyfosCommandType.SET_INPUT_GAIN) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/inputgain`, + args: [ + { + type: 'f', + value: command.value, + }, + ], + }) + } else if (command.type === SisyfosCommandType.SET_INPUT_SELECTOR) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/inputselector`, + args: [ + { + type: 'i', + value: command.value, + }, + ], + }) } else if (command.type === SisyfosCommandType.SET_CHANNEL) { if (command.values.label) { this._oscClient.send({ @@ -134,6 +182,28 @@ export class SisyfosApi extends EventEmitter { ], }) } + if (command.values.inputGain !== undefined) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/inputgain`, + args: [ + { + type: 'f', + value: command.values.inputGain, + }, + ], + }) + } + if (command.values.inputSelector !== undefined) { + this._oscClient.send({ + address: `/ch/${command.channel + 1}/inputselector`, + args: [ + { + type: 'i', + value: command.values.inputSelector, + }, + ], + }) + } if (command.values.pgmOn !== undefined) { const args: Array = [ { @@ -210,6 +280,33 @@ export class SisyfosApi extends EventEmitter { this._oscClient.send({ address: '/state/full', args: [] }) } + reSyncOneChannel(channel: number) { + if (!this._oscClient) { + throw new Error(`Can't resync channel, OSC client not initialised`) + } + // This will trigger Sisyfos to emit its state of that channel, to be picked up in this.receiver() + this._oscClient.send({ address: `/ch/${channel}/state`, args: [] }) + } + + setSisyfosChannel(channel: number, apiState: Partial) { + if (!this._oscClient) { + throw new Error(`Can't set channel, OSC client not initialised`) + } + const oscApiState: SisyfosChannelOSCAPI = { + pgmOn: apiState.pgmOn === 1, + voOn: apiState.pgmOn === 2, + pstOn: apiState.pstOn === 1, + label: apiState.label || '', + faderLevel: apiState.faderLevel || 0.75, + muteOn: apiState.muteOn || false, + inputGain: apiState.inputGain || 0.75, + inputSelector: apiState.inputSelector || 1, + fadeTime: apiState.fadeTime, + showChannel: apiState.visible, + } + this._oscClient.send({ address: `/setchannel/${channel}`, args: { type: 's', value: JSON.stringify(oscApiState) } }) + } + getChannelByLabel(label: string): number | undefined { return this._labelToChannel.get(label) } @@ -260,19 +357,21 @@ export class SisyfosApi extends EventEmitter { private receiver(message: osc.OscMessage) { const address = message.address.substr(1).split('/') - if (address[0] === 'state') { - if (address[1] === 'full') { - this._state = this.parseSisyfosState(message) - this._labelToChannel = new Map( - Object.entries(this._state.channels).map((v) => [v[1].label, Number(v[0])]) - ) - this.emit('initialized') - } else if (address[1] === 'ch' && this._state) { - const ch = address[2] - this._state.channels[ch] = { - ...this._state.channels[ch], - ...this.parseChannelCommand(message, address.slice(3)), - } + if (address[0] === 'state' && address[1] === 'full') { + this._state = this.parseSisyfosState(message) + this._labelToChannel = new Map( + Object.entries(this._state.channels).map((v) => [v[1].label, Number(v[0])]) + ) + this.emit('initialized') + } else if (address[0] === 'ch' && this._state) { + // This receives updates for a single channel + // But is not used in TSR as of now + // If once neeeded a new event should be implemented: + // like: this.emit('channel-state-changed') + const ch = Number(address[1]) - 1 + this._state.channels[ch] = { + ...this._state.channels[ch], + ...this.parseChannelCommand(message, address.slice(2)), } } else if (address[0] === 'pong') { // a reply to "/ping" @@ -303,13 +402,26 @@ export class SisyfosApi extends EventEmitter { } } - private parseChannelCommand(message: osc.OscMessage, address: Array) { + private parseChannelCommand(message: osc.OscMessage, address: Array): Partial { if (address[0] === 'pgm') { return { pgmOn: message.args[0].value } } else if (address[0] === 'pst') { return { pstOn: message.args[0].value } } else if (address[0] === 'faderlevel') { return { faderLevel: message.args[0].value } + } else if (address[0] === 'state') { + const stateFromChannel = this.parseSisyfosState(message).channels[0] + return { + pgmOn: stateFromChannel.pgmOn, + pstOn: stateFromChannel.pstOn, + faderLevel: stateFromChannel.faderLevel, + visible: stateFromChannel.visible, + label: stateFromChannel.label, + fadeTime: stateFromChannel.fadeTime, + muteOn: stateFromChannel.muteOn, + inputGain: stateFromChannel.inputGain, + inputSelector: stateFromChannel.inputSelector, + } } return {} } @@ -319,7 +431,7 @@ export class SisyfosApi extends EventEmitter { const deviceState: SisyfosState = { channels: {}, resync: false } Object.keys(extState.channel).forEach((index: string) => { - const ch = extState.channel[index] + const ch = extState.channel[index] as SisyfosChannelOSCAPI let pgmOn = 0 if (ch.pgmOn === true) { @@ -333,6 +445,10 @@ export class SisyfosApi extends EventEmitter { pstOn: ch.pstOn === true ? 1 : 0, label: ch.label || '', visible: ch.showChannel ? true : false, + fadeTime: ch.fadeTime || undefined, + muteOn: ch.muteOn || false, + inputGain: ch.inputGain || 0.75, + inputSelector: ch.inputSelector || 1, timelineObjIds: [], } @@ -347,11 +463,15 @@ export enum SisyfosCommandType { TOGGLE_PGM = 'togglePgm', TOGGLE_PST = 'togglePst', SET_FADER = 'setFader', + SET_INPUT_GAIN = 'setInputGain', + SET_INPUT_SELECTOR = 'setInputSelector', + SET_MUTE = 'setMute', CLEAR_PST_ROW = 'clearPstRow', LABEL = 'label', TAKE = 'take', VISIBLE = 'visible', RESYNC = 'resync', + RESYNC_CHANNEL = 'resyncChannel', SET_CHANNEL = 'setChannel', } @@ -362,7 +482,7 @@ export interface BaseCommand { export interface SetChannelCommand { type: SisyfosCommandType.SET_CHANNEL channel: number - values: Partial + values: Partial } export interface ChannelCommand extends BaseCommand { @@ -372,6 +492,10 @@ export interface ChannelCommand extends BaseCommand { | SisyfosCommandType.TOGGLE_PST | SisyfosCommandType.LABEL | SisyfosCommandType.VISIBLE + | SisyfosCommandType.RESYNC_CHANNEL + | SisyfosCommandType.SET_INPUT_SELECTOR + | SisyfosCommandType.SET_INPUT_GAIN + | SisyfosCommandType.SET_MUTE channel: number } @@ -380,11 +504,16 @@ export interface GlobalCommand extends BaseCommand { } export interface BoolCommand extends ChannelCommand { - type: SisyfosCommandType.VISIBLE + type: SisyfosCommandType.VISIBLE | SisyfosCommandType.SET_MUTE value: boolean } export interface ValueCommand extends ChannelCommand { - type: SisyfosCommandType.TOGGLE_PST | SisyfosCommandType.VISIBLE + type: + | SisyfosCommandType.TOGGLE_PST + | SisyfosCommandType.VISIBLE + | SisyfosCommandType.RESYNC_CHANNEL + | SisyfosCommandType.SET_INPUT_SELECTOR + | SisyfosCommandType.SET_INPUT_GAIN value: number } @@ -406,7 +535,7 @@ export type SisyfosCommand = | StringCommand | SetChannelCommand -export interface SisyfosChannel extends SisyfosAPIChannel { +export interface SisyfosChannel extends SisyfosChannelAPI { timelineObjIds: string[] } export interface SisyfosState { @@ -417,17 +546,36 @@ export interface SisyfosState { // ------------------------------------------------------ // Interfaces for the data that comes over OSC: -export interface SisyfosAPIChannel { + +export interface SisyfosChannelAPI { faderLevel: number pgmOn: number pstOn: number label: string visible: boolean fadeTime?: number + muteOn: boolean + inputGain: number + inputSelector: number +} + +// ------------------------------------------------------ +// Interfaces for the data that sends over OSC to Sisyfos: +export interface SisyfosChannelOSCAPI { + faderLevel?: number + pgmOn?: boolean + voOn?: boolean + pstOn?: boolean + label?: string + showChannel?: boolean + fadeTime?: number + muteOn?: boolean + inputGain?: number + inputSelector?: number } export interface SisyfosAPIState { channels: { - [index: string]: SisyfosAPIChannel + [index: string]: SisyfosChannelAPI } } diff --git a/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts b/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts index 759f02dd1..4c01a1c26 100644 --- a/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts +++ b/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts @@ -209,9 +209,9 @@ export class SisyfosMessageDevice extends DeviceWithState( actionId0: A, - _payload: SisyfosActionExecutionPayload + payload: SisyfosActionExecutionPayload ): Promise> { - const actionId = actionId0 as SisyfosActions // type fix for when there is only a single action + const actionId = actionId0 switch (actionId) { case SisyfosActions.Reinit: return this._makeReadyInner() @@ -221,6 +221,16 @@ export class SisyfosMessageDevice extends DeviceWithState ({ result: ActionExecutionResultCode.Error, })) + case SisyfosActions.SetSisyfosChannelState: + if (typeof payload?.channel !== 'number') { + return { + result: ActionExecutionResultCode.Error, + } + } + this._sisyfos.setSisyfosChannel(payload.channel + 1, { ...this.getDeviceState().channels[payload.channel] }) + return { + result: ActionExecutionResultCode.Ok, + } default: return actionNotFoundMessage(actionId) } @@ -277,15 +287,19 @@ export class SisyfosMessageDevice extends DeviceWithState, mappings: Mappings) { + convertStateToSisyfosState(state: Timeline.TimelineState, mappings: Mappings): SisyfosState { const deviceState: SisyfosState = this.getDeviceState(true, mappings) // Set labels to layer names @@ -435,6 +449,9 @@ export class SisyfosMessageDevice extends DeviceWithState