Skip to content

Commit

Permalink
feat: add support for xkeys, streamdeck and spacemouse
Browse files Browse the repository at this point in the history
  • Loading branch information
nytamin committed Jan 25, 2024
1 parent ace2fc5 commit 5197b41
Show file tree
Hide file tree
Showing 14 changed files with 1,024 additions and 85 deletions.
6 changes: 5 additions & 1 deletion packages/apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"vite": "^4.5.0"
},
"dependencies": {
"@elgato-stream-deck/webhid": "^6.0.0",
"@popperjs/core": "^2.11.8",
"@sofie-automation/sorensen": "^1.4.2",
"@sofie-prompter-editor/shared-lib": "0.0.0",
"@sofie-prompter-editor/shared-model": "0.0.0",
"bootstrap": "^5.3.2",
"buffer": "^6.0.3",
"eventemitter3": "^5.0.1",
"mobx": "^6.10.2",
"mobx-react-lite": "^4.0.5",
Expand All @@ -56,7 +58,9 @@
"react-helmet-async": "^1.3.0",
"react-router-dom": "^6.18.0",
"socket.io-client": "^4.7.2",
"uuid": "^9.0.1"
"spacemouse-webhid": "^0.0.2",
"uuid": "^9.0.1",
"xkeys-webhid": "^3.1.0"
},
"lint-staged": {
"*.{js,css,json,md,scss}": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Plugin } from 'prosemirror-state'
import { UILineId } from '../../model/UILine'
import { UILineId } from '../../../model/UILine'
import { Node } from 'prosemirror-model'
import { schema } from '../scriptSchema'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,57 @@ import { observer } from 'mobx-react-lite'
import React from 'react'
import { RootAppStore } from 'src/stores/RootAppStore'
import { AlertBar } from 'src/components/AlertBar/AlertBar'
import { Button } from 'react-bootstrap'

export const SystemStatusAlertBars = observer(function SystemStatusAlertBars(): React.JSX.Element {
const isAPIConnected = RootAppStore.connected
const isSofieConnected = RootAppStore.sofieConnected

const xKeysRequestsAccess = RootAppStore.triggerStore.xKeysRequestsAccess
const streamdeckRequestsAccess = RootAppStore.triggerStore.streamdeckRequestsAccess
const spacemouseRequestsAccess = RootAppStore.triggerStore.spacemouseRequestsAccess

let requestAccess: {
name: string
allow: () => void
deny: () => void
} | null = null

if (xKeysRequestsAccess) {
requestAccess = {
name: 'X-Keys panels',
allow: () => RootAppStore.triggerStore.xkeysAccess(true),
deny: () => RootAppStore.triggerStore.xkeysAccess(false),
}
} else if (streamdeckRequestsAccess) {
requestAccess = {
name: 'Streamdeck panels',
allow: () => RootAppStore.triggerStore.streamdeckAccess(true),
deny: () => RootAppStore.triggerStore.streamdeckAccess(false),
}
} else if (spacemouseRequestsAccess) {
requestAccess = {
name: 'SpaceMouse devices',
allow: () => RootAppStore.triggerStore.spacemouseAccess(true),
deny: () => RootAppStore.triggerStore.spacemouseAccess(false),
}
}

return (
<>
{!isAPIConnected ? <AlertBar variant="danger">Prompter is having network troubles</AlertBar> : null}
{!isSofieConnected ? <AlertBar variant="danger">Prompter is having trouble connecting to Sofie</AlertBar> : null}
{requestAccess ? (
<AlertBar variant="info">
Please allow access to {requestAccess.name} to setup shortcuts:
<Button variant="primary" onClick={requestAccess.allow}>
Allow
</Button>
<Button variant="secondary" onClick={requestAccess.deny}>
Deny
</Button>
</AlertBar>
) : null}
</>
)
})
Expand Down
30 changes: 20 additions & 10 deletions packages/apps/client/src/lib/triggers/triggerActions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
/* eslint-disable no-mixed-spaces-and-tabs */

export type TriggerAction =
| {
type: 'prompterMove'
/** The speed to move the prompter */
speed: number
}
| {
type: 'prompterJump'
// todo
}
export type AnyTriggerAction =
| TriggerAction<
'prompterMove',
{
/** The speed to move the prompter */
speed: number
}
>
| TriggerAction<
'prompterJump',
{
// todo
}
>
| TriggerAction<'movePrompterToHere', {}>

type TriggerAction<Type extends string, Payload extends Record<string, any>> = {
type: Type
payload: Payload
}
78 changes: 69 additions & 9 deletions packages/apps/client/src/lib/triggers/triggerConfig.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import {BindOptions} from '@sofie-automation/sorensen'
import { TriggerAction } from './triggerActions.ts'
import { BindOptions } from '@sofie-automation/sorensen'
import { AnyTriggerAction } from './triggerActions.ts'
import { DeviceModelId } from '@elgato-stream-deck/webhid'



