Skip to content

Commit

Permalink
feat: stream deck studio support
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Sep 8, 2024
1 parent 671c8d1 commit 8ae1086
Show file tree
Hide file tree
Showing 24 changed files with 1,103 additions and 89 deletions.
2 changes: 2 additions & 0 deletions assets/linux/50-companion-desktop.rules
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0086", MODE:="666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="008f", MODE:="666", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="009a", MODE:="666", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00aa", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666", TAG+="uaccess"
Expand All @@ -27,6 +28,7 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0086", MODE:="666
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="008f", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="009a", MODE:="666", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00aa", MODE:="666", TAG+="uaccess"

# xkeys
SUBSYSTEM=="usb", ATTRS{idVendor}=="05f3", MODE:="666", TAG+="uaccess"
Expand Down
2 changes: 2 additions & 0 deletions assets/linux/50-companion-headless.rules
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0086", MODE:="666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="008f", MODE:="666", GROUP="companion"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666", GROUP="companion"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="009a", MODE:="666", GROUP="companion"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00aa", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666", GROUP="companion"
Expand All @@ -27,6 +28,7 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0086", MODE:="666
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="008f", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="009a", MODE:="666", GROUP="companion"
KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00aa", MODE:="666", GROUP="companion"

# xkeys
SUBSYSTEM=="usb", ATTRS{idVendor}=="05f3", MODE:="666", GROUP="companion"
Expand Down
2 changes: 1 addition & 1 deletion companion/lib/Resources/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export async function transformButtonImage(render, rotation, targetWidth, target
const imageRsRotation = translateRotation(rotation)
if (imageRsRotation !== null) image = image.rotate(imageRsRotation)

image = image.scale(targetWidth, targetHeight)
image = image.scale(targetWidth, targetHeight, imageRs.ResizeMode.Fit)

// pad, in case a button is non-square
const dimensions = image.getCurrentDimensions()
Expand Down
101 changes: 91 additions & 10 deletions companion/lib/Service/SurfaceDiscovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ServiceBase from './Base.js'
import { Bonjour, Browser } from '@julusian/bonjour-service'
import systeminformation from 'systeminformation'
import got from 'got'
import { StreamDeckTcpDiscoveryService } from '@elgato-stream-deck/tcp'

const SurfaceDiscoveryRoom = 'surfaces:discovery'

Expand Down Expand Up @@ -34,6 +35,10 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
* @type {Browser | undefined}
*/
#satelliteBrowser
/**
* @type {StreamDeckTcpDiscoveryService | undefined}
*/
#streamDeckDiscovery

/**
* @type {NodeJS.Timeout | undefined}
Expand All @@ -44,12 +49,14 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
* @param {import('../Registry.js').default} registry - the application core
*/
constructor(registry) {
super(registry, 'Service/Satellite', 'discoveryEnabled', null)
super(registry, 'Service/SurfaceDiscovery', 'discoveryEnabled', null)

this.init()
}

listen() {
this.currentState = true

if (!this.#satelliteBrowser) {
try {
this.#satelliteBrowser = this.#bonjour.find({ type: 'companion-satellite', protocol: 'tcp' })
Expand All @@ -70,12 +77,45 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
this.#satelliteBrowser.on('srv-update', (newService, oldService) => {
this.#updateSatelliteService(oldService, newService)
})

this.currentState = true
} catch (e) {
this.logger.debug(`ERROR failed to start searching for companion satellite devices`)
}
}

if (!this.#streamDeckDiscovery) {
try {
this.#streamDeckDiscovery = new StreamDeckTcpDiscoveryService()

this.#streamDeckDiscovery.on('up', (streamdeck) => {
const uiService = this.#convertStreamDeckForUi(streamdeck)
if (!uiService) return

this.logger.debug(
`Found streamdeck tcp device ${streamdeck.name} at ${streamdeck.address}:${streamdeck.port}`
)

this.io.emitToRoom(SurfaceDiscoveryRoom, 'surfaces:discovery:update', {
type: 'update',
info: uiService,
})
})
this.#streamDeckDiscovery.on('down', (streamdeck) => {
const uiService = this.#convertStreamDeckForUi(streamdeck)
if (!uiService) return

