Skip to content

Commit 5075bd1

Browse files
authored
Merge pull request #45 from bitfocus/jaw/add-show-control
Jaw/add show control
2 parents 20ff63d + 5d7c712 commit 5075bd1

File tree

9 files changed

+263
-14
lines changed

9 files changed

+263
-14
lines changed

src/actions/control.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { CompanionActionDefinitions } from '@companion-module/base'
2+
import { CompanionActionWithCallback } from './common.js'
3+
import { InstanceBaseExt } from '../types.js'
4+
import { WingConfig } from '../config.js'
5+
import { ControlCommands } from '../commands/control.js'
6+
import { StateUtil } from '../state/index.js'
7+
import { GetDropdown } from '../choices/common.js'
8+
import { getIdLabelPair } from '../choices/utils.js'
9+
10+
export enum OtherActionId {
11+
RecallScene = 'recall-scene',
12+
RecallSceneFromList = 'recall-scene-from-list',
13+
SendLibraryAction = 'send-library-action',
14+
}
15+
16+
export function createControlActions(self: InstanceBaseExt<WingConfig>): CompanionActionDefinitions {
17+
const send = self.sendCommand
18+
const state = self.state
19+
const ensureLoaded = self.ensureLoaded
20+
21+
const actions: { [id in OtherActionId]: CompanionActionWithCallback | undefined } = {
22+
[OtherActionId.RecallScene]: {
23+
name: 'Recall Scene',
24+
description: 'Recall a scene and optionally go to it',
25+
options: [
26+
{
27+
type: 'number',
28+
id: 'num',
29+
label: 'Scene Number',
30+
min: 1,
31+
max: 16384,
32+
default: 1,
33+
},
34+
{
35+
type: 'checkbox',
36+
id: 'go',
37+
label: 'Go to Scene?',
38+
default: true,
39+
},
40+
],
41+
callback: async (event) => {
42+
send(ControlCommands.LibrarySceneSelectionIndex(), event.options.num as number)
43+
if (event.options.go) {
44+
send(ControlCommands.LibraryAction(), 'GO')
45+
}
46+
},
47+
subscribe: () => {
48+
const cmd = ControlCommands.LibraryActiveSceneIndex()
49+
ensureLoaded(cmd)
50+
},
51+
learn: () => {
52+
const cmd = ControlCommands.LibraryActiveSceneIndex()
53+
return { num: StateUtil.getNumberFromState(cmd, state) }
54+
},
55+
},
56+
[OtherActionId.RecallSceneFromList]: {
57+
name: 'Recall Scene from List',
58+
description:
59+
'Recall a scene from a list and optionally go to it (NOTE: When you add/remove/move scenes in your show, you must update this command.)',
60+
options: [
61+
GetDropdown(
62+
'Scene',
63+
'num',
64+
state.namedChoices.scenes,
65+
undefined,
66+
'This uses the index of the scene, and changes when scenes are added or removed.',
67+
),
68+
{
69+
type: 'checkbox',
70+
id: 'go',
71+
label: 'Go to Scene?',
72+
default: true,
73+
},
74+
],
75+
callback: async (event) => {
76+
const go = event.options.go as boolean
77+
send(ControlCommands.LibrarySceneSelectionIndex(), event.options.num as number)
78+
if (go) {
79+
send(ControlCommands.LibraryAction(), 'GO')
80+
}
81+
},
82+
subscribe: () => {
83+
const cmd = ControlCommands.LibraryActiveSceneIndex()
84+
ensureLoaded(cmd)
85+
ensureLoaded(ControlCommands.LibraryNode(), '?')
86+
},
87+
learn: () => {
88+
const cmd = ControlCommands.LibraryActiveSceneIndex()
89+
return { num: StateUtil.getNumberFromState(cmd, state) }
90+
},
91+
},
92+
[OtherActionId.SendLibraryAction]: {
93+
name: 'Send Library Action',
94+
description: 'Trigger a library action (Select and navigate scenes in a show)',
95+
options: [
96+
GetDropdown(
97+
'Action',
98+
'act',
99+
[
100+
getIdLabelPair('GOPREV', 'Select Previous and Go'),
101+
getIdLabelPair('GONEXT', 'Select Next and Go'),
102+
getIdLabelPair('GO', 'Go'),
103+
getIdLabelPair('PREV', 'Select Previous'),
104+
getIdLabelPair('NEXT', 'Select Next'),
105+
//getIdLabelPair('GOTAG', 'G'),
106+
],
107+
'GO',
108+
),
109+
],
110+
callback: async (event) => {
111+
const act = event.options.act as string
112+
if (act === 'GO') {
113+
send(ControlCommands.LibrarySceneSelectionIndex(), 0) // required for 'GO' with PREV/NEXT
114+
}
115+
const cmd = ControlCommands.LibraryAction()
116+
send(cmd, act)
117+
},
118+
},
119+
}
120+
121+
return actions
122+
}