export type TriggerConfig = TriggerConfigKeyboard | TriggerConfigXkeys
export type TriggerConfig =
| TriggerConfigKeyboard
| TriggerConfigXkeys
| TriggerConfigStreamdeck
| TriggerConfigSpacemouse

export enum TriggerConfigType {
KEYBOARD = 'keyboard',
XKEYS = 'xkeys',
STREAMDECK = 'streamdeck',
SPACEMOUSE = 'spacemouse',
}
export interface TriggerConfigBase {
type: TriggerConfigType

action: TriggerAction
}
export interface TriggerConfigKeyboard extends TriggerConfigBase {
type: TriggerConfigType.KEYBOARD

action: AnyTriggerAction
/**
* a "+" and space concatenated list of KeyboardEvent.key key values (see: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values),
* in order (order not significant for modifier keys), f.g. "Control+Shift+KeyA", "Control+Shift+KeyB KeyU".
Expand All @@ -33,10 +38,65 @@ export interface TriggerConfigKeyboard extends TriggerConfigBase {
up?: boolean

global?: BindOptions['global']

}
export interface TriggerConfigXkeys extends TriggerConfigBase {
type: TriggerConfigType.XKEYS

// Todo
/** userId of the xkeys panel, or null to match any */
productId: number | null
/** userId of the xkeys, or null to match any */
unitId: number | null

eventType: 'down' | 'up' | 'jog' | 'joystick' | 'rotary' | 'shuttle' | 'tbar' | 'trackball'
/** Index of the key, joystick, etc */
index: number

/** If action.payload is not set, use value from the xkeys */
action:
| AnyTriggerAction
| {
type: AnyTriggerAction['type']
// no payload, use value from the xkeys
}
}
export interface TriggerConfigStreamdeck extends TriggerConfigBase {
type: TriggerConfigType.STREAMDECK

/** userId of the Streamdeck, or null to match any */
modelId: DeviceModelId | null
/** userId of the Streamdeck, or null to match any */
serialNumber: string | null

eventType: 'down' | 'up' | 'rotate' | 'encoderDown' | 'encoderUp'
/** Index of the key, knob, etc */
index: number

/** If action.payload is not set, use value from the xkeys */
action:
| AnyTriggerAction
| {
type: AnyTriggerAction['type']
// no payload, use value from the streamdeck
}
}

export interface TriggerConfigSpacemouse extends TriggerConfigBase {
type: TriggerConfigType.SPACEMOUSE

/** userId of the xkeys panel, or null to match any */
productId: number | null
/** userId of the xkeys, or null to match any */
unitId: number | null

eventType: 'down' | 'up' | 'rotate' | 'translate'
/** Index of the key, if needed, 0 otherwise */
index: number

/** If action.payload is not set, use value from the xkeys */
action:
| AnyTriggerAction
| {
type: AnyTriggerAction['type']
// no payload, use value from the xkeys
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EventEmitter } from 'eventemitter3'
import { AnyTriggerAction } from '../triggerActions'
import { TriggerConfig } from '../triggerConfig.ts'

export interface TriggerHandlerEvents {
action: [action: AnyTriggerAction]

requestXkeysAccess: []
requestStreamdeckAccess: []
requestSpacemouseAccess: []
}

export abstract class TriggerHandler extends EventEmitter<TriggerHandlerEvents> {
protected triggers: TriggerConfig[] = []
abstract initialize(triggers?: TriggerConfig[]): Promise<void>
abstract destroy(): Promise<void>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Sorensen from '@sofie-automation/sorensen'
import { TriggerHandler } from './TriggerHandler'
import { TriggerConfig, TriggerConfigType } from '../triggerConfig'

export class TriggerHandlerKeyboard extends TriggerHandler {
async initialize(triggers?: TriggerConfig[]): Promise<void> {
if (triggers) this.triggers = triggers
// hot-module-reload fix:
if (!(window as any).sorensenInitialized) {
;(window as any).sorensenInitialized = true
await Sorensen.init()
} else {
await Sorensen.destroy()
await Sorensen.init()
}

for (const trigger of this.triggers) {
if (trigger.type !== TriggerConfigType.KEYBOARD) continue

Sorensen.bind(
trigger.keys,
() => {
this.emit('action', trigger.action)
},
{
up: trigger.up,
global: trigger.global,

exclusive: true,
ordered: 'modifiersFirst',
preventDefaultPartials: false,
preventDefaultDown: true,
}
)
}
}
async destroy(): Promise<void> {
Sorensen.destroy().catch((e: Error) => console.error('Sorensen.destroy', e))
}
}
Loading

0 comments on commit 5197b41

Please sign in to comment.