this.io.emitToRoom(SurfaceDiscoveryRoom, 'surfaces:discovery:update', {
type: 'remove',
itemId: uiService.id,
})
})

setImmediate(() => {
this.#streamDeckDiscovery?.query()
})
} catch (e) {
this.logger.debug(`ERROR failed to start searching for streamdeck tcp devices`)
}
}
}

/**
Expand Down Expand Up @@ -132,21 +172,44 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
}
}

/**
*
* @param {import('@elgato-stream-deck/tcp').StreamDeckTcpDefinition} streamdeck
* @returns {import('@companion-app/shared/Model/Surfaces.js').ClientDiscoveredSurfaceInfo | null}
*/
#convertStreamDeckForUi(streamdeck) {
if (!streamdeck.isPrimary) return null

return {
id: `streamdeck:${streamdeck.serialNumber ?? streamdeck.name}`,

surfaceType: 'streamdeck',

name: streamdeck.name,
address: streamdeck.address,
port: streamdeck.port,

modelName: streamdeck.modelName,
serialnumber: streamdeck.serialNumber,
}
}

/**
* Kill the socket, if exists.
* @access protected
* @override
*/
disableModule() {
if (this.#satelliteBrowser) {
if (this.currentState) {
this.currentState = false
try {
this.currentState = false

for (const service of this.#satelliteBrowser.services) {
this.#forgetSatelliteService(service)
if (this.#satelliteBrowser) {
for (const service of this.#satelliteBrowser.services) {
this.#forgetSatelliteService(service)
}
this.#satelliteBrowser.stop()
this.#satelliteBrowser = undefined
}
this.#satelliteBrowser.stop()
this.#satelliteBrowser = undefined

clearTimeout(this.#satelliteExpireInterval)
this.#satelliteExpireInterval = undefined
Expand All @@ -155,6 +218,17 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
} catch (/** @type {any} */ e) {
this.logger.silly(`Could not stop searching for satellite devices: ${e.message}`)
}

try {
if (this.#streamDeckDiscovery) {
this.#streamDeckDiscovery.destroy()
this.#streamDeckDiscovery = undefined
}

this.logger.info(`Stopped searching for streamdeck tcp devices`)
} catch (/** @type {any} */ e) {
this.logger.silly(`Could not stop searching for streamdeck tcp devices: ${e.message}`)
}
}
}

Expand All @@ -177,6 +251,13 @@ export class ServiceSurfaceDiscovery extends ServiceBase {
}
}

if (this.#streamDeckDiscovery) {
for (const service of this.#streamDeckDiscovery.knownStreamDecks) {
const uiService = this.#convertStreamDeckForUi(service)
if (uiService) services[uiService.id] = uiService
}
}

return services
})
client.onPromise('surfaces:discovery:leave', () => {
Expand Down
79 changes: 79 additions & 0 deletions companion/lib/Surface/Controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import FrameworkMacropadDriver from './USB/FrameworkMacropad.js'
import MystrixDriver from './USB/203SystemsMystrix.js'
import CoreBase from '../Core/Base.js'
import { SurfaceGroup } from './Group.js'
import { SurfaceOutboundController } from './Outbound.js'
import { SurfaceUSBBlackmagicController } from './USB/BlackmagicController.js'
import { VARIABLE_UNKNOWN_VALUE } from '../Variables/Util.js'

Expand Down Expand Up @@ -113,12 +114,20 @@ class SurfaceController extends CoreBase {
*/
#runningRefreshDevices = false

/**
* @type {SurfaceOutboundController}
* @readonly
*/
#outboundController

/**
* @param {import('../Registry.js').default} registry
*/
constructor(registry) {
super(registry, 'Surface/Controller')

this.#outboundController = new SurfaceOutboundController(this, registry.db, registry.io)

this.#surfacesAllLocked = !!this.userconfig.getKey('link_lockouts')

setImmediate(() => {
Expand All @@ -142,6 +151,7 @@ class SurfaceController extends CoreBase {
this.#refreshDevices().catch(() => {
this.logger.warn('Initial USB scan failed')
})
this.#outboundController.init()

this.updateDevicesList()

Expand Down Expand Up @@ -320,6 +330,8 @@ class SurfaceController extends CoreBase {
* @access public
*/
clientConnect(client) {
this.#outboundController.clientConnect(client)

client.onPromise('emulator:startup', (id) => {
const fullId = EmulatorRoom(id)

Expand Down Expand Up @@ -641,6 +653,10 @@ class SurfaceController extends CoreBase {
* @returns {ClientDevicesListItem[]}
*/
getDevicesList() {
const remoteConnections = Object.values(this.#outboundController.storage)

const usedRemoteConnectionIds = new Set()

/**
*
* @param {string} id
Expand All @@ -660,14 +676,27 @@ class SurfaceController extends CoreBase {
isConnected: !!surfaceHandler,
displayName: getSurfaceName(config, id),
location: null,
remoteConnectionId: null,
}

if (surfaceHandler) {
let location = surfaceHandler.panel.info.location
if (location && location.startsWith('::ffff:')) location = location.substring(7)
let remotePort = surfaceHandler.panel.info.remotePort

surfaceInfo.location = location || null
surfaceInfo.configFields = surfaceHandler.panel.info.configFields || []

// Translate remote connections info
if (config?.integrationType === 'elgato-streamdeck-tcp') {
const matchingRemote = remoteConnections.find(
(s) => s.type === 'elgato' && s.address === location && s.port === remotePort
)
if (matchingRemote) {
surfaceInfo.remoteConnectionId = matchingRemote.id
usedRemoteConnectionIds.add(matchingRemote.id)
}
}
}

return surfaceInfo
Expand Down Expand Up @@ -753,6 +782,36 @@ class SurfaceController extends CoreBase {
}
}

for (const connection of remoteConnections) {
if (usedRemoteConnectionIds.has(connection.id)) continue

const tcpId = `tcp://${connection.address}:${connection.port}`

/** @type {ClientDevicesListItem} */
const groupResult = {
id: tcpId,
index: undefined,
displayName: `${connection.displayName || connection.type} - Offline`,
isAutoGroup: true,
surfaces: [
{
id: tcpId,
type: 'Unknown Elgato Stream Deck (TCP)',
integrationType: config?.integrationType || '',
name: config?.name || '',
// location: 'Offline',
configFields: [],
isConnected: false,
displayName: connection.displayName,
location: null,
remoteConnectionId: connection.id,
},
],
}
result.push(groupResult)
// groupsMap.set(groupId, groupResult)
}

return result
}

Expand Down Expand Up @@ -967,6 +1026,24 @@ class SurfaceController extends CoreBase {
}
}

/**
*
* @param {import('@elgato-stream-deck/tcp').StreamDeckTcp} streamdeck
*/
async addStreamdeckTcpDevice(streamdeck) {
const fakePath = `tcp://${streamdeck.remoteAddress}:${streamdeck.remotePort}`

this.removeDevice(fakePath)

const device = await ElgatoStreamDeckDriver.fromTcp(fakePath, streamdeck)

this.#createSurfaceHandler(fakePath, 'elgato-streamdeck-tcp', device)

setImmediate(() => this.updateDevicesList())

return device
}

/**
* Add a satellite device
* @param {import('./IP/Satellite.js').SatelliteDeviceInfo} deviceInfo
Expand Down Expand Up @@ -1202,6 +1279,8 @@ class SurfaceController extends CoreBase {
}
}

this.#outboundController.quit()

this.#surfaceHandlers.clear()
this.updateDevicesList()
}
Expand Down
1 change: 1 addition & 0 deletions companion/lib/Surface/Handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const PINCODE_NUMBER_POSITIONS_SKIP_FIRST_COL = [
* type: string
* configFields: import('@companion-app/shared/Model/Surfaces.js').CompanionSurfaceConfigField[]
* location?: string
* remotePort?: number
* }} SurfacePanelInfo
* @typedef {{
* info: SurfacePanelInfo
Expand Down
Loading

0 comments on commit 8ae1086

Please sign in to comment.