src/actions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createMatrixActions } from './matrix.js'
1111
import { createUsbPlayerActions } from './usbplayer.js'
1212
import { createCardsActions } from './cards.js'
1313
import { createCommonActions } from './common.js'
14+
import { createControlActions } from './control.js'
1415

1516
export function createActions(self: InstanceBaseExt<WingConfig>): CompanionActionDefinitions {
1617
const actions = {
@@ -24,6 +25,7 @@ export function createActions(self: InstanceBaseExt<WingConfig>): CompanionActio
2425
...createUsbPlayerActions(self),
2526
...createCardsActions(self),
2627
...createConfigurationActions(self),
28+
...createControlActions(self),
2729
}
2830

2931
return actions

src/actions/other.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WingConfig } from '../config.js'
66
export enum OtherActionId {
77
SendCommand = 'send-command',
88
SendCommandWithNumber = 'send-command-with-number',
9+
SendCommandWithString = 'send-command-with-string',
910
}
1011

1112
export function GetOtherActions(self: InstanceBaseExt<WingConfig>): CompanionActionDefinitions {
@@ -48,6 +49,25 @@ export function GetOtherActions(self: InstanceBaseExt<WingConfig>): CompanionAct
4849
send(event.options.cmd as string, event.options.num as number)
4950
},
5051
},
52+
[OtherActionId.SendCommandWithString]: {
53+
name: 'Send Command with String',
54+
description: 'Send an OSC command with a string as an argument to the console.',
55+
options: [
56+
{
57+
type: 'textinput',
58+
id: 'cmd',
59+
label: 'Command',
60+
},
61+
{
62+
type: 'textinput',
63+
id: 'val',
64+
label: 'Value',
65+
},
66+
],
67+
callback: async (event) => {
68+
send(event.options.cmd as string, event.options.val as string)
69+
},
70+
},
5171
}
5272

5373
return actions

src/choices/common.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,25 @@ export function GetDropdown(
6666
defaultChoice?: string,
6767
tooltip?: string,
6868
): CompanionInputFieldDropdown {
69-
return {
70-
type: 'dropdown',
71-
label: label,
72-
id: id,
73-
default: defaultChoice ?? choices[0].id,
74-
choices: choices,
75-
tooltip: tooltip,
69+
if (choices.length == 0) {
70+
// return empty dropdown
71+
return {
72+
type: 'dropdown',
73+
label: label,
74+
id: id,
75+
default: '',
76+
choices: [],
77+
tooltip: tooltip,
78+
}
79+
} else {
80+
return {
81+
type: 'dropdown',
82+
label: label,
83+
id: id,
84+
default: defaultChoice ?? choices[0].id ?? '',
85+
choices: choices,
86+
tooltip: tooltip,
87+
}
7688
}
7789
}
7890

src/commands/control.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// eslint-disable-next-line @typescript-eslint/no-namespace
2+
export namespace ControlCommands {
3+
export function Node(): string {
4+
return `/$ctl`
5+
}
6+
7+
export function LibraryNode(): string {
8+
return `${Node()}/lib`
9+
}
10+
11+
export function LibraryScenes(): string {
12+
return `${LibraryNode()}/$scenes`
13+
}
14+
15+
export function LibraryActiveSceneIndex(): string {
16+
return `${LibraryNode()}/$actidx`
17+
}
18+
19+
export function LibraryActiveSceneName(): string {
20+
return `${LibraryNode()}/$active`
21+
}
22+
23+
export function LibraryActiveShowName(): string {
24+
return `${LibraryNode()}/$actshow`
25+
}
26+
27+
export function LibraryAction(): string {
28+
return `${LibraryNode()}/$action`
29+
}
30+
31+
export function LibrarySceneSelectionIndex(): string {
32+
return `${LibraryNode()}/$actionidx`
33+
}
34+
}

src/index.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,16 @@ export class WingInstance extends InstanceBase<WingConfig> implements InstanceBa
275275
this.state.set(message.address, args)
276276

277277
if (this.inFlightRequests[message.address]) {
278-
this.log('debug', `Received answer for request ${message.address}`)
278+
// this.log('debug', `Received answer for request ${message.address}`)
279279
this.inFlightRequests[message.address]()
280280
delete this.inFlightRequests[message.address]
281281
}
282282

283283
// setImmediate(() => {
284284
this.checkFeedbackChanges(message)
285285

286+
this.updateLists(message)
287+
286288
this.variableMessages[message.address] = message
287289
// this.debounceUpdateVariables()
288290

@@ -315,6 +317,27 @@ export class WingInstance extends InstanceBase<WingConfig> implements InstanceBa
315317
})
316318
}
317319

