Skip to content

Commit

Permalink
feat: streamdeck studio support (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian authored Sep 8, 2024
1 parent 4550912 commit baf506d
Show file tree
Hide file tree
Showing 70 changed files with 2,088 additions and 142 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ typings/
!.yarn/releases
!.yarn/sdks
!.yarn/versions

hack-*
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
preset: 'ts-jest',

moduleNameMapper: {
'@elgato-stream-deck/node-lib': '<rootDir>/packages/node-lib/src/lib.ts',
'@elgato-stream-deck/(.+)': '<rootDir>/packages/$1/src',
'^(..?/.+).js?$': '$1',
},
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
},
"workspaces": [
"packages/core",
"packages/node-lib",
"packages/node",
"packages/tcp",
"packages/webhid",
"packages/webhid-demo"
],
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/__tests__/hid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as EventEmitter from 'eventemitter3'
import { EventEmitter } from 'eventemitter3'
import type { EncodeJPEGHelper } from '../models/base.js'
import type { HIDDevice, HIDDeviceEvents, HIDDeviceInfo } from '../hid-device.js'
import type { ChildHIDDeviceInfo, HIDDevice, HIDDeviceEvents, HIDDeviceInfo } from '../hid-device.js'
export class DummyHID extends EventEmitter<HIDDeviceEvents> implements HIDDevice {
constructor(
public readonly path: string,
Expand All @@ -25,4 +25,8 @@ export class DummyHID extends EventEmitter<HIDDeviceEvents> implements HIDDevice
public async getDeviceInfo(): Promise<HIDDeviceInfo> {
throw new Error('Method not implemented.')
}

public async getChildDeviceInfo(): Promise<ChildHIDDeviceInfo | null> {
throw new Error('Method not implemented.')
}
}
16 changes: 8 additions & 8 deletions packages/core/src/__tests__/util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { transformImageBuffer } from '../util.js'

function getSimpleBuffer(dim: number, components: 3 | 4): Buffer {
const buf = Buffer.alloc(dim * dim * components)
function getSimpleBuffer(width: number, height: number, components: 3 | 4): Buffer {
const buf = Buffer.alloc(width * height * components)
for (let i = 0; i < buf.length; i++) {
buf[i] = i
}
return buf
}
describe('imageToByteArray', () => {
test('basic rgb -> rgba', () => {
const srcBuffer = getSimpleBuffer(2, 3)
const srcBuffer = getSimpleBuffer(2, 2, 3)
const res = transformImageBuffer(
srcBuffer,
{ format: 'rgb', offset: 0, stride: 2 * 3 },
Expand All @@ -21,7 +21,7 @@ describe('imageToByteArray', () => {
expect(res).toMatchSnapshot()
})
test('basic rgb -> bgr', () => {
const srcBuffer = getSimpleBuffer(2, 3)
const srcBuffer = getSimpleBuffer(2, 2, 3)
const res = transformImageBuffer(
srcBuffer,
{ format: 'rgb', offset: 0, stride: 2 * 3 },
Expand All @@ -33,7 +33,7 @@ describe('imageToByteArray', () => {
expect(res).toMatchSnapshot()
})
test('basic bgra -> bgr', () => {
const srcBuffer = getSimpleBuffer(2, 4)
const srcBuffer = getSimpleBuffer(2, 2, 4)
const res = transformImageBuffer(
srcBuffer,
{ format: 'bgra', offset: 0, stride: 2 * 4 },
Expand All @@ -45,7 +45,7 @@ describe('imageToByteArray', () => {
expect(res).toMatchSnapshot()
})
test('basic bgra -> rgba', () => {
const srcBuffer = getSimpleBuffer(2, 4)
const srcBuffer = getSimpleBuffer(2, 2, 4)
const res = transformImageBuffer(
srcBuffer,
{ format: 'bgra', offset: 0, stride: 2 * 4 },
Expand All @@ -58,7 +58,7 @@ describe('imageToByteArray', () => {
})

test('basic vflip', () => {
const srcBuffer = getSimpleBuffer(3, 3)
const srcBuffer = getSimpleBuffer(3, 3, 3)
const res = transformImageBuffer(
srcBuffer,
{ format: 'bgr', offset: 0, stride: 3 * 3 },
Expand All @@ -71,7 +71,7 @@ describe('imageToByteArray', () => {
})

test('basic xflip', () => {
const srcBuffer = getSimpleBuffer(3, 3)
const srcBuffer = getSimpleBuffer(3, 3, 3)
const res = transformImageBuffer(
srcBuffer,
{ format: 'bgr', offset: 0, stride: 3 * 3 },
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/controlDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export interface StreamDeckEncoderControlDefinition extends StreamDeckControlDef

index: number
hidIndex: number

/** Whether the encoder has an led */
hasLed: boolean

/** The number of steps in encoder led rings (if any) */
ledRingSteps: number
}

export interface StreamDeckLcdSegmentControlDefinition extends StreamDeckControlDefinitionBase {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/controlsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function generateButtonsGrid(
height: number,
pixelSize: Dimension,
rtl = false,
columnOffset = 0,
): StreamDeckButtonControlDefinition[] {
const controls: StreamDeckButtonControlDefinition[] = []

Expand All @@ -17,7 +18,7 @@ export function generateButtonsGrid(
controls.push({
type: 'button',
row,
column,
column: column + columnOffset,
index,
hidIndex,
feedbackType: 'lcd',
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/hid-device.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as EventEmitter from 'eventemitter3'
import type { EventEmitter } from 'eventemitter3'

export interface HIDDeviceEvents {
error: [data: any]
Expand All @@ -18,10 +18,17 @@ export interface HIDDevice extends EventEmitter<HIDDeviceEvents> {
sendReports(buffers: Uint8Array[]): Promise<void>

getDeviceInfo(): Promise<HIDDeviceInfo>

getChildDeviceInfo(): Promise<ChildHIDDeviceInfo | null>
}

export interface HIDDeviceInfo {
path: string | undefined
productId: number
vendorId: number
readonly path: string | undefined
readonly productId: number
readonly vendorId: number
}

export interface ChildHIDDeviceInfo extends HIDDeviceInfo {
readonly serialNumber: string
readonly tcpPort: number
}
13 changes: 13 additions & 0 deletions packages/core/src/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,17 @@ export enum DeviceModelId {
PEDAL = 'pedal',
PLUS = 'plus',
NEO = 'neo',
STUDIO = 'studio',
}

export const MODEL_NAMES: { [key in DeviceModelId]: string } = {
[DeviceModelId.ORIGINAL]: 'Stream Deck',
[DeviceModelId.MINI]: 'Stream Deck Mini',
[DeviceModelId.XL]: 'Stream Deck XL',
[DeviceModelId.ORIGINALV2]: 'Stream Deck',
[DeviceModelId.ORIGINALMK2]: 'Stream Deck MK.2',
[DeviceModelId.PLUS]: 'Stream Deck +',
[DeviceModelId.PEDAL]: 'Stream Deck Pedal',
[DeviceModelId.NEO]: 'Stream Deck Neo',
[DeviceModelId.STUDIO]: 'Stream Deck Studio',
}
59 changes: 49 additions & 10 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HIDDevice } from './hid-device.js'
import { DeviceModelId } from './id.js'
import { DeviceModelId, MODEL_NAMES } from './id.js'
import type { StreamDeck } from './types.js'
import type { OpenStreamDeckOptions } from './models/base.js'
import { StreamDeckOriginalFactory } from './models/original.js'
Expand All @@ -10,13 +10,17 @@ import { StreamDeckOriginalMK2Factory } from './models/original-mk2.js'
import { StreamDeckPlusFactory } from './models/plus.js'
import { StreamDeckPedalFactory } from './models/pedal.js'
import { StreamDeckNeoFactory } from './models/neo.js'
import { StreamDeckStudioFactory } from './models/studio.js'
import type { PropertiesService } from './services/properties/interface.js'

export * from './types.js'
export * from './id.js'
export * from './controlDefinition.js'
export { HIDDevice, HIDDeviceInfo, HIDDeviceEvents } from './hid-device.js'
export { OpenStreamDeckOptions } from './models/base.js'
export type { HIDDevice, HIDDeviceInfo, HIDDeviceEvents, ChildHIDDeviceInfo } from './hid-device.js'
export type { OpenStreamDeckOptions } from './models/base.js'
export { StreamDeckProxy } from './proxy.js'
export type { PropertiesService } from './services/properties/interface.js'
export { uint8ArrayToDataView } from './util.js'

/** Elgato vendor id */
export const VENDOR_ID = 0x0fd9
Expand All @@ -30,57 +34,92 @@ export interface DeviceModelSpec {
id: DeviceModelId
type: DeviceModelType
productIds: number[]
factory: (device: HIDDevice, options: Required<OpenStreamDeckOptions>) => StreamDeck
productName: string

factory: (
device: HIDDevice,
options: Required<OpenStreamDeckOptions>,
propertiesService?: PropertiesService,
) => StreamDeck

hasNativeTcp: boolean
}

/** List of all the known models, and the classes to use them */
export const DEVICE_MODELS2: { [key in DeviceModelId]: Omit<DeviceModelSpec, 'id'> } = {
export const DEVICE_MODELS2: { [key in DeviceModelId]: Omit<DeviceModelSpec, 'id' | 'productName'> } = {
[DeviceModelId.ORIGINAL]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x0060],
factory: StreamDeckOriginalFactory,

hasNativeTcp: false,
},
[DeviceModelId.MINI]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x0063, 0x0090],
factory: StreamDeckMiniFactory,

hasNativeTcp: false,
},
[DeviceModelId.XL]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x006c, 0x008f],
factory: StreamDeckXLFactory,

hasNativeTcp: false,
},
[DeviceModelId.ORIGINALV2]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x006d],
factory: StreamDeckOriginalV2Factory,

hasNativeTcp: false,
},
[DeviceModelId.ORIGINALMK2]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x0080],
factory: StreamDeckOriginalMK2Factory,

hasNativeTcp: false,
},
[DeviceModelId.PLUS]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x0084],
factory: StreamDeckPlusFactory,

hasNativeTcp: false,
},
[DeviceModelId.PEDAL]: {
type: DeviceModelType.PEDAL,
productIds: [0x0086],
factory: StreamDeckPedalFactory,

hasNativeTcp: false,
},
[DeviceModelId.NEO]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x009a],
factory: StreamDeckNeoFactory,

hasNativeTcp: false,
},
[DeviceModelId.STUDIO]: {
type: DeviceModelType.STREAMDECK,
productIds: [0x00aa],
factory: StreamDeckStudioFactory,

hasNativeTcp: true,
},
}

/** @deprecated maybe? */
export const DEVICE_MODELS: DeviceModelSpec[] = Object.entries<Omit<DeviceModelSpec, 'id'>>(DEVICE_MODELS2).map(
([id, spec]) => ({
id: id as any as DeviceModelId,
export const DEVICE_MODELS: DeviceModelSpec[] = Object.entries<Omit<DeviceModelSpec, 'id' | 'productName'>>(
DEVICE_MODELS2,
).map(([id, spec]) => {
const modelId = id as any as DeviceModelId
return {
id: modelId,
productName: MODEL_NAMES[modelId],
...spec,
}),
)
}
})
Loading

0 comments on commit baf506d

Please sign in to comment.