stagelinq 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.gitattributes +2 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc.js +10 -0
  4. package/.vscode/launch.json +40 -0
  5. package/.vscode/tasks.json +24 -0
  6. package/Databases/Databases.ts +69 -0
  7. package/Databases/DbConnection.ts +42 -0
  8. package/Databases/index.ts +2 -0
  9. package/LICENSE +674 -0
  10. package/LogEmitter/index.ts +51 -0
  11. package/README.md +83 -0
  12. package/StageLinq/index.ts +58 -0
  13. package/TODO.md +5 -0
  14. package/albumArt/index.ts +2 -0
  15. package/albumArt/maybeDownloadFiles.ts +34 -0
  16. package/cli/index.ts +142 -0
  17. package/devices/Player.ts +182 -0
  18. package/devices/PlayerMessageQueue.ts +62 -0
  19. package/index.ts +5 -0
  20. package/network/NetworkDevice.ts +287 -0
  21. package/network/StageLinqDevices.ts +215 -0
  22. package/network/StageLinqListener.ts +50 -0
  23. package/network/announce.ts +133 -0
  24. package/network/index.ts +4 -0
  25. package/package.json +31 -0
  26. package/services/FileTransfer.ts +298 -0
  27. package/services/Service.ts +144 -0
  28. package/services/StateMap.ts +170 -0
  29. package/services/index.ts +3 -0
  30. package/stagelinq.code-workspace +16 -0
  31. package/tsconfig.json +13 -0
  32. package/types/common.ts +240 -0
  33. package/types/database.ts +50 -0
  34. package/types/index.ts +53 -0
  35. package/types/player.ts +41 -0
  36. package/types/tokens.ts +39 -0
  37. package/utils/Context.ts +45 -0
  38. package/utils/ReadContext.ts +114 -0
  39. package/utils/WriteContext.ts +105 -0
  40. package/utils/getTempFilePath.ts +23 -0
  41. package/utils/hex.ts +62 -0
  42. package/utils/index.ts +6 -0
  43. package/utils/log.ts +0 -0
  44. package/utils/sleep.ts +3 -0
  45. package/utils/tcp.ts +17 -0
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "stagelinq",
3
+ "version": "1.0.0",
4
+ "description": "Typescript library to connect to Denon StageLinq devices",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1",
7
+ "start": "node dist/cli/index.js",
8
+ "build": "tsc --build tsconfig.json",
9
+ "watch": "tsc --build tsconfig.json -w"
10
+ },
11
+ "author": "Chris Le (TRIODE)",
12
+ "license": "GNU GPLv3",
13
+ "main": "index.ts",
14
+ "dependencies": {
15
+ "better-sqlite3": "^7.5.0",
16
+ "console-stamp": "^3.0.3",
17
+ "file-type": "^16.5.3",
18
+ "ip": "^1.1.5",
19
+ "minimist": "^1.2.5",
20
+ "promise-socket": "^7.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/assert": "^1.5.5",
24
+ "@types/better-sqlite3": "^7.4.0",
25
+ "@types/ip": "^1.1.0",
26
+ "@types/minimist": "^1.2.2",
27
+ "@types/node": "^16.4.0",
28
+ "prettier": "^2.5.1",
29
+ "typescript": "^4.6.2"
30
+ }
31
+ }
@@ -0,0 +1,298 @@
1
+ import { DOWNLOAD_TIMEOUT } from '../types';
2
+ import { Logger } from '../LogEmitter';
3
+ import { ReadContext } from '../utils/ReadContext';
4
+ import { Service } from './Service';
5
+ import { sleep } from '../utils/sleep';
6
+ import { strict as assert } from 'assert';
7
+ import { WriteContext } from '../utils/WriteContext';
8
+ import type { ServiceMessage, Source } from '../types';
9
+
10
+ const MAGIC_MARKER = 'fltx';
11
+ export const CHUNK_SIZE = 4096;
12
+
13
+ // FIXME: Strongly type this for all possible messages?
14
+ type FileTransferData = any;
15
+
16
+ enum MessageId {
17
+ TimeCode = 0x0,
18
+ FileStat = 0x1,
19
+ EndOfMessage = 0x2,
20
+ SourceLocations = 0x3,
21
+ FileTransferId = 0x4,
22
+ FileTransferChunk = 0x5,
23
+ Unknown0 = 0x8,
24
+ }
25
+
26
+ interface FileTransferProgress {
27
+ sizeLeft: number;
28
+ total: number;
29
+ bytesDownloaded: number;
30
+ percentComplete: number;
31
+ }
32
+
33
+ export declare interface FileTransfer {
34
+ on(event: 'fileTransferProgress', listener: (progress: FileTransferProgress) => void): this;
35
+ }
36
+
37
+ export class FileTransfer extends Service<FileTransferData> {
38
+ private receivedFile: WriteContext = null;
39
+
40
+ async init() {}
41
+
42
+ protected parseData(p_ctx: ReadContext): ServiceMessage<FileTransferData> {
43
+ const check = p_ctx.getString(4);
44
+ assert(check === MAGIC_MARKER);
45
+ const code = p_ctx.readUInt32();
46
+
47
+ // If first 4 bytes are non-zero, a timecode is sent
48
+ if (code > 0) {
49
+ assert(p_ctx.sizeLeft() === 8);
50
+ const id = p_ctx.readUInt32();
51
+ assert(id === 0x07d2);
52
+ assert(p_ctx.readUInt32() === 0);
53
+ return {
54
+ id: MessageId.TimeCode,
55
+ message: {
56
+ timecode: code,
57
+ },
58
+ };
59
+ }
60
+
61
+ // Else
62
+ const messageId: MessageId = p_ctx.readUInt32();
63
+ switch (messageId) {
64
+ case MessageId.SourceLocations: {
65
+ const sources: string[] = [];
66
+ const sourceCount = p_ctx.readUInt32();
67
+ for (let i = 0; i < sourceCount; ++i) {
68
+ // We get a location
69
+ const location = p_ctx.readNetworkStringUTF16();
70
+ sources.push(location);
71
+ }
72
+ // Final three bytes should be 0x1 0x1 0x1
73
+ assert(p_ctx.readUInt8() === 0x1);
74
+ assert(p_ctx.readUInt8() === 0x1);
75
+ assert(p_ctx.readUInt8() === 0x1);
76
+ assert(p_ctx.isEOF());
77
+ return {
78
+ id: messageId,
79
+ message: {
80
+ sources: sources,
81
+ },
82
+ };
83
+ }
84
+
85
+ case MessageId.FileStat: {
86
+ assert(p_ctx.sizeLeft() === 53);
87
+ // Last 4 bytes (FAT32) indicate size of file
88
+ p_ctx.seek(49);
89
+ const size = p_ctx.readUInt32();
90
+ return {
91
+ id: messageId,
92
+ message: {
93
+ size: size,
94
+ },
95
+ };
96
+ }
97
+
98
+ case MessageId.EndOfMessage: {
99
+ // End of result indication?
100
+ return {
101
+ id: messageId,
102
+ message: null,
103
+ };
104
+ }
105
+
106
+ case MessageId.FileTransferId: {
107
+ assert(p_ctx.sizeLeft() === 12);
108
+ assert(p_ctx.readUInt32() === 0x0);
109
+ const filesize = p_ctx.readUInt32();
110
+ const id = p_ctx.readUInt32();
111
+
112
+ return {
113
+ id: messageId,
114
+ message: {
115
+ size: filesize,
116
+ txid: id,
117
+ },
118
+ };
119
+ }
120
+
121
+ case MessageId.FileTransferChunk: {
122
+ assert(p_ctx.readUInt32() === 0x0);
123
+ const offset = p_ctx.readUInt32();
124
+ const chunksize = p_ctx.readUInt32();
125
+ assert(chunksize === p_ctx.sizeLeft());
126
+ assert(p_ctx.sizeLeft() <= CHUNK_SIZE);
127
+
128
+ return {
129
+ id: messageId,
130
+ message: {
131
+ data: p_ctx.readRemainingAsNewBuffer(),
132
+ offset: offset,
133
+ size: chunksize,
134
+ },
135
+ };
136
+ }
137
+
138
+ case MessageId.Unknown0: {
139
+ return {
140
+ id: messageId,
141
+ message: null,
142
+ };
143
+ }
144
+
145
+ default:
146
+ {
147
+ assert.fail(`File Transfer Unhandled message id '${messageId}'`);
148
+ }
149
+ break;
150
+ }
151
+ }
152
+
153
+ protected messageHandler(p_data: ServiceMessage<FileTransferData>): void {
154
+ if (p_data.id === MessageId.FileTransferChunk && this.receivedFile) {
155
+ assert(this.receivedFile.sizeLeft() >= p_data.message.size);
156
+ this.receivedFile.write(p_data.message.data);
157
+ } else {
158
+ // Logger.log(p_data);
159
+ }
160
+ }
161
+
162
+ async getFile(p_location: string): Promise<Uint8Array> {
163
+ assert(this.receivedFile === null);
164
+
165
+ await this.requestFileTransferId(p_location);
166
+ const txinfo = await this.waitForMessage(MessageId.FileTransferId);
167
+
168
+ if (txinfo) {
169
+ this.receivedFile = new WriteContext({ size: txinfo.size });
170
+
171
+ const totalChunks = Math.ceil(txinfo.size / CHUNK_SIZE);
172
+
173
+ await this.requestChunkRange(txinfo.txid, 0, totalChunks - 1);
174
+
175
+ try {
176
+ await new Promise(async (resolve, reject) => {
177
+ setTimeout(() => {
178
+ reject(new Error(`Failed to download '${p_location}'`));
179
+ }, DOWNLOAD_TIMEOUT);
180
+
181
+ while (this.receivedFile.isEOF() === false) {
182
+ const total = parseInt(txinfo.size);
183
+ const bytesDownloaded = total - this.receivedFile.sizeLeft();
184
+ const percentComplete = (bytesDownloaded / total) * 100;
185
+ this.emit('fileTransferProgress', {
186
+ sizeLeft: this.receivedFile.sizeLeft(),
187
+ total: txinfo.size,
188
+ bytesDownloaded: bytesDownloaded,
189
+ percentComplete: percentComplete
190
+ })
191
+ Logger.debug(`Reading ${p_location} progressComplete=${Math.ceil(percentComplete)}% ${bytesDownloaded}/${total}`);
192
+ await sleep(200);
193
+ }
194
+ Logger.debug(`Download complete.`);
195
+ resolve(true);
196
+ });
197
+ } catch (err) {
198
+ const msg = `Could not read database from ${p_location}: ${err.message}`
199
+ Logger.error(msg);
200
+ throw new Error(msg);
201
+ }
202
+
203
+ Logger.debug(`Signaling transfer complete.`);
204
+ await this.signalTransferComplete();
205
+ }
206
+
207
+ const buf = this.receivedFile ? this.receivedFile.getBuffer() : null;
208
+ this.receivedFile = null;
209
+ return buf;
210
+ }
211
+
212
+ async getSources(): Promise<Source[]> {
213
+ const result: Source[] = [];
214
+
215
+ await this.requestSources();
216
+ const message = await this.waitForMessage(MessageId.SourceLocations);
217
+ if (message) {
218
+ for (const source of message.sources) {
219
+ //try to retrieve V2.x Database2/m.db first. If file doesn't exist or 0 size, retrieve V1.x /m.db
220
+ const databases = [`/${source}/Engine Library/Database2/m.db`, `/${source}/Engine Library/m.db`];
221
+ for (const database of databases) {
222
+ await this.requestStat(database);
223
+ const fstatMessage = await this.waitForMessage(MessageId.FileStat);
224
+ if (fstatMessage.size > 0) {
225
+ result.push({
226
+ name: source,
227
+ database: {
228
+ location: database,
229
+ size: fstatMessage.size,
230
+ },
231
+ });
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ ///////////////////////////////////////////////////////////////////////////
242
+ // Private methods
243
+
244
+ private async requestStat(p_filepath: string): Promise<void> {
245
+ // 0x7d1: seems to request some sort of fstat on a file
246
+ const ctx = new WriteContext();
247
+ ctx.writeFixedSizedString(MAGIC_MARKER);
248
+ ctx.writeUInt32(0x0);
249
+ ctx.writeUInt32(0x7d1);
250
+ ctx.writeNetworkStringUTF16(p_filepath);
251
+ await this.writeWithLength(ctx);
252
+ }
253
+
254
+ private async requestSources(): Promise<void> {
255
+ // 0x7d2: Request available sources
256
+ const ctx = new WriteContext();
257
+ ctx.writeFixedSizedString(MAGIC_MARKER);
258
+ ctx.writeUInt32(0x0);
259
+ ctx.writeUInt32(0x7d2); // Database query
260
+ ctx.writeUInt32(0x0);
261
+ await this.writeWithLength(ctx);
262
+ }
263
+
264
+ private async requestFileTransferId(p_filepath: string): Promise<void> {
265
+ // 0x7d4: Request transfer id?
266
+ const ctx = new WriteContext();
267
+ ctx.writeFixedSizedString(MAGIC_MARKER);
268
+ ctx.writeUInt32(0x0);
269
+ ctx.writeUInt32(0x7d4);
270
+ ctx.writeNetworkStringUTF16(p_filepath);
271
+ ctx.writeUInt32(0x0); // Not sure why we need 0x0 here
272
+ await this.writeWithLength(ctx);
273
+ }
274
+
275
+ private async requestChunkRange(p_txid: number, p_chunkStartId: number, p_chunkEndId: number): Promise<void> {
276
+ // 0x7d5: seems to be the code to request chunk range
277
+ const ctx = new WriteContext();
278
+ ctx.writeFixedSizedString(MAGIC_MARKER);
279
+ ctx.writeUInt32(0x0);
280
+ ctx.writeUInt32(0x7d5);
281
+ ctx.writeUInt32(0x0);
282
+ ctx.writeUInt32(p_txid); // I assume this is the transferid
283
+ ctx.writeUInt32(0x0);
284
+ ctx.writeUInt32(p_chunkStartId);
285
+ ctx.writeUInt32(0x0);
286
+ ctx.writeUInt32(p_chunkEndId);
287
+ await this.writeWithLength(ctx);
288
+ }
289
+
290
+ private async signalTransferComplete(): Promise<void> {
291
+ // 0x7d6: seems to be the code to signal transfer completed
292
+ const ctx = new WriteContext();
293
+ ctx.writeFixedSizedString(MAGIC_MARKER);
294
+ ctx.writeUInt32(0x0);
295
+ ctx.writeUInt32(0x7d6);
296
+ await this.writeWithLength(ctx);
297
+ }
298
+ }
@@ -0,0 +1,144 @@
1
+ //import { hex } from '../utils/hex';
2
+ import { EventEmitter } from 'events';
3
+ import { Logger } from '../LogEmitter';
4
+ import { MessageId, MESSAGE_TIMEOUT, Tokens } from '../types';
5
+ import { NetworkDevice } from '../network/NetworkDevice';
6
+ import { ReadContext } from '../utils/ReadContext';
7
+ import { strict as assert } from 'assert';
8
+ import { WriteContext } from '../utils/WriteContext';
9
+ import * as tcp from '../utils/tcp';
10
+ import type { ServiceMessage } from '../types';
11
+
12
+ export abstract class Service<T> extends EventEmitter {
13
+ private address: string;
14
+ private port: number;
15
+ public readonly name: string;
16
+ protected controller: NetworkDevice;
17
+ protected connection: tcp.Connection = null;
18
+
19
+ constructor(p_address: string, p_port: number, p_controller: NetworkDevice) {
20
+ super();
21
+ this.address = p_address;
22
+ this.port = p_port;
23
+ this.name = this.constructor.name;
24
+ this.controller = p_controller;
25
+ }
26
+
27
+ async connect(): Promise<void> {
28
+ assert(!this.connection);
29
+ this.connection = await tcp.connect(this.address, this.port);
30
+ let queue: Buffer = null;
31
+
32
+ this.connection.socket.on('data', (p_data: Buffer) => {
33
+ let buffer: Buffer = null;
34
+ if (queue && queue.length > 0) {
35
+ buffer = Buffer.concat([queue, p_data]);
36
+ } else {
37
+ buffer = p_data;
38
+ }
39
+
40
+ // FIXME: Clean up this arraybuffer confusion mess
41
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
42
+ const ctx = new ReadContext(arrayBuffer, false);
43
+ queue = null;
44
+
45
+ try {
46
+ while (ctx.isEOF() === false) {
47
+ if (ctx.sizeLeft() < 4) {
48
+ queue = ctx.readRemainingAsNewBuffer();
49
+ break;
50
+ }
51
+
52
+ const length = ctx.readUInt32();
53
+ if (length <= ctx.sizeLeft()) {
54
+ const message = ctx.read(length);
55
+ // Use slice to get an actual copy of the message instead of working on the shared underlying ArrayBuffer
56
+ const data = message.buffer.slice(message.byteOffset, message.byteOffset + length);
57
+ // Logger.info("RECV", length);
58
+ //hex(message);
59
+ const parsedData = this.parseData(new ReadContext(data, false));
60
+
61
+ // Forward parsed data to message handler
62
+ this.messageHandler(parsedData);
63
+ this.emit('message', parsedData);
64
+ } else {
65
+ ctx.seek(-4); // Rewind 4 bytes to include the length again
66
+ queue = ctx.readRemainingAsNewBuffer();
67
+ break;
68
+ }
69
+ }
70
+ } catch (err) {
71
+ // FIXME: Rethrow based on the severity?
72
+ Logger.error(err);
73
+ }
74
+ });
75
+
76
+ // FIXME: Is this required for all Services?
77
+ const ctx = new WriteContext();
78
+ ctx.writeUInt32(MessageId.ServicesAnnouncement);
79
+ ctx.write(Tokens.SoundSwitch);
80
+ ctx.writeNetworkStringUTF16(this.name);
81
+ ctx.writeUInt16(this.connection.socket.localPort); // FIXME: In the Go code this is the local TCP port, but 0 or any other 16 bit value seems to work fine as well
82
+ await this.write(ctx);
83
+
84
+ await this.init();
85
+
86
+ Logger.debug(`Connected to service '${this.name}' at port ${this.port}`);
87
+ }
88
+
89
+ disconnect() {
90
+ assert(this.connection);
91
+ try {
92
+ this.connection.destroy();
93
+ } catch (e) {
94
+ Logger.error('Error disconnecting', e);
95
+ } finally {
96
+ this.connection = null;
97
+ }
98
+ }
99
+
100
+ async waitForMessage(p_messageId: number): Promise<T> {
101
+ return await new Promise((resolve, reject) => {
102
+ const listener = (p_message: ServiceMessage<T>) => {
103
+ if (p_message.id === p_messageId) {
104
+ this.removeListener('message', listener);
105
+ resolve(p_message.message);
106
+ }
107
+ };
108
+ this.addListener('message', listener);
109
+ setTimeout(() => {
110
+ reject(new Error(`Failed to receive message '${p_messageId}' on time`));
111
+ }, MESSAGE_TIMEOUT);
112
+ });
113
+ }
114
+
115
+ async write(p_ctx: WriteContext) {
116
+ assert(p_ctx.isLittleEndian() === false);
117
+ assert(this.connection);
118
+ const buf = p_ctx.getBuffer();
119
+ // Logger.info("SEND");
120
+ //hex(buf);
121
+ const written = await this.connection.write(buf);
122
+ assert(written === buf.byteLength);
123
+ return written;
124
+ }
125
+
126
+ async writeWithLength(p_ctx: WriteContext) {
127
+ assert(p_ctx.isLittleEndian() === false);
128
+ assert(this.connection);
129
+ const newCtx = new WriteContext({ size: p_ctx.tell() + 4, autoGrow: false });
130
+ newCtx.writeUInt32(p_ctx.tell());
131
+ newCtx.write(p_ctx.getBuffer());
132
+ assert(newCtx.isEOF());
133
+ return await this.write(newCtx);
134
+ }
135
+
136
+ // FIXME: Cannot use abstract because of async; is there another way to get this?
137
+ protected async init() {
138
+ assert.fail('Implement this');
139
+ }
140
+
141
+ protected abstract parseData(p_ctx: ReadContext): ServiceMessage<T>;
142
+
143
+ protected abstract messageHandler(p_data: ServiceMessage<T>): void;
144
+ }
@@ -0,0 +1,170 @@
1
+ import { strict as assert } from 'assert';
2
+ import { StageLinqValue } from '../types';
3
+ import { ReadContext } from '../utils/ReadContext';
4
+ import { WriteContext } from '../utils/WriteContext';
5
+ import { Service } from './Service';
6
+ import type { ServiceMessage } from '../types';
7
+ // import { Logger } from '../LogEmitter';
8
+
9
+ export const States = [
10
+ // Mixer
11
+ StageLinqValue.MixerCH1faderPosition,
12
+ StageLinqValue.MixerCH2faderPosition,
13
+ StageLinqValue.MixerCrossfaderPosition,
14
+
15
+ // Decks
16
+ StageLinqValue.EngineDeck1Play,
17
+ StageLinqValue.EngineDeck1PlayState,
18
+ StageLinqValue.EngineDeck1PlayStatePath,
19
+ StageLinqValue.EngineDeck1TrackArtistName,
20
+ StageLinqValue.EngineDeck1TrackTrackNetworkPath,
21
+ StageLinqValue.EngineDeck1TrackSongLoaded,
22
+ StageLinqValue.EngineDeck1TrackSongName,
23
+ StageLinqValue.EngineDeck1TrackTrackData,
24
+ StageLinqValue.EngineDeck1TrackTrackName,
25
+ StageLinqValue.EngineDeck1CurrentBPM,
26
+ StageLinqValue.EngineDeck1ExternalMixerVolume,
27
+
28
+ StageLinqValue.EngineDeck2Play,
29
+ StageLinqValue.EngineDeck2PlayState,
30
+ StageLinqValue.EngineDeck2PlayStatePath,
31
+ StageLinqValue.EngineDeck2TrackArtistName,
32
+ StageLinqValue.EngineDeck2TrackTrackNetworkPath,
33
+ StageLinqValue.EngineDeck2TrackSongLoaded,
34
+ StageLinqValue.EngineDeck2TrackSongName,
35
+ StageLinqValue.EngineDeck2TrackTrackData,
36
+ StageLinqValue.EngineDeck2TrackTrackName,
37
+ StageLinqValue.EngineDeck2CurrentBPM,
38
+ StageLinqValue.EngineDeck2ExternalMixerVolume,
39
+
40
+ StageLinqValue.EngineDeck3Play,
41
+ StageLinqValue.EngineDeck3PlayState,
42
+ StageLinqValue.EngineDeck3PlayStatePath,
43
+ StageLinqValue.EngineDeck3TrackArtistName,
44
+ StageLinqValue.EngineDeck3TrackTrackNetworkPath,
45
+ StageLinqValue.EngineDeck3TrackSongLoaded,
46
+ StageLinqValue.EngineDeck3TrackSongName,
47
+ StageLinqValue.EngineDeck3TrackTrackData,
48
+ StageLinqValue.EngineDeck3TrackTrackName,
49
+ StageLinqValue.EngineDeck3CurrentBPM,
50
+ StageLinqValue.EngineDeck3ExternalMixerVolume,
51
+
52
+ StageLinqValue.EngineDeck4Play,
53
+ StageLinqValue.EngineDeck4PlayState,
54
+ StageLinqValue.EngineDeck4PlayStatePath,
55
+ StageLinqValue.EngineDeck4TrackArtistName,
56
+ StageLinqValue.EngineDeck4TrackTrackNetworkPath,
57
+ StageLinqValue.EngineDeck4TrackSongLoaded,
58
+ StageLinqValue.EngineDeck4TrackSongName,
59
+ StageLinqValue.EngineDeck4TrackTrackData,
60
+ StageLinqValue.EngineDeck4TrackTrackName,
61
+ StageLinqValue.EngineDeck4CurrentBPM,
62
+ StageLinqValue.EngineDeck4ExternalMixerVolume,
63
+
64
+ StageLinqValue.ClientPreferencesLayerA,
65
+ StageLinqValue.ClientPreferencesPlayer,
66
+ StageLinqValue.ClientPreferencesPlayerJogColorA,
67
+ StageLinqValue.ClientPreferencesPlayerJogColorB,
68
+ StageLinqValue.EngineDeck1DeckIsMaster,
69
+ StageLinqValue.EngineDeck2DeckIsMaster,
70
+ StageLinqValue.EngineMasterMasterTempo,
71
+ StageLinqValue.EngineSyncNetworkMasterStatus,
72
+ StageLinqValue.MixerChannelAssignment1,
73
+ StageLinqValue.MixerChannelAssignment2,
74
+ StageLinqValue.MixerChannelAssignment3,
75
+ StageLinqValue.MixerChannelAssignment4,
76
+ StageLinqValue.MixerNumberOfChannels,
77
+
78
+ ];
79
+
80
+ const MAGIC_MARKER = 'smaa';
81
+ // FIXME: Is this thing really an interval?
82
+ const MAGIC_MARKER_INTERVAL = 0x000007d2;
83
+ const MAGIC_MARKER_JSON = 0x00000000;
84
+
85
+ export interface StateData {
86
+ name: string;
87
+ json?: {
88
+ type: number;
89
+ string?: string;
90
+ value?: number;
91
+ };
92
+ interval?: number;
93
+ }
94
+
95
+ export class StateMap extends Service<StateData> {
96
+ async init() {
97
+ for (const state of States) {
98
+ await this.subscribeState(state, 0);
99
+ }
100
+ }
101
+
102
+ protected parseData(p_ctx: ReadContext): ServiceMessage<StateData> {
103
+ const marker = p_ctx.getString(4);
104
+ assert(marker === MAGIC_MARKER);
105
+
106
+ const type = p_ctx.readUInt32();
107
+ switch (type) {
108
+ case MAGIC_MARKER_JSON: {
109
+ const name = p_ctx.readNetworkStringUTF16();
110
+ const json = JSON.parse(p_ctx.readNetworkStringUTF16());
111
+ return {
112
+ id: MAGIC_MARKER_JSON,
113
+ message: {
114
+ name: name,
115
+ json: json,
116
+ },
117
+ };
118
+ }
119
+
120
+ case MAGIC_MARKER_INTERVAL: {
121
+ const name = p_ctx.readNetworkStringUTF16();
122
+ const interval = p_ctx.readInt32();
123
+ return {
124
+ id: MAGIC_MARKER_INTERVAL,
125
+ message: {
126
+ name: name,
127
+ interval: interval,
128
+ },
129
+ };
130
+ }
131
+
132
+ default:
133
+ break;
134
+ }
135
+ assert.fail(`Unhandled type ${type}`);
136
+ return null;
137
+ }
138
+
139
+ protected messageHandler(_: ServiceMessage<StateData>): void {
140
+ // Logger.debug(
141
+ // `${p_data.message.name} => ${
142
+ // p_data.message.json ? JSON.stringify(p_data.message.json) : p_data.message.interval
143
+ // }`
144
+ // );
145
+ }
146
+
147
+ private async subscribeState(p_state: string, p_interval: number) {
148
+ // Logger.log(`Subscribe to state '${p_state}'`);
149
+ const getMessage = function (): Buffer {
150
+ const ctx = new WriteContext();
151
+ ctx.writeFixedSizedString(MAGIC_MARKER);
152
+ ctx.writeUInt32(MAGIC_MARKER_INTERVAL);
153
+ ctx.writeNetworkStringUTF16(p_state);
154
+ ctx.writeUInt32(p_interval);
155
+ return ctx.getBuffer();
156
+ };
157
+
158
+ const message = getMessage();
159
+ {
160
+ const ctx = new WriteContext();
161
+ ctx.writeUInt32(message.length);
162
+ const written = await this.connection.write(ctx.getBuffer());
163
+ assert(written === 4);
164
+ }
165
+ {
166
+ const written = await this.connection.write(message);
167
+ assert(written === message.length);
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,3 @@
1
+ export * from './FileTransfer';
2
+ export * from './Service';
3
+ export * from './StateMap';
@@ -0,0 +1,16 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ },
6
+ {
7
+ "path": "../stagelinq-pcap"
8
+ },
9
+ {
10
+ "path": "../go-stagelinq"
11
+ }
12
+ ],
13
+ "settings": {
14
+ "task.allowAutomaticTasks": "on"
15
+ }
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "outDir": "dist",
6
+ "sourceMap": true,
7
+ "allowJs": false,
8
+ "alwaysStrict": true,
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true
12
+ }
13
+ }