320+
private updateLists(msg: osc.OscMessage): void {
321+
const args = msg.args as osc.MetaArgument[]
322+
323+
const libRe = /\/\$ctl\/lib/
324+
// const usbRe = /\/play/
325+
// const sd1Re = /\cards\/wlive\/1\/\$stat/
326+
// const sd2Re = /\cards\/wlive\/2\/\$stat/
327+
328+
// scene list
329+
if (libRe.test(msg.address)) {
330+
console.log('Got library')
331+
const content = (args[0].value as string) ?? ''
332+
const scenes = content.match(/\$scenes\s+list\s+\[([^\]]+)\]/)
333+
console.log(scenes)
334+
if (scenes) {
335+
const sceneList = scenes[1].split(',').map((s) => s.trim())
336+
this.state.namedChoices.scenes = sceneList.map((s, i) => ({ id: i + 1, label: s }))
337+
}
338+
}
339+
}
340+
318341
private checkFeedbackChanges(msg: osc.OscMessage): void {
319342
const toUpdate = this.WingSubscriptions.getFeedbacks(msg.address)
320343
if (toUpdate.length > 0) {
@@ -327,12 +350,14 @@ export class WingInstance extends InstanceBase<WingConfig> implements InstanceBa
327350
const busNameRe = /\/bus\/\d+\/\$name/
328351
const mtxNameRe = /\/mtx\/\d+\/\$name/
329352
const mainNameRe = /\/main\/\d+\/\$name/
353+
const libRe = /\/\$ctl\/lib/
330354
if (
331355
channelNameRe.test(msg.address) ||
332356
busNameRe.test(msg.address) ||
333357
auxNameRe.test(msg.address) ||
334358
mtxNameRe.test(msg.address) ||
335-
mainNameRe.test(msg.address)
359+
mainNameRe.test(msg.address) ||
360+
libRe.test(msg.address)
336361
) {
337362
// this.log('info', 'Would update now')
338363

@@ -398,10 +423,10 @@ export class WingInstance extends InstanceBase<WingConfig> implements InstanceBa
398423
args: args,
399424
}
400425
this.osc.send(command)
401-
this.osc.send({ address: cmd, args: [] })
426+
// this.osc.send({ address: cmd, args: [] })
402427
}
403428

404-
ensureLoaded = (path: string): void => {
429+
ensureLoaded = (path: string, arg?: string | number): void => {
405430
this.requestQueue
406431
.add(async () => {
407432
if (this.inFlightRequests[path]) {
@@ -418,7 +443,7 @@ export class WingInstance extends InstanceBase<WingConfig> implements InstanceBa
418443
this.inFlightRequests[path] = resolve
419444
})
420445

421-
this.sendCommand(path, undefined)
446+
this.sendCommand(path, arg ?? undefined)
422447

423448
await p
424449
})

src/state/state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type NameChoices = {
1313
mains: DropdownChoice[]
1414
dcas: DropdownChoice[]
1515
mutegroups: DropdownChoice[]
16+
scenes: DropdownChoice[]
1617
}
1718

1819
type Names = {
@@ -23,6 +24,7 @@ type Names = {
2324
mains: string[]
2425
dcas: string[]
2526
mutegroups: string[]
27+
scenes: string[]
2628
}
2729
export class WingState implements IStoredChannelSubject {
2830
private readonly data: Map<string, osc.MetaArgument[]>
@@ -37,6 +39,7 @@ export class WingState implements IStoredChannelSubject {
3739
mains: [],
3840
dcas: [],
3941
mutegroups: [],
42+
scenes: [],
4043
}
4144

4245
names: Names = {
@@ -47,6 +50,7 @@ export class WingState implements IStoredChannelSubject {
4750
mains: [],
4851
dcas: [],
4952
mutegroups: [],
53+
scenes: [],
5054
}
5155

5256
constructor(model: ModelSpec) {
@@ -191,7 +195,7 @@ export class WingState implements IStoredChannelSubject {
191195
}
192196
}
193197

194-
public requestNames(model: ModelSpec, ensureLoaded: (path: string) => void): void {
198+
public requestNames(model: ModelSpec, ensureLoaded: (path: string, arg?: string | number) => void): void {
195199
for (let ch = 1; ch <= model.channels; ch++) {
196200
ensureLoaded(Commands.Channel.RealName(ch))
197201
}

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export interface InstanceBaseExt<TConfig> extends InstanceBase<TConfig> {
99
transitions: WingTransitions
1010
state: WingState
1111
sendCommand: (cmd: string, argument?: number | string, preferFloat?: boolean) => void
12-
ensureLoaded: (path: string) => void
12+
ensureLoaded: (path: string, arg?: string | number) => void
1313
}

0 commit comments

Comments
 (0)