From 8af3262bd631416e03a630890925de83abaf38a3 Mon Sep 17 00:00:00 2001 From: lukejdav Date: Tue, 2 Apr 2024 16:05:27 +0100 Subject: [PATCH] Post Meeting updates and notes Co-authored-by: Johan Nyman --- packages/quick-tsr/input/devices.ts | 20 ++- packages/quick-tsr/input/mappings.ts | 59 +++++- packages/quick-tsr/input/timeline.ts | 169 +++++++++++++----- .../src/generated/artnet.ts | 28 +++ .../src/generated/index.ts | 4 + .../integrations/artnet/$schemas/notes.txt | 24 +++ .../integrations/artnet/$schemas/options.json | 4 +- .../src/integrations/artnet/index.ts | 135 ++++++++++++++ 8 files changed, 388 insertions(+), 55 deletions(-) create mode 100644 packages/timeline-state-resolver-types/src/generated/artnet.ts create mode 100644 packages/timeline-state-resolver/src/integrations/artnet/$schemas/notes.txt create mode 100644 packages/timeline-state-resolver/src/integrations/artnet/index.ts diff --git a/packages/quick-tsr/input/devices.ts b/packages/quick-tsr/input/devices.ts index 585c26dcb4..2f5f77c57b 100644 --- a/packages/quick-tsr/input/devices.ts +++ b/packages/quick-tsr/input/devices.ts @@ -3,11 +3,25 @@ import { DeviceType } from 'timeline-state-resolver' export const input: TSRInput = { devices: { - caspar0: { - type: DeviceType.CASPARCG, + universe0: { + type: DeviceType.ARTNET, options: { host: '127.0.0.1', - port: 5250, + mode: 'full', + fps: 44, + + profileBehaviours: { + + // By default, the priority mode for all channels are "highest value takes precedence" + 'strobe' : 'invert' + + // '0:145': 'last' + // 'last' + // 'highest' + // 'lowest' + // 'invert' + + } }, }, }, diff --git a/packages/quick-tsr/input/mappings.ts b/packages/quick-tsr/input/mappings.ts index 3d72fcca66..bfb8d703bf 100644 --- a/packages/quick-tsr/input/mappings.ts +++ b/packages/quick-tsr/input/mappings.ts @@ -4,13 +4,60 @@ import type { TSRInput } from '../src' export const input: TSRInput = { mappings: { - casparLayer0: literal>({ - device: DeviceType.CASPARCG, - deviceId: 'caspar0', + dimmer0: literal>({ + device: DeviceType.ARTNET, + deviceId: 'universe0', options: { - mappingType: MappingCasparCGType.Layer, - channel: 1, - layer: 10, + universe: 0, + profileMappings: { + // Maps dimmer to channel 1: + "dimmer": 1, + } + }, + }), + rgb0: literal>({ + device: DeviceType.ARTNET, + deviceId: 'universe0', + options: { + universe: 0, + profileMappings: { + "dimmer": 6, + "RGB": [7,8,9], + } + }, + }), + rgb0_dimmer: literal>({ + device: DeviceType.ARTNET, + deviceId: 'universe0', + options: { + universe: 0, + profileMappings: { + "dimmer": 6, + } + }, + }), + dimmerGroup0: literal>({ + device: DeviceType.ARTNET, + deviceId: 'universe0', + options: { + universe: 0, + profileMappings: { + // Maps dimmers to channel group: + "dimmers": [11, 12, 13, 14, 15, 16, 17, 18], + } + }, + }), + + everything: literal>({ + device: DeviceType.ARTNET, + deviceId: 'universe0', + options: { + universe: 0, + profileMappings: { + // "everything": ['1-50', '100-512'], + "everything": '1-512' + + } }, }), }, diff --git a/packages/quick-tsr/input/timeline.ts b/packages/quick-tsr/input/timeline.ts index d7f02e00f0..7ab745364c 100644 --- a/packages/quick-tsr/input/timeline.ts +++ b/packages/quick-tsr/input/timeline.ts @@ -9,6 +9,107 @@ import { literal } from 'timeline-state-resolver/dist/lib' export const input: TSRInput = { timeline: [ + + literal>({ + id: 'artnet0', + enable: { + start: Date.now(), + duration: 20 * 1000 + }, + layer: 'dimmer00', + content: { + values: { + dimmer: 255 + } + } + }), + literal>({ + id: 'artnet0', + enable: { + start: Date.now(), + duration: 20 * 1000 + }, + layer: 'rgb0', + content: { + values: { + // Sets an RGB value: + // rgb: [ 255, 127, 0 ] + + // Defaults to [255, 255, 255] (special case: uses the value for all) + // rgb: 255 + + // Defaults to [ 127, 255, 0 ] (underfill: extend with zeros) + // rgb: [ 127, 255 ] + + // Defaults to [ 127, 255, 209 ] (overfill: just cap and discard the unused values) + // rgb: [ 127, 255, 209, 52 ] + } + } + }), + literal>({ + id: 'myRGBLight0', + enable: { + start: Date.now(), + duration: 20 * 1000 + }, + layer: 'rgb0', + content: { + values: { + dimmer: 255, + rgb: [ 255, 127, 0 ] + } + }, + // keyframes: [ + // { + // id: 'kf0', + // enable: { + + // start: '#myRGBLight0.start + 10', + // }, + // content: { + // values: { + // dimmer: 127 + // } + // } + // } + // ] + }), + literal>({ + id: 'artnet0', + enable: { + start: Date.now(), + duration: 20 * 1000 + }, + layer: 'rgb0', + content: { + + values: { + dimmers: 255 + } + + // value: [255, 0, 0, 255] + } + }), + + literal>({ + id: 'whiteout', + enable: { + start: Date.now() + }, + layer: 'everything', + content: { + values: { + everything: 255 + } + } + }) + + + + + + + literal>({ id: 'video0', enable: { @@ -19,13 +120,13 @@ export const input: TSRInput = { content: { deviceType: DeviceType.CASPARCG, type: TimelineContentTypeCasparCg.MEDIA, - file: 'amb', + file: 'amb.mp4', mixer: { rotation: 0, - anchor: { - x: 0.5, - y: 0.5, - }, + // anchor: { + // x: 0.5, + // y: 0.5, + // }, fill: { x: 0.5, y: 0.5, @@ -34,53 +135,33 @@ export const input: TSRInput = { }, }, - $references: { - 'mixer.fill.xScale': { - // Local path to overwrite - datastoreKey: 'scale', // Reference key in datastore - overwrite: false, - }, - 'mixer.fill.yScale': { - // Local path to overwrite - datastoreKey: 'scale', // Reference key in datastore - overwrite: false, - }, - }, + // $references: { + // 'mixer.fill.xScale': { + // // Local path to overwrite + // datastoreKey: 'scale', // Reference key in datastore + // overwrite: false, + // }, + // 'mixer.fill.yScale': { + // // Local path to overwrite + // datastoreKey: 'scale', // Reference key in datastore + // overwrite: false, + // }, + // }, }, - /* keyframes: [ { id: 'kf0', enable: { - start: 1000, - duration: 5000, + while: true, + // start: '#video0.start + 10', }, content: { mixer: { - rotation: 45, - changeTransition: { - duration: 5000, - easing: Ease.LINEAR, - }, - }, - }, - }, - { - id: 'kf1', - enable: { - start: 1, - }, - content: { - mixer: { - changeTransition: { - duration: 5000, - easing: Ease.LINEAR, - }, - }, - }, - }, - ], - */ + rotation: 90 + } + } + } + ] }), ], } diff --git a/packages/timeline-state-resolver-types/src/generated/artnet.ts b/packages/timeline-state-resolver-types/src/generated/artnet.ts new file mode 100644 index 0000000000..bf5a129bb2 --- /dev/null +++ b/packages/timeline-state-resolver-types/src/generated/artnet.ts @@ -0,0 +1,28 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run "yarn generate-schema-types" to regenerate this file. + */ + +export interface ArtNetOptions { + host: string + mode: Mode + fps: number +} + +export enum Mode { + FULL = 'full', + PARTIAL = 'partial' +} + +export interface MappingArtnetUniverse { + universe: number + mappingType: MappingArtnetType.Universe +} + +export enum MappingArtnetType { + Universe = 'universe', +} + +export type SomeMappingArtnet = MappingArtnetUniverse diff --git a/packages/timeline-state-resolver-types/src/generated/index.ts b/packages/timeline-state-resolver-types/src/generated/index.ts index a222d9dd98..b893598d4f 100644 --- a/packages/timeline-state-resolver-types/src/generated/index.ts +++ b/packages/timeline-state-resolver-types/src/generated/index.ts @@ -9,6 +9,9 @@ export * from './action-schema' export * from './abstract' import { SomeMappingAbstract } from './abstract' +export * from './artnet' +import { SomeMappingArtnet } from './artnet' + export * from './atem' import { SomeMappingAtem } from './atem' @@ -74,6 +77,7 @@ import { SomeMappingVmix } from './vmix' export type TSRMappingOptions = | SomeMappingAbstract + | SomeMappingArtnet | SomeMappingAtem | SomeMappingCasparCG | SomeMappingHttpSend diff --git a/packages/timeline-state-resolver/src/integrations/artnet/$schemas/notes.txt b/packages/timeline-state-resolver/src/integrations/artnet/$schemas/notes.txt new file mode 100644 index 0000000000..02065f6c1a --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/artnet/$schemas/notes.txt @@ -0,0 +1,24 @@ +notes + +additional json for building channel groups +wont be included yet due to complexity relating to ui experience + + + "size": { + "type": "integer", + "ui:title": "Size", + "ui:description": "The number of channels to be included", + "ui:summaryTitle": "Group Size", + "default": 1, + "min": 1, + "max": 512 + }, + "channels": { + "type": "string", + "ui:title": "Channels", + "ui:description": "comma separated list of channels to use", + "ui:summaryTitle": "Channel List", + "default": 1, + "min": 1, + "max": 512 + } \ No newline at end of file diff --git a/packages/timeline-state-resolver/src/integrations/artnet/$schemas/options.json b/packages/timeline-state-resolver/src/integrations/artnet/$schemas/options.json index 89f36a454c..7184ffbf5d 100644 --- a/packages/timeline-state-resolver/src/integrations/artnet/$schemas/options.json +++ b/packages/timeline-state-resolver/src/integrations/artnet/$schemas/options.json @@ -11,7 +11,7 @@ "mode": { "type": "string", "ui:title": "Transmission Mode", - "ui:description": "Use Full or Partial transmission mode", + "ui:description": "Use Full or Partial transmission mode. Partial mode updates only changed values other than keyframes, reducing network traffic.", "ui:summaryTitle": "Transmission Mode", "enum": ["full", "partial"], "tsEnumNames": ["FULL", "PARTIAL"], @@ -27,6 +27,6 @@ "max": 120 } }, - "required": ["host", "mode", "fps"], + "required": ["host"], "additionalProperties": false } \ No newline at end of file diff --git a/packages/timeline-state-resolver/src/integrations/artnet/index.ts b/packages/timeline-state-resolver/src/integrations/artnet/index.ts new file mode 100644 index 0000000000..9f2c7a8789 --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/artnet/index.ts @@ -0,0 +1,135 @@ +import { + ActionExecutionResult, + DeviceStatus, + DeviceType, + Timeline, + TSRTimelineContent, + ArtNetCommandContent, + ArtNetOptions, +} from 'timeline-state-resolver-types' +import { CommandWithContext, Device } from '../../service/device' +import * as artnet from 'artnet' + +import Debug from 'debug' +import _ = require('underscore') +import { Easing } from '../../devices/transitions/easings' +import { assertNever } from '../../lib' +import { StatusCode } from 'timeline-state-resolver-types' +const debug = Debug('timeline-state-resolver:artnet') + +export interface ArtNetDeviceState { + [address: string]: ArtNetDeviceStateContent +} + +interface ArtNetDeviceStateContent extends ArtNetCommandContent { + fromTLObject: string +} + +export interface ArtNetCommandWithContext { + command: any // todo + context: string + timelineObjId: string +} + + +export class ArtNetDevice extends Device{ + + // private _oscClient!: + // private _ArtNetClientStatus: 'connected' | 'disconnected' = 'disconnected' + // private transitions: { + // [value: number]: { + // started: number + // } & ArtNetDeviceStateContent + + // } + + async init(options: ArtNetOptions): Promise { + this.options = options + return true + } + async terminate(): Promise { + // this._terminated = true + } + get connected(): boolean { + return false + } + getStatus(): Omit { + return { + statusCode: StatusCode.GOOD, + messages: [], + } + } + + convertTimelineStateToDeviceState(state: Timeline.TimelineState): ArtNetDeviceState { + + // TODO: Convert the timeline state into your own (internal) ArtNetState + // Tip: This is where you put the logic for "highest takes precedence" + + Object.values>(state.layer).forEach((layer) => { + + }) + + } + diffStates(oldState: ArtNetDeviceState | undefined, newState: ArtNetDeviceState): Array { + const commands: Array = [] + + // TODO: compare the new state with the old state, and then output the COMMANDS needed in order to acheive the new state. + + for (const [universeId, uniValues ] of Object.entries(newState)) { + + for (const [channel, channelValue ] of Object.entries(uniValues)) { + + const oldChannelValue = oldState?.[universeId]?.[channel] + + if (channelValue.value !== oldChannelValue?.value) { + // Something has changed. + + // send channelValue.value + // commands.push({ TODO: define command here }) + } + + } + + } + + + for (const [universeId, uniValues ] of Object.entries(oldState)) { + for (const [channel, channelValue ] of Object.entries(uniValues)) { + + if (!newState[universeId][channel]) { + // The channel value has been removed from the state + // send 0 + } + } + } + + return commands + + } + + async sendCommand(cmd: ArtNetDeviceCommand): Promise { + + // This is called when it's time to send the command + + // Send the command + + } + + private async updateArtNet() { + // updates sendArtnet with new values only + return + } + + private async sendArtNetUniverse() { + // sends entire artnet universe data with appropriate parameters + } + +} + +interface ArtNetDeviceState { + [universe: string]: { + [channel: string]: { + value: number + } + } +} \ No newline at end of file