From 381dbf702d498fc175e49db4220422450d6686a3 Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 3 Jan 2026 17:49:23 +0100 Subject: [PATCH 01/11] fix: variable handling operates on debounced updates changed: restructured variable definitions to include variable paths fix: variable parsing for non-numerics --- src/actions/common.ts | 3 +- src/handlers/connection-handler.ts | 11 +- src/handlers/device-detector.ts | 4 +- src/handlers/feedback-handler.ts | 27 +-- src/handlers/logger.ts | 17 +- src/handlers/osc-forwarder.ts | 18 +- src/handlers/state-handler.ts | 39 ++-- src/index.ts | 89 +++++---- src/state/state.ts | 151 ++------------ src/types.ts | 3 +- src/variables/auxiliary.ts | 21 +- src/variables/bus.ts | 6 +- src/variables/channel.ts | 21 +- src/variables/dca.ts | 11 +- src/variables/gpio.ts | 13 +- src/variables/index.ts | 37 ++++ src/variables/main.ts | 14 +- src/variables/matrix.ts | 12 +- src/variables/mutegroup.ts | 9 +- src/variables/showcontrol.ts | 6 +- src/variables/talkback.ts | 46 +++-- src/variables/usb.ts | 6 +- src/variables/variable-handler.ts | 309 ++++++++++++++++------------- src/variables/wlive.ts | 25 ++- 24 files changed, 490 insertions(+), 408 deletions(-) diff --git a/src/actions/common.ts b/src/actions/common.ts index faaa5de..b2030ec 100644 --- a/src/actions/common.ts +++ b/src/actions/common.ts @@ -95,8 +95,9 @@ export function createCommonActions(self: InstanceBaseExt): Companio const send = self.connection!.sendCommand.bind(self.connection) const ensureLoaded = self.stateHandler!.ensureLoaded.bind(self.stateHandler) const state = self.stateHandler?.state + const logger = self.logger if (!state) { - self.logger!.error('State handler or state is not available for creating common actions') + logger?.error('State handler or state is not available for creating common actions') throw new Error('State handler or state is not available') } const transitions = self.transitions diff --git a/src/handlers/connection-handler.ts b/src/handlers/connection-handler.ts index fdd4eb6..6e159eb 100644 --- a/src/handlers/connection-handler.ts +++ b/src/handlers/connection-handler.ts @@ -1,7 +1,8 @@ import osc, { OscMessage } from 'osc' -import { ModuleLogger } from './logger.js' + import { EventEmitter } from 'events' import { OSCSomeArguments } from '@companion-module/base' +import { ModuleLogger } from './logger.js' /** * Handles OSC UDP connection management, message sending, and event emission for the Behringer Wing module. @@ -9,9 +10,9 @@ import { OSCSomeArguments } from '@companion-module/base' */ export class ConnectionHandler extends EventEmitter { private osc: osc.UDPPort - private logger?: ModuleLogger private subscriptionTimer?: NodeJS.Timeout private subscriptionInterval: number = 9000 + private logger?: ModuleLogger /** * Create a new ConnectionHandler. @@ -60,7 +61,8 @@ export class ConnectionHandler extends EventEmitter { }) this.osc.on('message', (msg: OscMessage) => { - this.logger?.debug(`Received ${JSON.stringify(msg)}`) + const stringValue = (msg.args as osc.MetaArgument[])[0].value + this.logger?.debug(`State updated for ${msg.address}: ${stringValue}`) this.emit('message', msg) }) @@ -157,8 +159,7 @@ export class ConnectionHandler extends EventEmitter { args: args, } this.osc.send(command) - // this.osc.send({ address: cmd, args: [] }) // a bit ugly, but needed to keep the desk state up to date in companion if (preventLog) return - this.logger?.debug(`Sending OSC command: ${JSON.stringify(command)}`) + this.logger?.debug(`Sending OSC command: ${command.address} ${argument ?? ''}`) } } diff --git a/src/handlers/device-detector.ts b/src/handlers/device-detector.ts index a5c8caf..6fb61d4 100644 --- a/src/handlers/device-detector.ts +++ b/src/handlers/device-detector.ts @@ -1,7 +1,7 @@ import osc from 'osc' import { WingModel } from '../models/types.js' -import { ModuleLogger } from './logger.js' import { EventEmitter } from 'events' +import { ModuleLogger } from './logger.js' export interface DeviceInfo { deviceName: string @@ -25,8 +25,8 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect private osc?: osc.UDPPort private knownDevices = new Map() private queryTimer: NodeJS.Timeout | undefined - private logger?: ModuleLogger private noDeviceTimeout: NodeJS.Timeout | undefined + private logger?: ModuleLogger constructor(logger?: ModuleLogger) { super() diff --git a/src/handlers/feedback-handler.ts b/src/handlers/feedback-handler.ts index a2405be..4593ac8 100644 --- a/src/handlers/feedback-handler.ts +++ b/src/handlers/feedback-handler.ts @@ -1,9 +1,9 @@ import EventEmitter from 'events' -import { ModuleLogger } from './logger.js' import debounceFn from 'debounce-fn' import { FeedbackId } from '../feedbacks.js' import { OscMessage } from 'osc' import { WingSubscriptions } from '../state/index.js' +import { ModuleLogger } from './logger.js' /** * Handles feedback updates based on incoming OSC messages and manages feedback subscriptions. @@ -12,9 +12,9 @@ import { WingSubscriptions } from '../state/index.js' export class FeedbackHandler extends EventEmitter { private readonly messageFeedbacks = new Set() private readonly debounceMessageFeedbacks: () => void - private logger?: ModuleLogger private pollTimeout?: NodeJS.Timeout private pollInterval: number = 3000 + private logger: ModuleLogger | undefined subscriptions?: WingSubscriptions @@ -24,8 +24,8 @@ export class FeedbackHandler extends EventEmitter { */ constructor(logger?: ModuleLogger) { super() - this.logger = logger + this.logger = logger this.subscriptions = new WingSubscriptions() this.debounceMessageFeedbacks = debounceFn( @@ -47,16 +47,17 @@ export class FeedbackHandler extends EventEmitter { * Process an OSC message and trigger feedback checks if needed. * @param msg OSC message to process. */ - processMessage(msg: OscMessage): void { - this.logger?.debug(`Processing message for feedbacks: ${msg.address}`) - - const toUpdate = this.subscriptions?.getFeedbacks(msg.address) - if (toUpdate === undefined) { - return - } - if (toUpdate.length > 0) { - toUpdate.forEach((f) => this.messageFeedbacks.add(f)) - this.debounceMessageFeedbacks() + processMessage(msgs: Set): void { + this.logger?.debug(`Processing messages for feedbacks`) + for (const msg of msgs) { + const toUpdate = this.subscriptions?.getFeedbacks(msg.address) + if (toUpdate === undefined) { + return + } + if (toUpdate.length > 0) { + toUpdate.forEach((f) => this.messageFeedbacks.add(f)) + this.debounceMessageFeedbacks() + } } } diff --git a/src/handlers/logger.ts b/src/handlers/logger.ts index 16a7df2..a813b3e 100644 --- a/src/handlers/logger.ts +++ b/src/handlers/logger.ts @@ -8,21 +8,22 @@ type LoggerFunction = (level: LogLevel, message: string) => void export class ModuleLogger { private moduleName: string private loggerFn?: LoggerFunction + private enabled: boolean debugMode: boolean timestamps: boolean /** * Create a new ModuleLogger. + * * @param moduleName Name to prefix log messages. * @param loggerFn Optional custom log function. - * @param debugMode Whether to include source location information in logs. - * @param timestamps Whether to include timestamps in logs. */ constructor(moduleName: string, loggerFn?: LoggerFunction) { this.moduleName = moduleName this.loggerFn = loggerFn this.debugMode = false this.timestamps = false + this.enabled = true } /** @@ -38,6 +39,7 @@ export class ModuleLogger { * @param message Message to log. */ debug(message: string): void { + if (!this.enabled) return const msg = this.formatMessage(message) if (this.loggerFn) { this.loggerFn('debug', `[${this.moduleName}] ${msg}`) @@ -51,6 +53,7 @@ export class ModuleLogger { * @param message Message to log. */ info(message: string): void { + if (!this.enabled) return const msg = this.formatMessage(message) if (this.loggerFn) { this.loggerFn('info', `[${this.moduleName}] ${msg}`) @@ -64,6 +67,7 @@ export class ModuleLogger { * @param message Message to log. */ error(message: string): void { + if (!this.enabled) return const msg = this.formatMessage(message) if (this.loggerFn) { this.loggerFn('error', `[${this.moduleName}] ${msg}`) @@ -77,6 +81,7 @@ export class ModuleLogger { * @param message Message to log. */ warn(message: string): void { + if (!this.enabled) return const msg = this.formatMessage(message) if (this.loggerFn) { this.loggerFn('warn', `[${this.moduleName}] ${msg}`) @@ -85,6 +90,14 @@ export class ModuleLogger { } } + enable(): void { + this.enabled = true + } + + disable(): void { + this.enabled = false + } + /** * Format the message with optional timestamp and source location. * @private diff --git a/src/handlers/osc-forwarder.ts b/src/handlers/osc-forwarder.ts index 22af926..03dc557 100644 --- a/src/handlers/osc-forwarder.ts +++ b/src/handlers/osc-forwarder.ts @@ -4,20 +4,22 @@ import { ModuleLogger } from './logger.js' export class OscForwarder { private port: osc.UDPPort | undefined - private logger: ModuleLogger + private logger: ModuleLogger | undefined - constructor(logger: ModuleLogger) { + constructor(logger?: ModuleLogger) { this.logger = logger } - setup(enabled: boolean | undefined, host?: string, port?: number): void { + setup(enabled: boolean | undefined, host?: string, port?: number, logger?: ModuleLogger): void { this.close() + this.logger = logger + if (!enabled || !host || !port) { return } - this.logger.info(`Setting up OSC forwarder to ${host}:${port}`) + this.logger?.info(`Setting up OSC forwarder to ${host}:${port}`) try { this.port = new osc.UDPPort({ localAddress: '0.0.0.0', @@ -28,13 +30,13 @@ export class OscForwarder { }) this.port.on('error', (err: Error): void => { - this.logger.warn(`OSC Forwarder Error: ${err.message}`) + this.logger?.warn(`OSC Forwarder Error: ${err.message}`) }) this.port.open() - this.logger.info(`OSC forwarding enabled to ${host}:${port}`) + this.logger?.info(`OSC forwarding enabled to ${host}:${port}`) } catch (err: any) { - this.logger.error(`Failed to setup OSC forwarder: ${err?.message ?? err}`) + this.logger?.error(`Failed to setup OSC forwarder: ${err?.message ?? err}`) } } @@ -42,7 +44,7 @@ export class OscForwarder { try { this.port?.send(message) } catch (err: any) { - this.logger.warn(`OSC forward send failed: ${err?.message ?? err}`) + this.logger?.warn(`OSC forward send failed: ${err?.message ?? err}`) } } diff --git a/src/handlers/state-handler.ts b/src/handlers/state-handler.ts index f1a220b..48a94c9 100644 --- a/src/handlers/state-handler.ts +++ b/src/handlers/state-handler.ts @@ -1,10 +1,10 @@ import EventEmitter from 'events' -import { ModuleLogger } from './logger.js' import { ModelSpec } from '../models/types.js' import { WingState } from '../state/index.js' import PQueue from 'p-queue' import osc, { OscMessage } from 'osc' import debounceFn from 'debounce-fn' +import { ModuleLogger } from './logger.js' /** * Manages the internal state of the Behringer Wing, processes OSC messages, and emits state updates. @@ -12,8 +12,8 @@ import debounceFn from 'debounce-fn' */ export class StateHandler extends EventEmitter { private model: ModelSpec + private logger: ModuleLogger | undefined state?: WingState - private logger?: ModuleLogger private inFlightRequests: { [path: string]: () => void } = {} private readonly requestQueue: PQueue = new PQueue({ @@ -35,8 +35,8 @@ export class StateHandler extends EventEmitter { constructor(model: ModelSpec, logger?: ModuleLogger) { super() this.model = model - this.state = new WingState(model) this.logger = logger + this.state = new WingState(model) this.debounceUpdateCompanion = debounceFn(this.updateCompanionWithState.bind(this), { wait: 200, @@ -99,22 +99,27 @@ export class StateHandler extends EventEmitter { * Process an OSC message, update state, and emit events as needed. * @param msg OSC message to process. */ - processMessage(msg: OscMessage): void { - const { address, args } = msg - - const wasExpected = !!this.inFlightRequests[msg.address] - if (this.inFlightRequests[msg.address]) { - this.logger?.debug(`Received answer for request ${msg.address}`) - this.inFlightRequests[msg.address]() - delete this.inFlightRequests[msg.address] - } + processMessage(msgs: Set): void { + for (const msg of msgs) { + const { address, args } = msg + + const wasExpected = !!this.inFlightRequests[msg.address] + if (this.inFlightRequests[msg.address]) { + this.logger?.debug(`Received answer for request ${msg.address}`) + this.inFlightRequests[msg.address]() + delete this.inFlightRequests[msg.address] + } - this.state?.set(address, args as osc.MetaArgument[]) - this.logger?.debug(`State updated for ${address} with args: ${JSON.stringify(args)}`) + const value = args as osc.MetaArgument[] + this.state?.set(address, value) - const hadStructuralChange = this.updateLists(msg) - if (!wasExpected && hadStructuralChange) { - this.requestUpdate() + const stringValue = value[0].value + this.logger?.debug(`State updated for ${address}: ${stringValue}`) + + const hadStructuralChange = this.updateLists(msg) + if (!wasExpected && hadStructuralChange) { + this.requestUpdate() + } } } diff --git a/src/index.ts b/src/index.ts index ce2125a..5ba1196 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,11 @@ -import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField, Regex } from '@companion-module/base' +import { + InstanceBase, + runEntrypoint, + InstanceStatus, + SomeCompanionConfigField, + Regex, + CompanionVariableValues, +} from '@companion-module/base' import { InstanceBaseExt } from './types.js' import { GetConfigFields, WingConfig } from './config.js' import { UpgradeScripts } from './upgrades.js' @@ -11,31 +18,47 @@ import { ModelSpec, WingModel } from './models/types.js' import { getDeskModel } from './models/index.js' import { GetPresets } from './presets.js' import { ConnectionHandler } from './handlers/connection-handler.js' -import { ModuleLogger } from './handlers/logger.js' import { StateHandler } from './handlers/state-handler.js' import { FeedbackHandler } from './handlers/feedback-handler.js' import { VariableHandler } from './variables/variable-handler.js' import { OscForwarder } from './handlers/osc-forwarder.js' +import debounceFn from 'debounce-fn' +import { ModuleLogger } from './handlers/logger.js' export class WingInstance extends InstanceBase implements InstanceBaseExt { + private readonly debounceHandleMessages: () => void + private readonly messages = new Set() + config!: WingConfig model: ModelSpec connected: boolean = false deviceDetector: WingDeviceDetector | undefined - logger: ModuleLogger | undefined connection: ConnectionHandler | undefined stateHandler: StateHandler | undefined feedbackHandler: FeedbackHandler | undefined variableHandler: VariableHandler | undefined transitions: WingTransitions oscForwarder: OscForwarder | undefined + logger: ModuleLogger | undefined constructor(internal: unknown) { super(internal) this.model = getDeskModel(WingModel.Full) // later populated correctly this.transitions = new WingTransitions(this) + this.debounceHandleMessages = debounceFn( + () => { + this.handleMessages() + this.messages.clear() + }, + { + wait: 20, + maxWait: 100, + before: false, + after: true, + }, + ) } async init(config: WingConfig): Promise { @@ -43,9 +66,9 @@ export class WingInstance extends InstanceBase implements InstanceBa this.logger.setLoggerFn((level, message) => { this.log(level, message) }) + this.logger.disable() this.logger.debugMode = config.debugMode ?? false this.logger.timestamps = config.debugMode ?? false - await this.configUpdated(config) } @@ -65,11 +88,6 @@ export class WingInstance extends InstanceBase implements InstanceBa this.config = config this.model = getDeskModel(this.config.model) - if (config.debugMode === true) { - this.logger!.debugMode = true - this.logger!.timestamps = true - } - this.setupDeviceDetector() this.transitions.stopAll() @@ -129,12 +147,8 @@ export class WingInstance extends InstanceBase implements InstanceBa this.updateStatus(InstanceStatus.Connecting, 'waiting for response from console...') this.connection?.on('ready', () => { - this.updateStatus(InstanceStatus.Ok) - this.stateHandler?.state?.requestNames(this) - if (this.config.prefetchVariablesOnStartup) { - this.stateHandler?.state?.requestAllVariables(this) - } - this.stateHandler?.requestUpdate() + this.updateStatus(InstanceStatus.Connecting, 'Waiting for answer from console...') + void this.connection?.sendCommand('/*').catch(() => {}) }) this.connection?.on('error', (err: Error) => { @@ -149,20 +163,31 @@ export class WingInstance extends InstanceBase implements InstanceBa }) this.connection?.on('message', (msg: OscMessage) => { - if (this.connected == false) { - this.updateStatus(InstanceStatus.Ok) - this.connected = true + this.messages.add(msg) + this.oscForwarder?.send(msg) + this.debounceHandleMessages() + }) + } - this.logger?.info('OSC connection established') + private handleMessages(): void { + if (this.connected == false) { + this.updateStatus(InstanceStatus.Ok) + this.connected = true - this.feedbackHandler?.startPolling() + this.logger?.info('OSC connection established') + this.logger?.enable() + + this.feedbackHandler?.startPolling() + this.stateHandler?.state?.requestNames(this) + if (this.config.prefetchVariablesOnStartup) { + void this.stateHandler?.state?.requestAllVariables(this) } - this.feedbackHandler?.clearPollTimeout() - this.stateHandler?.processMessage(msg) - this.feedbackHandler?.processMessage(msg) - this.variableHandler?.processMessage(msg) - this.oscForwarder?.send(msg) - }) + this.stateHandler?.requestUpdate() + } + this.feedbackHandler?.clearPollTimeout() + this.stateHandler?.processMessage(this.messages) + this.feedbackHandler?.processMessage(this.messages) + this.variableHandler?.processMessage(this.messages) } private setupStateHandler(): void { @@ -209,18 +234,14 @@ export class WingInstance extends InstanceBase implements InstanceBa } private setupVariableHandler(): void { - this.variableHandler = new VariableHandler(this.model, this.config.variableUpdateRate, this.logger) + this.variableHandler = new VariableHandler(this.model, this.config.variableUpdateRate) this.variableHandler.on('create-variables', (variables) => { this.setVariableDefinitions(variables) }) - this.variableHandler.on('update-variable', (variable, value) => { - let rounded = Math.round((value + Number.EPSILON) * 10) / 10 - if (Number.isInteger(value)) { - rounded = Math.round(value) - } - this.setVariableValues({ [variable]: rounded }) + this.variableHandler.on('update-variables', (updates: CompanionVariableValues) => { + this.setVariableValues(updates) }) this.variableHandler.on('send', (cmd: string, arg?: number | string) => { @@ -231,7 +252,7 @@ export class WingInstance extends InstanceBase implements InstanceBa } private setupOscForwarder(): void { - if (!this.oscForwarder && this.logger) { + if (!this.oscForwarder) { this.oscForwarder = new OscForwarder(this.logger) } this.oscForwarder?.setup( diff --git a/src/state/state.ts b/src/state/state.ts index 44d29a5..1318360 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -5,6 +5,7 @@ import { DropdownChoice } from '@companion-module/base' import { ModelSpec } from '../models/types.js' import { getIdLabelPair } from '../choices/utils.js' import * as Commands from '../commands/index.js' +import { getAllVariables } from '../variables/index.js' type NameChoices = { channels: DropdownChoice[] @@ -205,7 +206,7 @@ export class WingState implements IStoredChannelSubject { public requestNames(self: WingInstance): void { const model = self.model - console.info('Requesting all names') + self.logger?.info('Requesting all names...') const sendCommand = self.connection!.sendCommand.bind(self.connection) for (let ch = 1; ch <= model.channels; ch++) { @@ -231,10 +232,28 @@ export class WingState implements IStoredChannelSubject { } } - public requestAllVariables(self: WingInstance): void { + public async requestAllVariables(self: WingInstance): Promise { const model = self.model const sendCommand = self.connection!.sendCommand.bind(self.connection) + const vars = getAllVariables(model) + + const chunkSize = 500 + const chunkWait = 50 + const chunks = Math.ceil(vars.length / chunkSize) + for (let c = 0; c < chunks; c++) { + const wait = c * chunkWait + const varChunk = vars.slice(c * chunkSize, (c + 1) * chunkSize) + setTimeout(() => { + for (const v of varChunk) { + const p = v.path + if (p === undefined) continue + void sendCommand(p, undefined, undefined, true) + } + }, wait) + } + // Separate Stuff + // TODO: eventually this should be unified in the main variable definitions // Desk/system status void sendCommand(Commands.Io.MainAltSwitch()) @@ -277,134 +296,6 @@ export class WingState implements IStoredChannelSubject { void sendCommand(Commands.Cards.WLiveCardSessionLength(card)) void sendCommand(Commands.Cards.WLiveCardSDFree(card)) } - - // GPIO states - for (let gpio = 1; gpio <= model.gpio; gpio++) { - void sendCommand(Commands.Control.GpioReadState(gpio)) - } - - // Talkback assigns (A and B) - for (let bus = 1; bus <= model.busses; bus++) { - void sendCommand(Commands.Configuration.TalkbackBusAssign('A', bus)) - void sendCommand(Commands.Configuration.TalkbackBusAssign('B', bus)) - } - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Configuration.TalkbackMatrixAssign('A', mtx)) - void sendCommand(Commands.Configuration.TalkbackMatrixAssign('B', mtx)) - } - for (let main = 1; main <= model.mains; main++) { - void sendCommand(Commands.Configuration.TalkbackMainAssign('A', main)) - void sendCommand(Commands.Configuration.TalkbackMainAssign('B', main)) - } - - // Names are requested elsewhere via state.requestNames - - // Channel strips - for (let ch = 1; ch <= model.channels; ch++) { - void sendCommand(Commands.Channel.InputGain(ch)) - void sendCommand(Commands.Channel.Color(ch)) - void sendCommand(Commands.Channel.Mute(ch)) - void sendCommand(Commands.Channel.Fader(ch)) - void sendCommand(Commands.Channel.Pan(ch)) - - for (let bus = 1; bus <= model.busses; bus++) { - void sendCommand(Commands.Channel.SendOn(ch, bus)) - void sendCommand(Commands.Channel.SendLevel(ch, bus)) - void sendCommand(Commands.Channel.SendPan(ch, bus)) - } - for (let main = 1; main <= model.mains; main++) { - void sendCommand(Commands.Channel.MainSendOn(ch, main)) - void sendCommand(Commands.Channel.MainSendLevel(ch, main)) - } - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Channel.MatrixSendOn(ch, mtx)) - void sendCommand(Commands.Channel.MatrixSendLevel(ch, mtx)) - void sendCommand(Commands.Channel.MatrixSendPan(ch, mtx)) - } - } - - // Auxes - for (let aux = 1; aux <= model.auxes; aux++) { - void sendCommand(Commands.Aux.InputGain(aux)) - void sendCommand(Commands.Aux.Color(aux)) - void sendCommand(Commands.Aux.Mute(aux)) - void sendCommand(Commands.Aux.Fader(aux)) - void sendCommand(Commands.Aux.Pan(aux)) - - for (let main = 1; main <= model.mains; main++) { - void sendCommand(Commands.Aux.MainSendOn(aux, main)) - void sendCommand(Commands.Aux.MainSendLevel(aux, main)) - } - for (let bus = 1; bus <= model.busses; bus++) { - void sendCommand(Commands.Aux.SendOn(aux, bus)) - void sendCommand(Commands.Aux.SendLevel(aux, bus)) - void sendCommand(Commands.Aux.SendPan(aux, bus)) - } - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Aux.MatrixSendOn(aux, mtx)) - void sendCommand(Commands.Aux.MatrixSendLevel(aux, mtx)) - void sendCommand(Commands.Aux.MatrixSendPan(aux, mtx)) - } - } - - // Busses - for (let bus = 1; bus <= model.busses; bus++) { - void sendCommand(Commands.Bus.Mute(bus)) - void sendCommand(Commands.Bus.Fader(bus)) - void sendCommand(Commands.Bus.Pan(bus)) - void sendCommand(Commands.Bus.Color(bus)) - - for (let main = 1; main <= model.mains; main++) { - void sendCommand(Commands.Bus.MainSendOn(bus, main)) - void sendCommand(Commands.Bus.MainSendLevel(bus, main)) - } - for (let other = 1; other <= model.busses; other++) { - if (other === bus) continue - void sendCommand(Commands.Bus.SendOn(bus, other)) - void sendCommand(Commands.Bus.SendLevel(bus, other)) - void sendCommand(Commands.Bus.SendPan(bus, other)) - } - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Bus.MatrixSendOn(bus, mtx)) - void sendCommand(Commands.Bus.MatrixSendLevel(bus, mtx)) - void sendCommand(Commands.Bus.MatrixSendPan(bus, mtx)) - } - } - - // Matrices - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Matrix.Mute(mtx)) - void sendCommand(Commands.Matrix.Fader(mtx)) - void sendCommand(Commands.Matrix.Pan(mtx)) - void sendCommand(Commands.Matrix.Color(mtx)) - } - - // Mains - for (let main = 1; main <= model.mains; main++) { - void sendCommand(Commands.Main.Mute(main)) - void sendCommand(Commands.Main.Fader(main)) - void sendCommand(Commands.Main.Pan(main)) - void sendCommand(Commands.Main.Color(main)) - - for (let mtx = 1; mtx <= model.matrices; mtx++) { - void sendCommand(Commands.Main.MatrixSendOn(main, mtx)) - void sendCommand(Commands.Main.MatrixSendLevel(main, mtx)) - void sendCommand(Commands.Main.MatrixSendPan(main, mtx)) - } - } - - // DCAs - for (let dca = 1; dca <= model.dcas; dca++) { - void sendCommand(Commands.Dca.Mute(dca)) - void sendCommand(Commands.Dca.Fader(dca)) - void sendCommand(Commands.Dca.Color(dca)) - } - - // Mute Groups - for (let mgrp = 1; mgrp <= model.mutegroups; mgrp++) { - // Mute group variables are handled via RE_MUTE - void sendCommand(Commands.MuteGroup.Mute(mgrp)) - } } public setStoredChannel(channel: number): void { diff --git a/src/types.ts b/src/types.ts index 5c71a41..689847b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,15 @@ import { InstanceBase } from '@companion-module/base' import { WingTransitions } from './handlers/transitions.js' import { ModelSpec } from './models/types.js' +import { ModuleLogger } from './handlers/logger.js' export interface InstanceBaseExt extends InstanceBase { config: TConfig transitions: WingTransitions // subscriptions: WingSubscriptions model: ModelSpec + logger?: ModuleLogger - logger?: import('./handlers/logger.js').ModuleLogger connection?: import('./handlers/connection-handler.js').ConnectionHandler | undefined stateHandler?: import('./handlers/state-handler.js').StateHandler | undefined feedbackHandler?: import('./handlers/feedback-handler.js').FeedbackHandler | undefined diff --git a/src/variables/auxiliary.ts b/src/variables/auxiliary.ts index 03ff539..e1a1eba 100644 --- a/src/variables/auxiliary.ts +++ b/src/variables/auxiliary.ts @@ -1,72 +1,87 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getAuxVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getAuxVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let aux = 1; aux <= model.auxes; aux++) { variables.push({ variableId: `aux${aux}_name`, name: `Aux ${aux} Name`, + path: Commands.Aux.Name(aux), }) variables.push({ variableId: `aux${aux}_gain`, name: `Aux ${aux} Gain`, + path: Commands.Aux.InputGain(aux), }) variables.push({ variableId: `aux${aux}_mute`, name: `Aux ${aux} Mute`, + path: Commands.Aux.Mute(aux), }) variables.push({ variableId: `aux${aux}_level`, name: `Aux ${aux} Level`, + path: Commands.Aux.Fader(aux), }) variables.push({ variableId: `aux${aux}_pan`, name: `Aux ${aux} Pan`, + path: Commands.Aux.Pan(aux), }) for (let main = 1; main <= model.mains; main++) { variables.push({ variableId: `aux${aux}_main${main}_mute`, name: `Aux ${aux} to Main ${main} Mute`, + path: Commands.Aux.MainSendOn(aux, main), }) variables.push({ variableId: `aux${aux}_main${main}_level`, name: `Aux ${aux} to Main ${main} Level`, + path: Commands.Aux.MainSendLevel(aux, main), }) } for (let bus = 1; bus <= model.busses; bus++) { variables.push({ variableId: `aux${aux}_bus${bus}_mute`, name: `Aux ${aux} to Bus ${bus} Mutes`, + path: Commands.Aux.SendOn(aux, bus), }) variables.push({ variableId: `aux${aux}_bus${bus}_level`, name: `Aux ${aux} to Bus ${bus} Level`, + path: Commands.Aux.SendLevel(aux, bus), }) variables.push({ variableId: `aux${aux}_bus${bus}_pan`, name: `Aux ${aux} to Bus ${bus} Pan`, + path: Commands.Aux.SendPan(aux, bus), }) } for (let mtx = 1; mtx <= model.matrices; mtx++) { variables.push({ variableId: `aux${aux}_mtx${mtx}_mute`, name: `Aux ${aux} to Matrix ${mtx} Mute`, + path: Commands.Aux.MatrixSendOn(aux, mtx), }) variables.push({ variableId: `aux${aux}_mtx${mtx}_level`, name: `Aux ${aux} to Matrix ${mtx} Level`, + path: Commands.Aux.MatrixSendLevel(aux, mtx), }) variables.push({ variableId: `aux${aux}_mtx${mtx}_pan`, name: `Aux ${aux} to Matrix ${mtx} Pan`, + path: Commands.Aux.MatrixSendPan(aux, mtx), }) } variables.push({ variableId: `aux${aux}_color`, name: `Aux ${aux} Color`, + path: Commands.Aux.Color(aux), }) } diff --git a/src/variables/bus.ts b/src/variables/bus.ts index 10e22a3..c4030b8 100644 --- a/src/variables/bus.ts +++ b/src/variables/bus.ts @@ -1,8 +1,8 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' -export function getBusVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getBusVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let bus = 1; bus <= model.busses; bus++) { variables.push({ diff --git a/src/variables/channel.ts b/src/variables/channel.ts index f7d8e4b..0f556ec 100644 --- a/src/variables/channel.ts +++ b/src/variables/channel.ts @@ -1,72 +1,87 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getChannelVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getChannelVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let ch = 1; ch <= model.channels; ch++) { variables.push({ variableId: `ch${ch}_name`, name: `Channel ${ch} Name`, + path: Commands.Channel.Name(ch), }) variables.push({ variableId: `ch${ch}_gain`, name: `Channel ${ch} Gain`, + path: Commands.Channel.InputGain(ch), }) variables.push({ variableId: `ch${ch}_mute`, name: `Channel ${ch} Mute`, + path: Commands.Channel.Mute(ch), }) variables.push({ variableId: `ch${ch}_level`, name: `Channel ${ch} Level`, + path: Commands.Channel.Fader(ch), }) variables.push({ variableId: `ch${ch}_pan`, name: `Channel ${ch} Pan`, + path: Commands.Channel.Pan(ch), }) for (let bus = 1; bus <= model.busses; bus++) { variables.push({ variableId: `ch${ch}_bus${bus}_mute`, name: `Channel ${ch} to Bus ${bus} Mute`, + path: Commands.Channel.SendOn(ch, bus), }) variables.push({ variableId: `ch${ch}_bus${bus}_level`, name: `Channel ${ch} to Bus ${bus} Level`, + path: Commands.Channel.SendLevel(ch, bus), }) variables.push({ variableId: `ch${ch}_bus${bus}_pan`, name: `Channel ${ch} to Bus ${bus} Pan`, + path: Commands.Channel.SendPan(ch, bus), }) } for (let main = 1; main <= model.mains; main++) { variables.push({ variableId: `ch${ch}_main${main}_mute`, name: `Channel ${ch} to Main ${main} Mute`, + path: Commands.Channel.MainSendOn(ch, main), }) variables.push({ variableId: `ch${ch}_main${main}_level`, name: `Channel ${ch} to Main ${main} Level`, + path: Commands.Channel.MainSendLevel(ch, main), }) } for (let mtx = 1; mtx <= model.matrices; mtx++) { variables.push({ variableId: `ch${ch}_mtx${mtx}_mute`, name: `Channel ${ch} to Matrix ${mtx} Mute`, + path: Commands.Channel.MatrixSendOn(ch, mtx), }) variables.push({ variableId: `ch${ch}_mtx${mtx}_level`, name: `Channel ${ch} to Matrix ${mtx} Level`, + path: Commands.Channel.MatrixSendLevel(ch, mtx), }) variables.push({ variableId: `ch${ch}_mtx${mtx}_pan`, name: `Channel ${ch} to Matrix ${mtx} Pan`, + path: Commands.Channel.MatrixSendPan(ch, mtx), }) } variables.push({ variableId: `ch${ch}_color`, name: `Channel ${ch} Color`, + path: Commands.Channel.Color(ch), }) } diff --git a/src/variables/dca.ts b/src/variables/dca.ts index b1c6d77..93b149c 100644 --- a/src/variables/dca.ts +++ b/src/variables/dca.ts @@ -1,26 +1,31 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getDcaVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getDcaVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let dca = 1; dca <= model.dcas; dca++) { variables.push({ variableId: `dca${dca}_name`, name: `DCA ${dca} Name`, + path: Commands.Dca.Name(dca), }) variables.push({ variableId: `dca${dca}_mute`, name: `DCA ${dca} Mute`, + path: Commands.Dca.Mute(dca), }) variables.push({ variableId: `dca${dca}_level`, name: `DCA ${dca} Level`, + path: Commands.Dca.Fader(dca), }) variables.push({ variableId: `dca${dca}_color`, name: `DCA ${dca} Color`, + path: Commands.Dca.Color(dca), }) } diff --git a/src/variables/gpio.ts b/src/variables/gpio.ts index b744f80..d1f0732 100644 --- a/src/variables/gpio.ts +++ b/src/variables/gpio.ts @@ -1,11 +1,16 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getGpioVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getGpioVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let gpio = 1; gpio <= model.gpio; gpio++) { - variables.push({ variableId: `gpio${gpio}`, name: `GPIO ${gpio} state (true = pressed/connected)` }) + variables.push({ + variableId: `gpio${gpio}`, + name: `GPIO ${gpio} state (true = pressed/connected)`, + path: Commands.Control.GpioState(gpio), + }) } return variables diff --git a/src/variables/index.ts b/src/variables/index.ts index c70c310..c18febc 100644 --- a/src/variables/index.ts +++ b/src/variables/index.ts @@ -1,4 +1,41 @@ +import { ModelSpec } from '../models/types.js' +import { getAuxVariables } from './auxiliary.js' +import { getBusVariables } from './bus.js' +import { getChannelVariables } from './channel.js' +import { getDcaVariables } from './dca.js' +import { getGpioVariables } from './gpio.js' +import { getMainVariables } from './main.js' +import { getMatrixVariables } from './matrix.js' +import { getMuteGroupVariables } from './mutegroup.js' +import { getShowControlVariables } from './showcontrol.js' +import { getTalkbackVariables } from './talkback.js' +import { getUsbVariables } from './usb.js' +import { getWliveVariables } from './wlive.js' + export interface VariableDefinition { variableId: string name: string + path?: string +} + +export function getAllVariables(model: ModelSpec): VariableDefinition[] { + const variables = [] + variables.push({ variableId: 'desk_ip', name: 'Desk IP Address' }) + variables.push({ variableId: 'desk_name', name: 'Desk Name' }) + variables.push({ variableId: 'main_alt_status', name: 'Main/Alt Input Source' }) + + variables.push(...getChannelVariables(model)) + variables.push(...getAuxVariables(model)) + variables.push(...getBusVariables(model)) + variables.push(...getMatrixVariables(model)) + variables.push(...getMainVariables(model)) + variables.push(...getDcaVariables(model)) + variables.push(...getMuteGroupVariables(model)) + variables.push(...getUsbVariables()) + variables.push(...getWliveVariables()) + variables.push(...getShowControlVariables()) + variables.push(...getGpioVariables(model)) + variables.push(...getTalkbackVariables(model)) + + return variables } diff --git a/src/variables/main.ts b/src/variables/main.ts index 3840554..7b8ad3c 100644 --- a/src/variables/main.ts +++ b/src/variables/main.ts @@ -1,40 +1,48 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getMainVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getMainVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let main = 1; main <= model.mains; main++) { variables.push({ variableId: `main${main}_name`, name: `Main ${main} Name`, + path: Commands.Main.Name(main), }) variables.push({ variableId: `main${main}_mute`, name: `Main ${main} Mute`, + path: Commands.Main.Mute(main), }) variables.push({ variableId: `main${main}_level`, name: `Main ${main} Level`, + path: Commands.Main.Fader(main), }) variables.push({ variableId: `main${main}_pan`, name: `Main ${main} Pan`, + path: Commands.Main.Pan(main), }) for (let send = 1; send <= model.matrices; send++) { variables.push({ variableId: `main${main}_mtx${send}_mute`, name: `Main ${main} to Matrix ${send} Mute`, + path: Commands.Main.MatrixSendOn(main, send), }) variables.push({ variableId: `main${main}_mtx${send}_level`, name: `Main ${main} to Matrix ${send} Level`, + path: Commands.Main.MatrixSendOn(main, send), }) } variables.push({ variableId: `main${main}_color`, name: `Main ${main} Color`, + path: Commands.Main.Color(main), }) } diff --git a/src/variables/matrix.ts b/src/variables/matrix.ts index cdafbc5..8fe2c7c 100644 --- a/src/variables/matrix.ts +++ b/src/variables/matrix.ts @@ -1,30 +1,36 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getMatrixVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getMatrixVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let mtx = 1; mtx <= model.matrices; mtx++) { variables.push({ variableId: `mtx${mtx}_name`, name: `Matrix ${mtx} Name`, + path: Commands.Matrix.Name(mtx), }) variables.push({ variableId: `mtx${mtx}_mute`, name: `Matrix ${mtx} Mute`, + path: Commands.Matrix.Mute(mtx), }) variables.push({ variableId: `mtx${mtx}_level`, name: `Matrix ${mtx} Level`, + path: Commands.Matrix.Fader(mtx), }) variables.push({ variableId: `mtx${mtx}_pan`, name: `Matrix ${mtx} Pan`, + path: Commands.Matrix.Pan(mtx), }) variables.push({ variableId: `mtx${mtx}_color`, name: `Matrix ${mtx} Color`, + path: Commands.Matrix.Color(mtx), }) } diff --git a/src/variables/mutegroup.ts b/src/variables/mutegroup.ts index f5e0795..6848b76 100644 --- a/src/variables/mutegroup.ts +++ b/src/variables/mutegroup.ts @@ -1,17 +1,20 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getMuteGroupVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getMuteGroupVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] for (let mgrp = 1; mgrp <= model.mutegroups; mgrp++) { variables.push({ variableId: `mgrp${mgrp}_name`, name: `Mutegroup ${mgrp} Name`, + path: Commands.MuteGroup.Name(mgrp), }) variables.push({ variableId: `mgrp${mgrp}_mute`, name: `Mutegroup ${mgrp} Mute`, + path: Commands.MuteGroup.Mute(mgrp), }) } diff --git a/src/variables/showcontrol.ts b/src/variables/showcontrol.ts index 98e308a..7b1b757 100644 --- a/src/variables/showcontrol.ts +++ b/src/variables/showcontrol.ts @@ -1,7 +1,7 @@ -import { CompanionVariableDefinition } from '@companion-module/base' +import { VariableDefinition } from './index.js' -export function getShowControlVariables(): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getShowControlVariables(): VariableDefinition[] { + const variables: VariableDefinition[] = [] variables.push({ variableId: 'active_show_name', name: 'Active Show Name' }) variables.push({ variableId: 'previous_scene_number', name: 'Previous Scene Number' }) diff --git a/src/variables/talkback.ts b/src/variables/talkback.ts index ef66ff3..fde15fa 100644 --- a/src/variables/talkback.ts +++ b/src/variables/talkback.ts @@ -1,21 +1,37 @@ -import { CompanionVariableDefinition } from '@companion-module/base' import { ModelSpec } from '../models/types.js' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getTalkbackVariables(model: ModelSpec): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getTalkbackVariables(model: ModelSpec): VariableDefinition[] { + const variables: VariableDefinition[] = [] - for (let bus = 1; bus <= model.busses; bus++) { - variables.push({ variableId: `talkback_a_bus${bus}_assign`, name: `Talkback A to Bus ${bus} assign` }) - variables.push({ variableId: `talkback_b_bus${bus}_assign`, name: `Talkback B to Bus ${bus} assign` }) - } - for (let mtx = 1; mtx <= model.matrices; mtx++) { - variables.push({ variableId: `talkback_a_mtx${mtx}_assign`, name: `Talkback A to Matrix ${mtx} assign` }) - variables.push({ variableId: `talkback_b_mtx${mtx}_assign`, name: `Talkback B to Matrix ${mtx} assign` }) - } - for (let main = 1; main <= model.mains; main++) { - variables.push({ variableId: `talkback_a_main${main}_assign`, name: `Talkback A to Main ${main} assign` }) - variables.push({ variableId: `talkback_b_main${main}_assign`, name: `Talkback B to Main ${main} assign` }) - } + const tbs = ['A', 'B'] + + tbs.map((tb) => { + const upper = tb.toUpperCase() + const lower = tb.toLowerCase() + for (let bus = 1; bus <= model.busses; bus++) { + variables.push({ + variableId: `talkback_${lower}_bus${bus}_assign`, + name: `Talkback ${upper} to Bus ${bus} assign`, + path: Commands.Configuration.TalkbackBusAssign(upper, bus), + }) + } + for (let mtx = 1; mtx <= model.matrices; mtx++) { + variables.push({ + variableId: `talkback_${lower}_mtx${mtx}_assign`, + name: `Talkback ${upper} to Matrix ${mtx} assign`, + path: Commands.Configuration.TalkbackMatrixAssign(upper, mtx), + }) + } + for (let main = 1; main <= model.mains; main++) { + variables.push({ + variableId: `talkback_${lower}_main${main}_assign`, + name: `Talkback ${upper} to Matrix ${main} assign`, + path: Commands.Configuration.TalkbackMainAssign(upper, main), + }) + } + }) return variables } diff --git a/src/variables/usb.ts b/src/variables/usb.ts index f3d33fe..fe067db 100644 --- a/src/variables/usb.ts +++ b/src/variables/usb.ts @@ -1,7 +1,7 @@ -import { CompanionVariableDefinition } from '@companion-module/base' +import { VariableDefinition } from './index.js' -export function getUsbVariables(): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getUsbVariables(): VariableDefinition[] { + const variables: VariableDefinition[] = [] variables.push({ variableId: 'usb_record_time_ss', name: 'USB Record Time (ss)' }) variables.push({ variableId: 'usb_record_time_mm_ss', name: 'USB Record Time (mm:ss)' }) diff --git a/src/variables/variable-handler.ts b/src/variables/variable-handler.ts index 4edc2e4..0dfab56 100644 --- a/src/variables/variable-handler.ts +++ b/src/variables/variable-handler.ts @@ -1,22 +1,12 @@ import EventEmitter from 'events' -import { ModuleLogger } from '../handlers/logger.js' import { ModelSpec } from '../models/types.js' -import { getChannelVariables } from './channel.js' -import { getAuxVariables } from './auxiliary.js' -import { getMatrixVariables } from './matrix.js' -import { getBusVariables } from './bus.js' -import { getMainVariables } from './main.js' -import { getDcaVariables } from './dca.js' -import { getMuteGroupVariables } from './mutegroup.js' -import { getUsbVariables } from './usb.js' -import { getWliveVariables } from './wlive.js' -import { getShowControlVariables } from './showcontrol.js' -import { getGpioVariables } from './gpio.js' -import { getTalkbackVariables } from './talkback.js' import osc, { OscMessage } from 'osc' -import { CompanionVariableDefinition, OSCMetaArgument } from '@companion-module/base' +import { CompanionVariableDefinition, CompanionVariableValues, OSCMetaArgument } from '@companion-module/base' import * as ActionUtil from '../actions/utils.js' import { IoCommands } from '../commands/io.js' +import { getAllVariables } from './index.js' +import debounceFn from 'debounce-fn' +import { ModuleLogger } from '../handlers/logger.js' const RE_NAME = /\/(\w+)\/(\d+)\/\$?name/ const RE_GAIN = /\/(\w+)\/(\d+)\/in\/set\/\$g/ @@ -30,11 +20,12 @@ const RE_GPIO = /^\/\$ctl\/gpio\/(\d+)\/\$state$/ const RE_CONTROL = /^\/\$ctl\/(lib|\$stat)\/(\$?\w+)/ const RE_COLOR = /\/(\w+)\/(\d+)\/\$?col/ +export type VariableUpdate = { name: string; value: string | number } export class VariableHandler extends EventEmitter { private model: ModelSpec - private variableUpdateInterval: NodeJS.Timeout | undefined - private logger?: ModuleLogger - private messages: { [path: string]: OscMessage } = {} + private readonly messages = new Set() + private readonly debounceUpdateVariables: () => void + private logger: ModuleLogger | undefined private variables: CompanionVariableDefinition[] = [] @@ -42,58 +33,72 @@ export class VariableHandler extends EventEmitter { super() this.model = model this.logger = logger - this.variableUpdateInterval = setInterval(() => { - if (Object.keys(this.messages).length == 0) { - return - } - this.updateVariables() - }, updateRate ?? 1000) + + this.debounceUpdateVariables = debounceFn( + () => { + this.updateVariables() + this.messages.clear() + }, + { + wait: updateRate ?? 1000, + before: false, + after: true, + }, + ) } setupVariables(): void { this.logger?.info('Setting up variables') + const vars = getAllVariables(this.model) this.variables.push({ variableId: 'desk_ip', name: 'Desk IP Address' }) this.variables.push({ variableId: 'desk_name', name: 'Desk Name' }) this.variables.push({ variableId: 'main_alt_status', name: 'Main/Alt Input Source' }) - this.variables.push(...getChannelVariables(this.model)) - this.variables.push(...getAuxVariables(this.model)) - this.variables.push(...getBusVariables(this.model)) - this.variables.push(...getMatrixVariables(this.model)) - this.variables.push(...getMainVariables(this.model)) - this.variables.push(...getDcaVariables(this.model)) - this.variables.push(...getMuteGroupVariables(this.model)) - this.variables.push(...getUsbVariables()) - this.variables.push(...getWliveVariables()) - this.variables.push(...getShowControlVariables()) - this.variables.push(...getGpioVariables(this.model)) - this.variables.push(...getTalkbackVariables(this.model)) + this.variables.push(...vars.map((v) => ({ variableId: v.variableId, name: v.name }))) this.logger?.info(`Defined ${this.variables.length} variables`) this.emit('create-variables', this.variables) } updateVariables(): void { - for (const message of Object.values(this.messages)) { + const messages = [...this.messages] + if (messages.length === 0) { + return + } + + const updates: VariableUpdate[] = [] + for (const message of messages) { const path = message.address const args = message.args as osc.MetaArgument[] - this.updateNameVariables(path, args[0]?.value as string) - this.updateGainVariables(path, args[0]?.value as number) - this.updateMuteVariables(path, args[0]?.value as number) - this.updateFaderVariables(path, args[0]?.value as number) - this.updatePanoramaVariables(path, args[0]?.value as number) - this.updateUsbVariables(path, args[0]) - this.updateSdVariables(path, args[0]) - this.updateTalkbackVariables(path, args[0]) - this.updateGpioVariables(path, args[0]?.value as number) - this.updateControlVariables(path, args[0]) - this.updateIoVariables(path, args[0]) - this.updateColorVariables(path, args[0]?.value as string) + + const result = + this.updateNameVariables(path, args[0]?.value as string) ?? + this.updateGainVariables(path, args[0]?.value as number) ?? + this.updateMuteVariables(path, args[0]?.value as number) ?? + this.updateFaderVariables(path, args[0]?.value as number) ?? + this.updatePanoramaVariables(path, args[0]?.value as number) ?? + this.updateUsbVariables(path, args[0]) ?? + this.updateSdVariables(path, args[0]) ?? + this.updateTalkbackVariables(path, args[0]) ?? + this.updateGpioVariables(path, args[0]?.value as number) ?? + this.updateControlVariables(path, args[0]) ?? + this.updateIoVariables(path, args[0]) ?? + this.updateColorVariables(path, args[0]?.value as string) + + if (result) { + updates.push(...result) + } } + const variables: CompanionVariableValues = {} + for (const { name, value } of updates) { + if (name === undefined || value === undefined) continue + variables[name] = value + } + this.emit('update-variables', variables) } - private updateNameVariables(path: string, value: string): void { + private updateNameVariables(path: string, value: string): VariableUpdate[] | undefined { const match = path.match(RE_NAME) if (!match) { return @@ -101,10 +106,10 @@ export class VariableHandler extends EventEmitter { const base = match[1] const num = match[2] - this.emit('update-variable', `${base}${num}_name`, value) + return [{ name: `${base}${num}_name`, value }] } - private updateGainVariables(path: string, value: number): void { + private updateGainVariables(path: string, value: number): VariableUpdate[] | undefined { const match = path.match(RE_GAIN) if (!match) { return @@ -113,10 +118,10 @@ export class VariableHandler extends EventEmitter { const base = match[1] const num = match[2] value = this.round(value, 1) - this.emit('update-variable', `${base}${num}_gain`, value) + return [{ name: `${base}${num}_gain`, value }] } - private updateMuteVariables(path: string, value: number): void { + private updateMuteVariables(path: string, value: number): VariableUpdate[] | undefined { const match = path.match(RE_MUTE) if (!match) return @@ -140,7 +145,7 @@ export class VariableHandler extends EventEmitter { if (dest == null) { const varName = `${source}${srcnum}_mute` - this.emit('update-variable', varName, value) + return [{ name: varName, value }] } else { if (dest === 'send') { dest = 'bus' @@ -148,11 +153,11 @@ export class VariableHandler extends EventEmitter { dest = 'mtx' } const varName = `${source}${srcnum}_${dest}${destnum}_mute` - this.emit('update-variable', varName, value) + return [{ name: varName, value }] } } - private updateFaderVariables(path: string, value: number): void { + private updateFaderVariables(path: string, value: number): VariableUpdate[] | undefined { const match = path.match(RE_FADER) if (!match) { @@ -180,13 +185,13 @@ export class VariableHandler extends EventEmitter { value = this.round(value, 1) if (value > -140) { - this.emit('update-variable', varName, value) + return [{ name: varName, value }] } else { - this.emit('update-variable', varName, '-oo') + return [{ name: varName, value: '-oo' }] } } - private updatePanoramaVariables(path: string, value: number): void { + private updatePanoramaVariables(path: string, value: number): VariableUpdate[] | undefined { const match = path.match(RE_PAN) if (!match) { @@ -213,10 +218,10 @@ export class VariableHandler extends EventEmitter { varName = `${source}_pan` } value = this.round(value, 0) - this.emit('update-variable', varName, Math.round(value)) + return [{ name: varName, value: Math.round(value) }] } - private updateUsbVariables(path: string, args: OSCMetaArgument): void { + private updateUsbVariables(path: string, args: OSCMetaArgument): VariableUpdate[] | undefined { const match = path.match(RE_USB) if (!match) { return @@ -238,15 +243,17 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', 'usb_record_time_ss', totalSeconds) - this.emit('update-variable', 'usb_record_time_mm_ss', `${totalMinutes}:${remainderSeconds}`) - this.emit('update-variable', 'usb_record_time_hh_mm_ss', `${hours}:${minutesWithinHour}:${secondsWithinMinute}`) + return [ + { name: 'usb_record_time_ss', value: totalSeconds }, + { name: 'usb_record_time_mm_ss', value: `${totalMinutes}:${remainderSeconds}` }, + { name: 'usb_record_time_hh_mm_ss', value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}` }, + ] } else if (command == '$actfile') { const filename = args.value as string - this.emit('update-variable', 'usb_record_path', filename) + return [{ name: 'usb_record_path', value: filename }] } else if (command == '$actstate') { const state = args.value as string - this.emit('update-variable', 'usb_record_state', state) + return [{ name: 'usb_record_state', value: state }] } } else if (direction == 'play') { if (command == '$pos') { @@ -263,9 +270,11 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', 'usb_play_pos_ss', totalSeconds) - this.emit('update-variable', 'usb_play_pos_mm_ss', `${totalMinutes}:${remainderSeconds}`) - this.emit('update-variable', 'usb_play_pos_hh_mm_ss', `${hours}:${minutesWithinHour}:${secondsWithinMinute}`) + return [ + { name: 'usb_play_pos_ss', value: totalSeconds }, + { name: 'usb_play_pos_mm_ss', value: `${totalMinutes}:${remainderSeconds}` }, + { name: 'usb_play_pos_hh_mm_ss', value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}` }, + ] } else if (command == '$total') { const seconds = args.value as number const totalSeconds = seconds.toString() @@ -280,40 +289,42 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', 'usb_play_total_ss', totalSeconds) - this.emit('update-variable', 'usb_play_total_mm_ss', `${totalMinutes}:${remainderSeconds}`) - this.emit('update-variable', 'usb_play_total_hh_mm_ss', `${hours}:${minutesWithinHour}:${secondsWithinMinute}`) + return [ + { name: 'usb_play_total_ss', value: totalSeconds }, + { name: 'usb_play_total_mm_ss', value: `${totalMinutes}:${remainderSeconds}` }, + { name: 'usb_play_total_hh_mm_ss', value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}` }, + ] } else if (command == '$actfile') { const filename = args.value as string - this.emit('update-variable', 'usb_play_path', filename) + return [{ name: 'usb_play_path', value: filename }] } else if (command == '$actstate') { const state = args.value as string - this.emit('update-variable', 'usb_play_state', state) + return [{ name: 'usb_play_state', value: state }] } else if (command == '$song') { const song = args.value as string - this.emit('update-variable', 'usb_play_name', song) + return [{ name: 'usb_play_name', value: song }] } else if (command == '$album') { const album = args.value as string - this.emit('update-variable', 'usb_play_directory', album) + return [{ name: 'usb_play_directory', value: album }] } else if (command == '$actlist') { const playlist = args.value as string - this.emit('update-variable', 'usb_play_playlist', playlist) + return [{ name: 'usb_play_playlist', value: playlist }] } else if (command == '$actidx') { const index = args.value as number - this.emit('update-variable', 'usb_play_playlist_index', index) + return [{ name: 'usb_play_playlist_index', value: index }] } else if (command == 'repeat') { const repeat = args.value as number - this.emit('update-variable', 'usb_play_repeat', repeat) + return [{ name: 'usb_play_repeat', value: repeat }] } } + return } - private updateSdVariables(path: string, args: OSCMetaArgument): void { + private updateSdVariables(path: string, args: OSCMetaArgument): VariableUpdate[] | undefined { // Check for SD link status first if (path === '/cards/wlive/$actlink' || path === '/cards/wlive/sdlink') { const linkStatus = args.value as string - this.emit('update-variable', 'wlive_link_status', linkStatus) - return + return [{ name: 'wlive_link_status', value: linkStatus }] } const match = path.match(RE_SD) @@ -331,28 +342,28 @@ export class VariableHandler extends EventEmitter { if (state == 'PPAUSE') { state = 'PAUSE' } - this.emit('update-variable', `wlive_${card}_state`, state) + return [{ name: `wlive_${card}_state`, value: state }] } else if (subcommand == 'sdstate') { const state = args.value as string - this.emit('update-variable', `wlive_${card}_sdstate`, state) + return [{ name: `wlive_${card}_sdstate`, value: state }] } else if (subcommand == 'sdsize') { const state = args.value as number - this.emit('update-variable', `wlive_${card}_sdsize`, state) + return [{ name: `wlive_${card}_sdsize`, value: state }] } else if (subcommand == 'markers') { const state = args.value as number - this.emit('update-variable', `wlive_${card}_marker_total`, state) + return [{ name: `wlive_${card}_marker_total`, value: state }] } else if (subcommand == 'markerpos') { const state = args.value as number - this.emit('update-variable', `wlive_${card}_marker_current`, state) + return [{ name: `wlive_${card}_marker_current`, value: state }] } else if (subcommand == 'sessions') { const state = args.value as number - this.emit('update-variable', `wlive_${card}_session_total`, state) + return [{ name: `wlive_${card}_session_total`, value: state }] } else if (subcommand == 'sessionpos') { const state = args.value as number - this.emit('update-variable', `wlive_${card}_session_current`, state) + return [{ name: `wlive_${card}_session_current`, value: state }] } else if (subcommand == 'markerlist') { const state = args.value as string - this.emit('update-variable', `wlive_${card}_marker_time`, state) + return [{ name: `wlive_${card}_marker_time`, value: state }] } else if (subcommand == 'etime') { const seconds = Math.floor((args.value as number) / 1000) const totalSeconds = seconds.toString() @@ -367,13 +378,14 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', `wlive_${card}_session_len_ss`, totalSeconds) - this.emit('update-variable', `wlive_${card}_session_len_mm_ss`, `${totalMinutes}:${remainderSeconds}`) - this.emit( - 'update-variable', - `wlive_${card}_session_len_hh_mm_ss`, - `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, - ) + return [ + { name: `wlive_${card}_elapsed_time_ss`, value: totalSeconds }, + { name: `wlive_${card}_elapsed_time_mm_ss`, value: `${totalMinutes}:${remainderSeconds}` }, + { + name: `wlive_${card}_elapsed_time_hh_mm_ss`, + value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, + }, + ] } else if (subcommand == 'sessionlen') { const seconds = Math.floor((args.value as number) / 1000) const totalSeconds = seconds.toString() @@ -388,13 +400,14 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', `wlive_${card}_session_len_ss`, totalSeconds) - this.emit('update-variable', `wlive_${card}_session_len_mm_ss`, `${totalMinutes}:${remainderSeconds}`) - this.emit( - 'update-variable', - `wlive_${card}_session_len_hh_mm_ss`, - `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, - ) + return [ + { name: `wlive_${card}_session_len_ss`, value: totalSeconds }, + { name: `wlive_${card}_session_len_mm_ss`, value: `${totalMinutes}:${remainderSeconds}` }, + { + name: `wlive_${card}_session_len_hh_mm_ss`, + value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, + }, + ] } else if (subcommand == 'sdfree') { const seconds = Math.floor((args.value as number) / 1000) const totalSeconds = seconds.toString() @@ -409,18 +422,21 @@ export class VariableHandler extends EventEmitter { .toString() .padStart(2, '0') const secondsWithinMinute = (seconds % 60).toString().padStart(2, '0') - this.emit('update-variable', `wlive_${card}_sdfree_ss`, totalSeconds) - this.emit('update-variable', `wlive_${card}_sdfree_mm_ss`, `${totalMinutes}:${remainderSeconds}`) - this.emit( - 'update-variable', - `wlive_${card}_sdfree_hh_mm_ss`, - `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, - ) + return [ + { name: `wlive_${card}_sdfree_ss`, value: totalSeconds }, + { name: `wlive_${card}_sdfree_mm_ss`, value: `${totalMinutes}:${remainderSeconds}` }, + { + name: `wlive_${card}_sdfree_hh_mm_ss`, + value: `${hours}:${minutesWithinHour}:${secondsWithinMinute}`, + }, + ] } + return } + return } - private updateTalkbackVariables(path: string, args: OSCMetaArgument): void { + private updateTalkbackVariables(path: string, args: OSCMetaArgument): VariableUpdate[] | undefined { const match = path.match(RE_TALKBACK) if (!match) { return @@ -439,20 +455,20 @@ export class VariableHandler extends EventEmitter { } const num = match[3] - this.emit('update-variable', `talkback_${talkback}_${destination}${num}_assign`, args.value as number) + return [{ name: `talkback_${talkback}_${destination}${num}_assign`, value: args.value as number }] } - private updateGpioVariables(path: string, value: number): void { + private updateGpioVariables(path: string, value: number): VariableUpdate[] | undefined { const match = path.match(RE_GPIO) if (!match) { return } const gpio = match[1] - this.emit('update-variable', `gpio${gpio}`, !value) + return [{ name: `gpio${gpio}`, value }] } - private updateControlVariables(path: string, args: OSCMetaArgument): void { + private updateControlVariables(path: string, args: OSCMetaArgument): VariableUpdate[] | undefined { const pathMatch = path.match(RE_CONTROL) if (!pathMatch) return @@ -463,30 +479,35 @@ export class VariableHandler extends EventEmitter { const fullShowPath = String(args.value) const showMatch = fullShowPath.match(/([^/\\]+)(?=\.show$)/) const showname = showMatch?.[1] ?? 'N/A' - this.emit('update-variable', 'active_show_name', showname) + return [ + { name: 'active_show_path', value: fullShowPath }, + { name: 'active_show_name', value: showname }, + ] } else if (subcommand === '$actidx') { const index = Number(args.value) - this.emit('update-variable', 'active_scene_number', index) - this.updateShowControlVariables(index) + return [{ name: 'active_show_index', value: index }, ...this.updateShowControlVariables(index)] } else if (subcommand === '$active') { const fullScenePath = String(args.value) const sceneMatch = fullScenePath.match(/([^/\\]+)[/\\]([^/\\]+)\..*$/) const scene = sceneMatch?.[2] ?? 'N/A' const parent = sceneMatch?.[1] ?? 'N/A' - - this.emit('update-variable', 'active_scene_name', scene) - this.emit('update-variable', 'active_scene_folder', parent) + return [ + { name: 'active_scene_name', value: scene }, + { name: 'active_scene_folder', value: parent }, + ] } + return } else if (command === '$stat') { if (subcommand === 'sof') { let index = Number(args.value) - // is arg a string or number? if (args.type === 'i') { // recieved an int, which start at 0 instead of -1 index = index - 1 } - this.emit('update-variable', 'sof_mode_index', index) - this.emit('update-variable', 'sof_mode_string', ActionUtil.getStringFromStripIndex(index)) + return [ + { name: 'sof_mode_index', value: index }, + { name: 'sof_mode_string', value: ActionUtil.getStringFromStripIndex(index) }, + ] } else if (subcommand === 'selidx') { let index = Number(args.value) // is arg a string or number? @@ -494,21 +515,27 @@ export class VariableHandler extends EventEmitter { // recieved an int, which start at 0 instead of 1 index = index + 1 } - this.emit('update-variable', 'sel_index', index) - this.emit('update-variable', 'sel_string', ActionUtil.getStringFromStripIndex(index)) + return [ + { name: 'sel_index', value: index }, + { name: 'sel_string', value: ActionUtil.getStringFromStripIndex(index) }, + ] } + return } + return } - private updateShowControlVariables(index: number): void { + private updateShowControlVariables(index: number): VariableUpdate[] { // const nameMap = self.state.sceneNameToIdMap const previous_number = index > 0 ? index - 1 : index const next_number = index + 1 + return [ + { name: 'previous_scene_number', value: previous_number }, + { name: 'active_scene_number', value: index }, + { name: 'next_scene_number', value: next_number }, + ] // const next_number = index < nameMap.size - 1 ? index + 1 : index - this.emit('update-variable', 'previous_scene_number', previous_number) - this.emit('update-variable', 'active_scene_number', index) - this.emit('update-variable', 'next_scene_number', next_number) // TODO: find a way to re-implement this nicely // function getKeyByValue(map: Map, value: number): string | undefined { @@ -526,7 +553,7 @@ export class VariableHandler extends EventEmitter { // this.emit('update-variable', 'next_scene_name', nextName as string) } - private updateIoVariables(path: string, arg: OSCMetaArgument): void { + private updateIoVariables(path: string, arg: OSCMetaArgument): VariableUpdate[] | undefined { const altsw = IoCommands.MainAltSwitch() if (path !== altsw) return @@ -547,10 +574,10 @@ export class VariableHandler extends EventEmitter { } } - this.emit('update-variable', 'main_alt_status', isMain ? 'Main' : 'Alt') + return [{ name: 'main_alt_status', value: isMain ? 'Main' : 'Alt' }] } - private updateColorVariables(path: string, value: string): void { + private updateColorVariables(path: string, value: string): VariableUpdate[] | undefined { const match = path.match(RE_COLOR) if (!match) { return @@ -558,19 +585,15 @@ export class VariableHandler extends EventEmitter { const base = match[1] const num = match[2] - this.emit('update-variable', `${base}${num}_color`, value) + return [{ name: `${base}${num}_color`, value }] } - processMessage(msg: OscMessage): void { - this.messages[msg.address] = msg + processMessage(msgs: Set): void { + msgs.forEach((msg) => this.messages.add(msg)) + this.debounceUpdateVariables() } - destroy(): void { - if (this.variableUpdateInterval) { - clearInterval(this.variableUpdateInterval) - this.variableUpdateInterval = undefined - } - } + destroy(): void {} round(num: number, precision: number): number { return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision) diff --git a/src/variables/wlive.ts b/src/variables/wlive.ts index 6e2b5c7..5d59138 100644 --- a/src/variables/wlive.ts +++ b/src/variables/wlive.ts @@ -1,14 +1,27 @@ -import { CompanionVariableDefinition } from '@companion-module/base' +import { VariableDefinition } from './index.js' +import * as Commands from '../commands/index.js' -export function getWliveVariables(): CompanionVariableDefinition[] { - const variables: CompanionVariableDefinition[] = [] +export function getWliveVariables(): VariableDefinition[] { + const variables: VariableDefinition[] = [] variables.push({ variableId: 'wlive_link_status', name: 'Wing Live SD Cards Link Status' }) for (let card = 1; card <= 2; card++) { - variables.push({ variableId: `wlive_${card}_state`, name: `Wing Live Card ${card} State` }) - variables.push({ variableId: `wlive_${card}_sdstate`, name: `Wing Live Card ${card} SD State` }) - variables.push({ variableId: `wlive_${card}_sdsize`, name: `Wing Live Card ${card} SD Size (GB)` }) + variables.push({ + variableId: `wlive_${card}_state`, + name: `Wing Live Card ${card} State`, + path: Commands.Cards.WLiveCardState(card), + }) + variables.push({ + variableId: `wlive_${card}_sdstate`, + name: `Wing Live Card ${card} SD State`, + path: Commands.Cards.WLiveCardSDState(card), + }) + variables.push({ + variableId: `wlive_${card}_sdsize`, + name: `Wing Live Card ${card} SD Size (GB)`, + path: Commands.Cards.WLiveCardSDSize(card), + }) variables.push({ variableId: `wlive_${card}_marker_total`, name: `Wing Live Card ${card} Total Markers` }) variables.push({ variableId: `wlive_${card}_marker_current`, name: `Wing Live Card ${card} Current Marker` }) variables.push({ variableId: `wlive_${card}_session_total`, name: `Wing Live Card ${card} Total Sessions` }) From 2936f4aa1a3331f345512c011373d6a8113cf57c Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 19:42:04 +0100 Subject: [PATCH 02/11] added config options for variable request chunk size and wait time --- src/config.ts | 27 +++++++++++++++++++++++++-- src/state/state.ts | 4 ++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index d2d0c2c..44f2eaf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,8 @@ export interface WingConfig { variableUpdateRate?: number /** When enabled, the module will request values for all variables on startup */ prefetchVariablesOnStartup?: boolean + startupVariableRequestChunkSize?: number + startupVariableRequestChunkWait?: number // Advanced Option requestTimeout?: number @@ -128,7 +130,6 @@ export function GetConfigFields(_self: InstanceBaseExt): SomeCompani width: 6, default: true, }, - spacer, { type: 'checkbox', id: 'show-advanced-options', @@ -138,6 +139,7 @@ export function GetConfigFields(_self: InstanceBaseExt): SomeCompani width: 12, default: false, }, + spacer, { type: 'number', id: 'requestTimeout', @@ -159,6 +161,28 @@ export function GetConfigFields(_self: InstanceBaseExt): SomeCompani default: false, isVisibleExpression: `$(options:show-advanced-options) == true`, }, + { + type: 'number', + id: 'startupVariableRequestChunkSize', + label: `Variable Request Chunk`, + tooltip: 'When are requested in chunks on startup, waiting between each chunk', + width: 6, + min: 200, + max: 2000, + default: 500, + isVisibleExpression: `$(options:show-advanced-options) == true`, + }, + { + type: 'number', + id: 'startupVariableRequestChunkWait', + label: `Variable Request Wait Time (ms)`, + tooltip: 'When are requested in chunks on startup, waiting between each chunk', + width: 6, + min: 20, + max: 1000, + default: 100, + isVisibleExpression: `$(options:show-advanced-options) == true`, + }, { type: 'number', id: 'subscriptionInterval', @@ -170,7 +194,6 @@ export function GetConfigFields(_self: InstanceBaseExt): SomeCompani default: 9000, isVisibleExpression: `$(options:show-advanced-options) == true`, }, - spacer, { type: 'static-text', id: 'osc-forwarding-info', diff --git a/src/state/state.ts b/src/state/state.ts index 1318360..8697788 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -238,8 +238,8 @@ export class WingState implements IStoredChannelSubject { const vars = getAllVariables(model) - const chunkSize = 500 - const chunkWait = 50 + const chunkSize = self.config.startupVariableRequestChunkSize ?? 500 + const chunkWait = self.config.startupVariableRequestChunkWait ?? 100 const chunks = Math.ceil(vars.length / chunkSize) for (let c = 0; c < chunks; c++) { const wait = c * chunkWait From 47bd03a043cf6c4e5dc4c288fdf8de8a9c365baa Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 19:59:34 +0100 Subject: [PATCH 03/11] fix: detected devices show up in ip address dropdown --- src/handlers/device-detector.ts | 14 ++++++++++---- src/index.ts | 16 +++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/handlers/device-detector.ts b/src/handlers/device-detector.ts index 6fb61d4..04afe01 100644 --- a/src/handlers/device-detector.ts +++ b/src/handlers/device-detector.ts @@ -14,6 +14,7 @@ export interface WingDeviceDetectorInterface { subscribe(instanceId: string): void unsubscribe(instanceId: string): void listKnown(): DeviceInfo[] + addLogger(logger: ModuleLogger): void } /** @@ -33,15 +34,21 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect this.logger = logger } + /** + * Add a logger + * @param logger An instance of a ModuleLogger + */ + public addLogger(logger: ModuleLogger): void { + this.logger = logger + } + /** * Register a subscriber to start device detection. * @param instanceId Unique identifier for the subscriber. */ public subscribe(instanceId: string): void { const startListening = this.subscribers.size === 0 - this.subscribers.add(instanceId) - if (startListening) { this.startListening() } @@ -71,7 +78,6 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect */ private startListening(): void { this.knownDevices.clear() - if (this.subscribers.size === 0) { return } @@ -112,6 +118,7 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect }) this.osc.on('message', (message): void => { + this.logger?.debug(`received device detector message ${JSON.stringify(message)}`) const args = message.args as osc.MetaArgument[] if (!args || args.length === 0 || args[0].type !== 's') { return @@ -152,7 +159,6 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect this.noDeviceTimeout = undefined } }) - this.osc.open() } diff --git a/src/index.ts b/src/index.ts index 5ba1196..cf49d50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { createActions } from './actions/index.js' import { GetFeedbacksList } from './feedbacks.js' import { OscMessage } from 'osc' import { WingTransitions } from './handlers/transitions.js' -import { WingDeviceDetector } from './handlers/device-detector.js' +import { WingDeviceDetectorInstance, WingDeviceDetectorInterface } from './handlers/device-detector.js' import { ModelSpec, WingModel } from './models/types.js' import { getDeskModel } from './models/index.js' import { GetPresets } from './presets.js' @@ -34,7 +34,7 @@ export class WingInstance extends InstanceBase implements InstanceBa connected: boolean = false - deviceDetector: WingDeviceDetector | undefined + deviceDetector: WingDeviceDetectorInterface | undefined connection: ConnectionHandler | undefined stateHandler: StateHandler | undefined feedbackHandler: FeedbackHandler | undefined @@ -66,7 +66,6 @@ export class WingInstance extends InstanceBase implements InstanceBa this.logger.setLoggerFn((level, message) => { this.log(level, message) }) - this.logger.disable() this.logger.debugMode = config.debugMode ?? false this.logger.timestamps = config.debugMode ?? false await this.configUpdated(config) @@ -102,8 +101,6 @@ export class WingInstance extends InstanceBase implements InstanceBa this.updateFeedbacks() this.setupOscForwarder() - - this.deviceDetector?.subscribe(this.id) } getConfigFields(): SomeCompanionConfigField[] { @@ -119,9 +116,11 @@ export class WingInstance extends InstanceBase implements InstanceBa } private setupDeviceDetector(): void { - this.deviceDetector = new WingDeviceDetector(this.logger) - this.deviceDetector?.unsubscribe(this.id) - + this.deviceDetector = WingDeviceDetectorInstance + if (this.logger !== undefined) { + this.deviceDetector.addLogger(this.logger) + } + this.deviceDetector.subscribe(this.id) if (this.deviceDetector) { ;(this.deviceDetector as any).on?.('no-device-detected', () => { this.logger?.warn('No console detected on the network') @@ -175,7 +174,6 @@ export class WingInstance extends InstanceBase implements InstanceBa this.connected = true this.logger?.info('OSC connection established') - this.logger?.enable() this.feedbackHandler?.startPolling() this.stateHandler?.state?.requestNames(this) From df83754e2d4da14a576cfb31dd26314a47793d73 Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 20:16:28 +0100 Subject: [PATCH 04/11] changed: reduced timeout until console is removed from known devices list --- src/handlers/device-detector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/device-detector.ts b/src/handlers/device-detector.ts index 04afe01..057024b 100644 --- a/src/handlers/device-detector.ts +++ b/src/handlers/device-detector.ts @@ -148,7 +148,7 @@ export class WingDeviceDetector extends EventEmitter implements WingDeviceDetect // If a device has not been seen for over a minute, remove it this.knownDevices.set(info.address, info) for (const [id, data] of Array.from(this.knownDevices.entries())) { - if (data.lastSeen < Date.now() - 60000) { + if (data.lastSeen < Date.now() - 20000) { this.logger?.info(`Removing console ${data.deviceName} at ${data.address} from known devices due to timeout`) this.knownDevices.delete(id) } From 0cbc3f9bd142ef413ae7156a76d14c342ea27cf8 Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 20:25:14 +0100 Subject: [PATCH 05/11] fix: module updates automatically on newly selected console from list --- src/index.ts | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index cf49d50..67a4418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,7 +62,7 @@ export class WingInstance extends InstanceBase implements InstanceBa } async init(config: WingConfig): Promise { - this.logger = new ModuleLogger('Wing') + this.logger = new ModuleLogger(this.label) this.logger.setLoggerFn((level, message) => { this.log(level, message) }) @@ -74,10 +74,23 @@ export class WingInstance extends InstanceBase implements InstanceBa async destroy(): Promise { this.deviceDetector?.unsubscribe(this.id) this.transitions.stopAll() + } + private start(config: WingConfig): void { + this.setupDeviceDetector() + this.setupConnectionHandler() + this.setupStateHandler() + this.setupFeedbackHandler() + this.setupVariableHandler() + this.transitions.setUpdateRate(config.fadeUpdateRate ?? 50) + this.setupOscForwarder() + this.updateActions() + this.updateFeedbacks() + } + + private stop(): void { this.connection?.close() this.stateHandler?.clearState() - this.oscForwarder?.close() this.oscForwarder = undefined this.variableHandler?.destroy() @@ -87,20 +100,8 @@ export class WingInstance extends InstanceBase implements InstanceBa this.config = config this.model = getDeskModel(this.config.model) - this.setupDeviceDetector() - this.transitions.stopAll() - - this.setupConnectionHandler() - this.setupStateHandler() - this.setupFeedbackHandler() - - this.setupVariableHandler() - - this.transitions.setUpdateRate(this.config.fadeUpdateRate ?? 50) - this.updateActions() - this.updateFeedbacks() - - this.setupOscForwarder() + this.stop() + this.start(config) } getConfigFields(): SomeCompanionConfigField[] { @@ -137,17 +138,20 @@ export class WingInstance extends InstanceBase implements InstanceBa if (!ipRegex.test(this.config.host ?? '')) { this.updateStatus(InstanceStatus.BadConfig, 'No host configured') - return } this.connection.open('0.0.0.0', 0, this.config.host!, 2223) this.connection.setSubscriptionInterval(this.config.subscriptionInterval ?? 9000) this.connection.startSubscription() - this.updateStatus(InstanceStatus.Connecting, 'waiting for response from console...') this.connection?.on('ready', () => { this.updateStatus(InstanceStatus.Connecting, 'Waiting for answer from console...') - void this.connection?.sendCommand('/*').catch(() => {}) + this.feedbackHandler?.startPolling() + this.stateHandler?.state?.requestNames(this) + if (this.config.prefetchVariablesOnStartup) { + void this.stateHandler?.state?.requestAllVariables(this) + } + this.stateHandler?.requestUpdate() }) this.connection?.on('error', (err: Error) => { @@ -174,13 +178,6 @@ export class WingInstance extends InstanceBase implements InstanceBa this.connected = true this.logger?.info('OSC connection established') - - this.feedbackHandler?.startPolling() - this.stateHandler?.state?.requestNames(this) - if (this.config.prefetchVariablesOnStartup) { - void this.stateHandler?.state?.requestAllVariables(this) - } - this.stateHandler?.requestUpdate() } this.feedbackHandler?.clearPollTimeout() this.stateHandler?.processMessage(this.messages) @@ -203,6 +200,8 @@ export class WingInstance extends InstanceBase implements InstanceBa }) this.stateHandler.on('update', () => { + this.updateActions() + this.updateFeedbacks() this.setPresetDefinitions(GetPresets(this)) this.setActionDefinitions(createActions(this)) this.setFeedbackDefinitions(GetFeedbacksList(this)) From 7a0afce35c2bb379e2b30a25f2feb9bc72afd2a9 Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 20:30:18 +0100 Subject: [PATCH 06/11] moved variable handler to handler directory --- src/{variables => handlers}/variable-handler.ts | 2 +- src/index.ts | 2 +- src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{variables => handlers}/variable-handler.ts (99%) diff --git a/src/variables/variable-handler.ts b/src/handlers/variable-handler.ts similarity index 99% rename from src/variables/variable-handler.ts rename to src/handlers/variable-handler.ts index 0dfab56..0cde188 100644 --- a/src/variables/variable-handler.ts +++ b/src/handlers/variable-handler.ts @@ -4,9 +4,9 @@ import osc, { OscMessage } from 'osc' import { CompanionVariableDefinition, CompanionVariableValues, OSCMetaArgument } from '@companion-module/base' import * as ActionUtil from '../actions/utils.js' import { IoCommands } from '../commands/io.js' -import { getAllVariables } from './index.js' import debounceFn from 'debounce-fn' import { ModuleLogger } from '../handlers/logger.js' +import { getAllVariables } from '../variables/index.js' const RE_NAME = /\/(\w+)\/(\d+)\/\$?name/ const RE_GAIN = /\/(\w+)\/(\d+)\/in\/set\/\$g/ diff --git a/src/index.ts b/src/index.ts index 67a4418..b92be23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import { GetPresets } from './presets.js' import { ConnectionHandler } from './handlers/connection-handler.js' import { StateHandler } from './handlers/state-handler.js' import { FeedbackHandler } from './handlers/feedback-handler.js' -import { VariableHandler } from './variables/variable-handler.js' +import { VariableHandler } from './handlers/variable-handler.js' import { OscForwarder } from './handlers/osc-forwarder.js' import debounceFn from 'debounce-fn' import { ModuleLogger } from './handlers/logger.js' diff --git a/src/types.ts b/src/types.ts index 689847b..dee6e9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,5 +13,5 @@ export interface InstanceBaseExt extends InstanceBase { connection?: import('./handlers/connection-handler.js').ConnectionHandler | undefined stateHandler?: import('./handlers/state-handler.js').StateHandler | undefined feedbackHandler?: import('./handlers/feedback-handler.js').FeedbackHandler | undefined - variableHandler?: import('./variables/variable-handler.js').VariableHandler | undefined + variableHandler?: import('./handlers/variable-handler.js').VariableHandler | undefined } From ae06f60e56a9ba7588d68de3a72230fd7a853b27 Mon Sep 17 00:00:00 2001 From: Janik Witzig Date: Sat, 10 Jan 2026 20:33:57 +0100 Subject: [PATCH 07/11] fix: added logger to variable handler --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b92be23..8861bb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -231,7 +231,7 @@ export class WingInstance extends InstanceBase implements InstanceBa } private setupVariableHandler(): void { - this.variableHandler = new VariableHandler(this.model, this.config.variableUpdateRate) + this.variableHandler = new VariableHandler(this.model, this.config.variableUpdateRate, this.logger) this.variableHandler.on('create-variables', (variables) => { this.setVariableDefinitions(variables) From 09b7f4f68cc8cf407bc0864cac458602ddc55cdc Mon Sep 17 00:00:00 2001 From: Janik Witzig <63373864+janikwitzig@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:44:59 +0100 Subject: [PATCH 08/11] Update src/variables/gpio.ts fix: GPIO state now uses the correct command Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/variables/gpio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variables/gpio.ts b/src/variables/gpio.ts index d1f0732..9df9103 100644 --- a/src/variables/gpio.ts +++ b/src/variables/gpio.ts @@ -9,7 +9,7 @@ export function getGpioVariables(model: ModelSpec): VariableDefinition[] { variables.push({ variableId: `gpio${gpio}`, name: `GPIO ${gpio} state (true = pressed/connected)`, - path: Commands.Control.GpioState(gpio), + path: Commands.Control.GpioReadState(gpio), }) } From 8630bd0b08e275d69db4e319028fc596528f609a Mon Sep 17 00:00:00 2001 From: Janik Witzig <63373864+janikwitzig@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:45:36 +0100 Subject: [PATCH 09/11] Update src/variables/talkback.ts fix: talkback variable main assign has correct name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/variables/talkback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variables/talkback.ts b/src/variables/talkback.ts index fde15fa..e544c06 100644 --- a/src/variables/talkback.ts +++ b/src/variables/talkback.ts @@ -27,7 +27,7 @@ export function getTalkbackVariables(model: ModelSpec): VariableDefinition[] { for (let main = 1; main <= model.mains; main++) { variables.push({ variableId: `talkback_${lower}_main${main}_assign`, - name: `Talkback ${upper} to Matrix ${main} assign`, + name: `Talkback ${upper} to Main ${main} assign`, path: Commands.Configuration.TalkbackMainAssign(upper, main), }) } From ff59887afe6c9f41815dcd4f21fceb6677d5b1d8 Mon Sep 17 00:00:00 2001 From: Janik Witzig <63373864+janikwitzig@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:45:56 +0100 Subject: [PATCH 10/11] Update src/variables/main.ts fix: matrix send level variable uses correct command Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/variables/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variables/main.ts b/src/variables/main.ts index 7b8ad3c..48e1764 100644 --- a/src/variables/main.ts +++ b/src/variables/main.ts @@ -35,7 +35,7 @@ export function getMainVariables(model: ModelSpec): VariableDefinition[] { variables.push({ variableId: `main${main}_mtx${send}_level`, name: `Main ${main} to Matrix ${send} Level`, - path: Commands.Main.MatrixSendOn(main, send), + path: Commands.Main.MatrixSendLevel(main, send), }) } From c7310136cd7dffd39fff18d447992624f074acf2 Mon Sep 17 00:00:00 2001 From: Janik Witzig <63373864+janikwitzig@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:48:04 +0100 Subject: [PATCH 11/11] Update src/handlers/feedback-handler.ts fix: feedback handler continues to loop if a feedback id is undefined instead of returning Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/handlers/feedback-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/feedback-handler.ts b/src/handlers/feedback-handler.ts index 4593ac8..7e2bf41 100644 --- a/src/handlers/feedback-handler.ts +++ b/src/handlers/feedback-handler.ts @@ -52,7 +52,7 @@ export class FeedbackHandler extends EventEmitter { for (const msg of msgs) { const toUpdate = this.subscriptions?.getFeedbacks(msg.address) if (toUpdate === undefined) { - return + continue } if (toUpdate.length > 0) { toUpdate.forEach((f) => this.messageFeedbacks.add(f))