Skip to content

Commit baf506d

Browse files
authored
feat: streamdeck studio support (#100)
1 parent 4550912 commit baf506d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2088
-142
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ typings/
7474
!.yarn/releases
7575
!.yarn/sdks
7676
!.yarn/versions
77+
78+
hack-*

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
preset: 'ts-jest',
3535

3636
moduleNameMapper: {
37+
'@elgato-stream-deck/node-lib': '<rootDir>/packages/node-lib/src/lib.ts',
3738
'@elgato-stream-deck/(.+)': '<rootDir>/packages/$1/src',
3839
'^(..?/.+).js?$': '$1',
3940
},

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
},
5757
"workspaces": [
5858
"packages/core",
59+
"packages/node-lib",
5960
"packages/node",
61+
"packages/tcp",
6062
"packages/webhid",
6163
"packages/webhid-demo"
6264
],

packages/core/src/__tests__/hid.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import * as EventEmitter from 'eventemitter3'
1+
import { EventEmitter } from 'eventemitter3'
22
import type { EncodeJPEGHelper } from '../models/base.js'
3-
import type { HIDDevice, HIDDeviceEvents, HIDDeviceInfo } from '../hid-device.js'
3+
import type { ChildHIDDeviceInfo, HIDDevice, HIDDeviceEvents, HIDDeviceInfo } from '../hid-device.js'
44
export class DummyHID extends EventEmitter<HIDDeviceEvents> implements HIDDevice {
55
constructor(
66
public readonly path: string,
@@ -25,4 +25,8 @@ export class DummyHID extends EventEmitter<HIDDeviceEvents> implements HIDDevice
2525
public async getDeviceInfo(): Promise<HIDDeviceInfo> {
2626
throw new Error('Method not implemented.')
2727
}
28+
29+
public async getChildDeviceInfo(): Promise<ChildHIDDeviceInfo | null> {
30+
throw new Error('Method not implemented.')
31+
}
2832
}

packages/core/src/__tests__/util.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { transformImageBuffer } from '../util.js'
22

3-
function getSimpleBuffer(dim: number, components: 3 | 4): Buffer {
4-
const buf = Buffer.alloc(dim * dim * components)
3+
function getSimpleBuffer(width: number, height: number, components: 3 | 4): Buffer {
4+
const buf = Buffer.alloc(width * height * components)
55
for (let i = 0; i < buf.length; i++) {
66
buf[i] = i
77
}
88
return buf
99
}
1010
describe('imageToByteArray', () => {
1111
test('basic rgb -> rgba', () => {
12-
const srcBuffer = getSimpleBuffer(2, 3)
12+
const srcBuffer = getSimpleBuffer(2, 2, 3)
1313
const res = transformImageBuffer(
1414
srcBuffer,
1515
{ format: 'rgb', offset: 0, stride: 2 * 3 },
@@ -21,7 +21,7 @@ describe('imageToByteArray', () => {
2121
expect(res).toMatchSnapshot()
2222
})
2323
test('basic rgb -> bgr', () => {
24-
const srcBuffer = getSimpleBuffer(2, 3)
24+
const srcBuffer = getSimpleBuffer(2, 2, 3)
2525
const res = transformImageBuffer(
2626
srcBuffer,
2727
{ format: 'rgb', offset: 0, stride: 2 * 3 },
@@ -33,7 +33,7 @@ describe('imageToByteArray', () => {
3333
expect(res).toMatchSnapshot()
3434
})
3535
test('basic bgra -> bgr', () => {
36-
const srcBuffer = getSimpleBuffer(2, 4)
36+
const srcBuffer = getSimpleBuffer(2, 2, 4)
3737
const res = transformImageBuffer(
3838
srcBuffer,
3939
{ format: 'bgra', offset: 0, stride: 2 * 4 },
@@ -45,7 +45,7 @@ describe('imageToByteArray', () => {
4545
expect(res).toMatchSnapshot()
4646
})
4747
test('basic bgra -> rgba', () => {
48-
const srcBuffer = getSimpleBuffer(2, 4)
48+
const srcBuffer = getSimpleBuffer(2, 2, 4)
4949
const res = transformImageBuffer(
5050
srcBuffer,
5151
{ format: 'bgra', offset: 0, stride: 2 * 4 },
@@ -58,7 +58,7 @@ describe('imageToByteArray', () => {
5858
})
5959

6060
test('basic vflip', () => {
61-
const srcBuffer = getSimpleBuffer(3, 3)
61+
const srcBuffer = getSimpleBuffer(3, 3, 3)
6262
const res = transformImageBuffer(
6363
srcBuffer,
6464
{ format: 'bgr', offset: 0, stride: 3 * 3 },
@@ -71,7 +71,7 @@ describe('imageToByteArray', () => {
7171
})
7272

7373
test('basic xflip', () => {
74-
const srcBuffer = getSimpleBuffer(3, 3)
74+
const srcBuffer = getSimpleBuffer(3, 3, 3)
7575
const res = transformImageBuffer(
7676
srcBuffer,
7777
{ format: 'bgr', offset: 0, stride: 3 * 3 },

packages/core/src/controlDefinition.ts

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export interface StreamDeckEncoderControlDefinition extends StreamDeckControlDef
3838

3939
index: number
4040
hidIndex: number
41+
42+
/** Whether the encoder has an led */
43+
hasLed: boolean
44+
45+
/** The number of steps in encoder led rings (if any) */
46+
ledRingSteps: number
4147
}
4248

4349
export interface StreamDeckLcdSegmentControlDefinition extends StreamDeckControlDefinitionBase {

packages/core/src/controlsGenerator.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function generateButtonsGrid(
66
height: number,
77
pixelSize: Dimension,
88
rtl = false,
9+
columnOffset = 0,
910
): StreamDeckButtonControlDefinition[] {
1011
const controls: StreamDeckButtonControlDefinition[] = []
1112

@@ -17,7 +18,7 @@ export function generateButtonsGrid(
1718
controls.push({
1819
type: 'button',
1920
row,
20-
column,
21+
column: column + columnOffset,
2122
index,
2223
hidIndex,
2324
feedbackType: 'lcd',

packages/core/src/hid-device.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type * as EventEmitter from 'eventemitter3'
1+
import type { EventEmitter } from 'eventemitter3'
22

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

2020
getDeviceInfo(): Promise<HIDDeviceInfo>
21+
22+
getChildDeviceInfo(): Promise<ChildHIDDeviceInfo | null>
2123
}
2224

2325
export interface HIDDeviceInfo {
24-
path: string | undefined
25-
productId: number
26-
vendorId: number
26+
readonly path: string | undefined
27+
readonly productId: number
28+
readonly vendorId: number
29+
}
30+
31+
export interface ChildHIDDeviceInfo extends HIDDeviceInfo {
32+
readonly serialNumber: string
33+
readonly tcpPort: number
2734
}

packages/core/src/id.ts

+13
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,17 @@ export enum DeviceModelId {
1313
PEDAL = 'pedal',
1414
PLUS = 'plus',
1515
NEO = 'neo',
16+
STUDIO = 'studio',
17+
}
18+
19+
export const MODEL_NAMES: { [key in DeviceModelId]: string } = {
20+
[DeviceModelId.ORIGINAL]: 'Stream Deck',
21+
[DeviceModelId.MINI]: 'Stream Deck Mini',
22+
[DeviceModelId.XL]: 'Stream Deck XL',
23+
[DeviceModelId.ORIGINALV2]: 'Stream Deck',
24+
[DeviceModelId.ORIGINALMK2]: 'Stream Deck MK.2',
25+
[DeviceModelId.PLUS]: 'Stream Deck +',
26+
[DeviceModelId.PEDAL]: 'Stream Deck Pedal',
27+
[DeviceModelId.NEO]: 'Stream Deck Neo',
28+
[DeviceModelId.STUDIO]: 'Stream Deck Studio',
1629
}

packages/core/src/index.ts

+49-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { HIDDevice } from './hid-device.js'
2-
import { DeviceModelId } from './id.js'
2+
import { DeviceModelId, MODEL_NAMES } from './id.js'
33
import type { StreamDeck } from './types.js'
44
import type { OpenStreamDeckOptions } from './models/base.js'
55
import { StreamDeckOriginalFactory } from './models/original.js'
@@ -10,13 +10,17 @@ import { StreamDeckOriginalMK2Factory } from './models/original-mk2.js'
1010
import { StreamDeckPlusFactory } from './models/plus.js'
1111
import { StreamDeckPedalFactory } from './models/pedal.js'
1212
import { StreamDeckNeoFactory } from './models/neo.js'
13+
import { StreamDeckStudioFactory } from './models/studio.js'
14+
import type { PropertiesService } from './services/properties/interface.js'
1315

1416
export * from './types.js'
1517
export * from './id.js'
1618
export * from './controlDefinition.js'
17-
export { HIDDevice, HIDDeviceInfo, HIDDeviceEvents } from './hid-device.js'
18-
export { OpenStreamDeckOptions } from './models/base.js'
19+
export type { HIDDevice, HIDDeviceInfo, HIDDeviceEvents, ChildHIDDeviceInfo } from './hid-device.js'
20+
export type { OpenStreamDeckOptions } from './models/base.js'
1921
export { StreamDeckProxy } from './proxy.js'
22+
export type { PropertiesService } from './services/properties/interface.js'
23+
export { uint8ArrayToDataView } from './util.js'
2024

2125
/** Elgato vendor id */
2226
export const VENDOR_ID = 0x0fd9
@@ -30,57 +34,92 @@ export interface DeviceModelSpec {
3034
id: DeviceModelId
3135
type: DeviceModelType
3236
productIds: number[]
33-
factory: (device: HIDDevice, options: Required<OpenStreamDeckOptions>) => StreamDeck
37+
productName: string
38+
39+
factory: (
40+
device: HIDDevice,
41+
options: Required<OpenStreamDeckOptions>,
42+
propertiesService?: PropertiesService,
43+
) => StreamDeck
44+
45+
hasNativeTcp: boolean
3446
}
3547

3648
/** List of all the known models, and the classes to use them */
37-
export const DEVICE_MODELS2: { [key in DeviceModelId]: Omit<DeviceModelSpec, 'id'> } = {
49+
export const DEVICE_MODELS2: { [key in DeviceModelId]: Omit<DeviceModelSpec, 'id' | 'productName'> } = {
3850
[DeviceModelId.ORIGINAL]: {
3951
type: DeviceModelType.STREAMDECK,
4052
productIds: [0x0060],
4153
factory: StreamDeckOriginalFactory,
54+
55+
hasNativeTcp: false,
4256
},
4357
[DeviceModelId.MINI]: {
4458
type: DeviceModelType.STREAMDECK,
4559
productIds: [0x0063, 0x0090],
4660
factory: StreamDeckMiniFactory,
61+
62+
hasNativeTcp: false,
4763
},
4864
[DeviceModelId.XL]: {
4965
type: DeviceModelType.STREAMDECK,
5066
productIds: [0x006c, 0x008f],
5167
factory: StreamDeckXLFactory,
68+
69+
hasNativeTcp: false,
5270
},
5371
[DeviceModelId.ORIGINALV2]: {
5472
type: DeviceModelType.STREAMDECK,
5573
productIds: [0x006d],
5674
factory: StreamDeckOriginalV2Factory,
75+
76+
hasNativeTcp: false,
5777
},
5878
[DeviceModelId.ORIGINALMK2]: {
5979
type: DeviceModelType.STREAMDECK,
6080
productIds: [0x0080],
6181
factory: StreamDeckOriginalMK2Factory,
82+
83+
hasNativeTcp: false,
6284
},
6385
[DeviceModelId.PLUS]: {
6486
type: DeviceModelType.STREAMDECK,
6587
productIds: [0x0084],
6688
factory: StreamDeckPlusFactory,
89+
90+
hasNativeTcp: false,
6791
},
6892
[DeviceModelId.PEDAL]: {
6993
type: DeviceModelType.PEDAL,
7094
productIds: [0x0086],
7195
factory: StreamDeckPedalFactory,
96+
97+
hasNativeTcp: false,
7298
},
7399
[DeviceModelId.NEO]: {
74100
type: DeviceModelType.STREAMDECK,
75101
productIds: [0x009a],
76102
factory: StreamDeckNeoFactory,
103+
104+
hasNativeTcp: false,
105+
},
106+
[DeviceModelId.STUDIO]: {
107+
type: DeviceModelType.STREAMDECK,
108+
productIds: [0x00aa],
109+
factory: StreamDeckStudioFactory,
110+
111+
hasNativeTcp: true,
77112
},
78113
}
79114

80115
/** @deprecated maybe? */
81-
export const DEVICE_MODELS: DeviceModelSpec[] = Object.entries<Omit<DeviceModelSpec, 'id'>>(DEVICE_MODELS2).map(
82-
([id, spec]) => ({
83-
id: id as any as DeviceModelId,
116+
export const DEVICE_MODELS: DeviceModelSpec[] = Object.entries<Omit<DeviceModelSpec, 'id' | 'productName'>>(
117+
DEVICE_MODELS2,
118+
).map(([id, spec]) => {
119+
const modelId = id as any as DeviceModelId
120+
return {
121+
id: modelId,
122+
productName: MODEL_NAMES[modelId],
84123
...spec,
85-
}),
86-
)
124+
}
125+
})

0 commit comments

Comments
 (0)