@djodjonx/x32-simulator 0.0.1

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 (86) hide show
  1. package/.commitlintrc.json +3 -0
  2. package/.github/workflows/publish.yml +38 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.oxlintrc.json +56 -0
  6. package/CHANGELOG.md +11 -0
  7. package/INSTALL.md +107 -0
  8. package/LICENSE +21 -0
  9. package/README.md +141 -0
  10. package/dist/UdpNetworkGateway-BrroQ6-Q.mjs +1189 -0
  11. package/dist/UdpNetworkGateway-Ccdd7Us5.cjs +1265 -0
  12. package/dist/index.cjs +7 -0
  13. package/dist/index.d.cts +207 -0
  14. package/dist/index.d.mts +207 -0
  15. package/dist/index.mjs +3 -0
  16. package/dist/server.cjs +1060 -0
  17. package/dist/server.d.cts +10 -0
  18. package/dist/server.d.mts +10 -0
  19. package/dist/server.mjs +1055 -0
  20. package/docs/OSC-Communication.md +184 -0
  21. package/docs/X32-INTERNAL.md +262 -0
  22. package/docs/X32-OSC.pdf +0 -0
  23. package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
  24. package/package.json +68 -0
  25. package/src/application/use-cases/BroadcastUpdatesUseCase.ts +120 -0
  26. package/src/application/use-cases/ManageSessionsUseCase.ts +9 -0
  27. package/src/application/use-cases/ProcessPacketUseCase.ts +26 -0
  28. package/src/application/use-cases/SimulationService.ts +122 -0
  29. package/src/domain/entities/SubscriptionManager.ts +126 -0
  30. package/src/domain/entities/X32State.ts +78 -0
  31. package/src/domain/models/MeterConfig.ts +22 -0
  32. package/src/domain/models/MeterData.ts +59 -0
  33. package/src/domain/models/OscMessage.ts +93 -0
  34. package/src/domain/models/X32Address.ts +78 -0
  35. package/src/domain/models/X32Node.ts +43 -0
  36. package/src/domain/models/types.ts +96 -0
  37. package/src/domain/ports/ILogger.ts +27 -0
  38. package/src/domain/ports/INetworkGateway.ts +8 -0
  39. package/src/domain/ports/IStateRepository.ts +16 -0
  40. package/src/domain/services/MeterService.ts +46 -0
  41. package/src/domain/services/OscMessageHandler.ts +88 -0
  42. package/src/domain/services/SchemaFactory.ts +308 -0
  43. package/src/domain/services/SchemaRegistry.ts +67 -0
  44. package/src/domain/services/StaticResponseService.ts +52 -0
  45. package/src/domain/services/strategies/BatchStrategy.ts +74 -0
  46. package/src/domain/services/strategies/MeterStrategy.ts +45 -0
  47. package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +36 -0
  48. package/src/domain/services/strategies/OscCommandStrategy.ts +22 -0
  49. package/src/domain/services/strategies/StateAccessStrategy.ts +71 -0
  50. package/src/domain/services/strategies/StaticResponseStrategy.ts +42 -0
  51. package/src/domain/services/strategies/SubscriptionStrategy.ts +56 -0
  52. package/src/infrastructure/mappers/OscCodec.ts +54 -0
  53. package/src/infrastructure/repositories/InMemoryStateRepository.ts +21 -0
  54. package/src/infrastructure/services/ConsoleLogger.ts +177 -0
  55. package/src/infrastructure/services/UdpNetworkGateway.ts +71 -0
  56. package/src/presentation/cli/server.ts +194 -0
  57. package/src/presentation/library/library.ts +9 -0
  58. package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +104 -0
  59. package/tests/application/use-cases/ManageSessionsUseCase.test.ts +12 -0
  60. package/tests/application/use-cases/ProcessPacketUseCase.test.ts +49 -0
  61. package/tests/application/use-cases/SimulationService.test.ts +77 -0
  62. package/tests/domain/entities/SubscriptionManager.test.ts +50 -0
  63. package/tests/domain/entities/X32State.test.ts +52 -0
  64. package/tests/domain/models/MeterData.test.ts +23 -0
  65. package/tests/domain/models/OscMessage.test.ts +38 -0
  66. package/tests/domain/models/X32Address.test.ts +30 -0
  67. package/tests/domain/models/X32Node.test.ts +30 -0
  68. package/tests/domain/services/MeterService.test.ts +27 -0
  69. package/tests/domain/services/OscMessageHandler.test.ts +51 -0
  70. package/tests/domain/services/SchemaRegistry.test.ts +47 -0
  71. package/tests/domain/services/StaticResponseService.test.ts +15 -0
  72. package/tests/domain/services/strategies/BatchStrategy.test.ts +41 -0
  73. package/tests/domain/services/strategies/MeterStrategy.test.ts +19 -0
  74. package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +22 -0
  75. package/tests/domain/services/strategies/StateAccessStrategy.test.ts +49 -0
  76. package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +15 -0
  77. package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +45 -0
  78. package/tests/infrastructure/mappers/OscCodec.test.ts +41 -0
  79. package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +29 -0
  80. package/tests/infrastructure/services/ConsoleLogger.test.ts +74 -0
  81. package/tests/infrastructure/services/UdpNetworkGateway.test.ts +61 -0
  82. package/tests/presentation/cli/server.test.ts +178 -0
  83. package/tests/presentation/library/library.test.ts +13 -0
  84. package/tsconfig.json +21 -0
  85. package/tsdown.config.ts +15 -0
  86. package/vitest.config.ts +9 -0
@@ -0,0 +1,1189 @@
1
+ import * as os from "os";
2
+ import { EventEmitter } from "node:events";
3
+ import * as dgram from "node:dgram";
4
+
5
+ //#region src/application/use-cases/ProcessPacketUseCase.ts
6
+ var ProcessPacketUseCase = class {
7
+ constructor(messageHandler, gateway) {
8
+ this.messageHandler = messageHandler;
9
+ this.gateway = gateway;
10
+ }
11
+ execute(packet, rinfo) {
12
+ if (packet.oscType === "bundle") packet.elements?.forEach((el) => this.execute(el, rinfo));
13
+ else if (packet.oscType === "message") {
14
+ const args = packet.args.map((arg) => {
15
+ if (typeof arg === "object" && arg !== null && "value" in arg) return arg.value;
16
+ return arg;
17
+ });
18
+ this.messageHandler.handle({
19
+ address: packet.address,
20
+ args
21
+ }, rinfo).forEach((reply) => {
22
+ this.gateway.send(rinfo, reply.address, reply.args);
23
+ });
24
+ }
25
+ }
26
+ };
27
+
28
+ //#endregion
29
+ //#region src/domain/ports/ILogger.ts
30
+ /**
31
+ * Standard log categories for the domain.
32
+ */
33
+ let LogCategory = /* @__PURE__ */ function(LogCategory$1) {
34
+ LogCategory$1["SYSTEM"] = "SYSTEM";
35
+ LogCategory$1["OSC_IN"] = "OSC_IN";
36
+ LogCategory$1["OSC_OUT"] = "OSC_OUT";
37
+ LogCategory$1["DISPATCH"] = "DISPATCH";
38
+ LogCategory$1["STATE"] = "STATE";
39
+ LogCategory$1["SUB"] = "SUB";
40
+ LogCategory$1["METER"] = "METER";
41
+ return LogCategory$1;
42
+ }({});
43
+
44
+ //#endregion
45
+ //#region src/application/use-cases/BroadcastUpdatesUseCase.ts
46
+ var BroadcastUpdatesUseCase = class {
47
+ constructor(subscriptionManager, state, gateway, logger, meterService, schemaRegistry) {
48
+ this.subscriptionManager = subscriptionManager;
49
+ this.state = state;
50
+ this.gateway = gateway;
51
+ this.logger = logger;
52
+ this.meterService = meterService;
53
+ this.schemaRegistry = schemaRegistry;
54
+ }
55
+ execute() {
56
+ this.subscriptionManager.getSubscribers().forEach((sub) => {
57
+ if (sub.type === "path" || !sub.alias) {
58
+ if (sub.type === "meter" && sub.meterPath) {
59
+ const meterData = this.meterService.generateMeterData(sub.meterPath, this.state);
60
+ this.gateway.send({
61
+ address: sub.address,
62
+ port: sub.port
63
+ }, sub.meterPath, [meterData.toBlob()]);
64
+ }
65
+ return;
66
+ }
67
+ const rinfo = {
68
+ address: sub.address,
69
+ port: sub.port
70
+ };
71
+ if (sub.type === "batch" && sub.paths) {
72
+ if (sub.paths.length === 1 && sub.paths[0].startsWith("/meters")) {
73
+ const meterData = this.meterService.generateMeterData(sub.paths[0], this.state);
74
+ this.gateway.send(rinfo, sub.alias, [meterData.toBlob()]);
75
+ return;
76
+ }
77
+ let stride;
78
+ const dataSize = sub.paths.length * 4;
79
+ const factor = sub.factor || 0;
80
+ const count = sub.count || 0;
81
+ if (factor > 0) stride = Math.max(factor * 4, dataSize);
82
+ else stride = sub.paths.length === 3 ? 16 : dataSize;
83
+ const blobSize = count * stride;
84
+ if (isNaN(blobSize) || blobSize < 0) {
85
+ this.logger.error(LogCategory.SYSTEM, `Invalid blob size`, {
86
+ alias: sub.alias,
87
+ blobSize,
88
+ count,
89
+ stride,
90
+ factor,
91
+ dataSize
92
+ });
93
+ return;
94
+ }
95
+ const blob = Buffer.alloc(blobSize);
96
+ for (let i = 0; i < count; i++) {
97
+ const root = this.schemaRegistry.getRootFromIndex(sub.start + i);
98
+ let currentOffset = i * stride;
99
+ if (!root) continue;
100
+ sub.paths.forEach((s, pIdx) => {
101
+ if (pIdx * 4 >= stride) return;
102
+ if (s.startsWith("/meters")) blob.writeFloatLE(0, currentOffset);
103
+ else {
104
+ const target = `${root}${s}`;
105
+ const node = this.schemaRegistry.getNode(target);
106
+ let val = this.state.get(target);
107
+ if (val === void 0) val = node ? node.default : 0;
108
+ if (typeof val === "number") if (node && node.type === "f") blob.writeFloatLE(val, currentOffset);
109
+ else blob.writeInt32LE(val, currentOffset);
110
+ else blob.writeInt32LE(0, currentOffset);
111
+ }
112
+ currentOffset += 4;
113
+ });
114
+ }
115
+ this.gateway.send(rinfo, sub.alias, [blob]);
116
+ } else if (sub.type === "format" && sub.pattern) {
117
+ const stride = sub.factor ? sub.factor * 4 : 4;
118
+ const start = sub.start || 0;
119
+ const count = sub.count || 0;
120
+ const totalSize = (start + count) * stride;
121
+ if (isNaN(totalSize) || totalSize < 0) return;
122
+ const blob = Buffer.alloc(totalSize);
123
+ let offset = start * stride;
124
+ for (let i = start; i < start + count; i++) {
125
+ const id = i.toString().padStart(2, "0");
126
+ const target = sub.pattern.replace(/\*\*?/, id);
127
+ let val = this.state.get(target);
128
+ if (val === void 0) val = 0;
129
+ if (typeof val === "number") {
130
+ const node = this.schemaRegistry.getNode(target);
131
+ if (node && node.type === "f") blob.writeFloatLE(val, offset);
132
+ else blob.writeInt32LE(val, offset);
133
+ } else blob.writeInt32LE(0, offset);
134
+ offset += stride;
135
+ }
136
+ this.gateway.send(rinfo, sub.alias, [blob]);
137
+ }
138
+ });
139
+ }
140
+ broadcastSingleChange(path, value) {
141
+ this.subscriptionManager.getSubscribers().forEach((sub) => {
142
+ if (path === sub.path || sub.path === "/xremote" || sub.path && sub.path.includes("*") && path.startsWith(sub.path.split("*")[0])) this.gateway.send({
143
+ address: sub.address,
144
+ port: sub.port
145
+ }, path, [value]);
146
+ });
147
+ }
148
+ };
149
+
150
+ //#endregion
151
+ //#region src/application/use-cases/ManageSessionsUseCase.ts
152
+ var ManageSessionsUseCase = class {
153
+ constructor(subscriptionManager) {
154
+ this.subscriptionManager = subscriptionManager;
155
+ }
156
+ cleanup() {
157
+ this.subscriptionManager.cleanup();
158
+ }
159
+ };
160
+
161
+ //#endregion
162
+ //#region src/domain/entities/SubscriptionManager.ts
163
+ /**
164
+ * Manages OSC client subscriptions and their lifecycle.
165
+ */
166
+ var SubscriptionManager = class {
167
+ logger;
168
+ subscribers = [];
169
+ constructor(logger) {
170
+ this.logger = logger;
171
+ }
172
+ /**
173
+ * Cleans up expired subscriptions.
174
+ * Standard X32 subscriptions last 10 seconds.
175
+ */
176
+ cleanup() {
177
+ const now = Date.now();
178
+ const initialCount = this.subscribers.length;
179
+ this.subscribers = this.subscribers.filter((s) => {
180
+ const active = s.expires > now;
181
+ if (!active) this.logger.debug(LogCategory.SUB, `Expired subscriber`, {
182
+ type: s.type,
183
+ ip: s.address,
184
+ port: s.port,
185
+ path: "path" in s ? s.path : s.alias
186
+ });
187
+ return active;
188
+ });
189
+ if (this.subscribers.length !== initialCount) this.logger.info(LogCategory.SUB, `Cleanup finished`, {
190
+ removed: initialCount - this.subscribers.length,
191
+ remaining: this.subscribers.length
192
+ });
193
+ }
194
+ /**
195
+ * Gets all active subscribers.
196
+ * @returns Array of subscribers.
197
+ */
198
+ getSubscribers() {
199
+ return this.subscribers;
200
+ }
201
+ /**
202
+ * Adds or renews a path-based subscription (e.g. /subscribe or /xremote).
203
+ * @param rinfo - Client remote info.
204
+ * @param path - Subscription path.
205
+ */
206
+ addPathSubscriber(rinfo, path) {
207
+ const key = `${rinfo.address}:${rinfo.port}:${path}`;
208
+ const expires = Date.now() + 1e4;
209
+ const existing = this.subscribers.find((s) => s.type === "path" && `${s.address}:${s.port}:${s.path}` === key);
210
+ if (existing) {
211
+ existing.expires = expires;
212
+ this.logger.debug(LogCategory.SUB, `Renewed subscription`, {
213
+ ip: rinfo.address,
214
+ path
215
+ });
216
+ } else {
217
+ this.subscribers.push({
218
+ type: "path",
219
+ address: rinfo.address,
220
+ port: rinfo.port,
221
+ path,
222
+ expires
223
+ });
224
+ this.logger.info(LogCategory.SUB, `New subscription`, {
225
+ ip: rinfo.address,
226
+ path
227
+ });
228
+ }
229
+ }
230
+ /**
231
+ * Adds or renews a batch subscription.
232
+ * @param rinfo - Client remote info.
233
+ * @param alias - Response alias.
234
+ * @param paths - Target paths.
235
+ * @param start - Start index.
236
+ * @param count - Count.
237
+ * @param factor - Frequency factor.
238
+ * @param args - Command arguments.
239
+ */
240
+ addBatchSubscriber(rinfo, alias, paths, start, count, factor, args) {
241
+ const expires = Date.now() + 1e4;
242
+ this.subscribers = this.subscribers.filter((s) => !(s.type === "batch" && s.alias === alias && s.address === rinfo.address));
243
+ this.subscribers.push({
244
+ type: "batch",
245
+ address: rinfo.address,
246
+ port: rinfo.port,
247
+ alias,
248
+ paths,
249
+ start,
250
+ count,
251
+ factor,
252
+ expires,
253
+ args
254
+ });
255
+ this.logger.info(LogCategory.SUB, `Batch subscription`, {
256
+ alias,
257
+ count: paths.length,
258
+ factor,
259
+ ip: rinfo.address,
260
+ args
261
+ });
262
+ }
263
+ /**
264
+ * Adds or renews a format subscription.
265
+ * @param rinfo - Client remote info.
266
+ * @param alias - Response alias.
267
+ * @param pattern - Path pattern.
268
+ * @param start - Start index.
269
+ * @param count - Count.
270
+ * @param factor - Frequency factor.
271
+ */
272
+ addFormatSubscriber(rinfo, alias, pattern, start, count, factor) {
273
+ const expires = Date.now() + 1e4;
274
+ this.subscribers = this.subscribers.filter((s) => !(s.type === "format" && s.alias === alias && s.address === rinfo.address));
275
+ this.subscribers.push({
276
+ type: "format",
277
+ address: rinfo.address,
278
+ port: rinfo.port,
279
+ alias,
280
+ pattern,
281
+ start,
282
+ count,
283
+ factor,
284
+ expires
285
+ });
286
+ this.logger.info(LogCategory.SUB, `Format subscription`, {
287
+ alias,
288
+ pattern,
289
+ factor,
290
+ ip: rinfo.address
291
+ });
292
+ }
293
+ /**
294
+ * Adds or renews a high-frequency meter subscription.
295
+ * @param rinfo - Client remote info.
296
+ * @param meterPath - Target meter path.
297
+ */
298
+ addMeterSubscriber(rinfo, meterPath) {
299
+ const expires = Date.now() + 1e4;
300
+ const existing = this.subscribers.find((s) => s.type === "meter" && s.meterPath === meterPath && s.address === rinfo.address);
301
+ this.subscribers = this.subscribers.filter((s) => !(s.type === "meter" && s.meterPath === meterPath && s.address === rinfo.address));
302
+ this.subscribers.push({
303
+ type: "meter",
304
+ address: rinfo.address,
305
+ port: rinfo.port,
306
+ meterPath,
307
+ expires
308
+ });
309
+ if (!existing) this.logger.info(LogCategory.SUB, `Meter subscription`, {
310
+ path: meterPath,
311
+ ip: rinfo.address
312
+ });
313
+ else this.logger.debug(LogCategory.SUB, `Meter renewal`, {
314
+ path: meterPath,
315
+ ip: rinfo.address
316
+ });
317
+ }
318
+ /**
319
+ * Removes a subscription.
320
+ * @param rinfo - Client remote info.
321
+ * @param path - Subscription path.
322
+ */
323
+ removeSubscriber(rinfo, path) {
324
+ const initial = this.subscribers.length;
325
+ this.subscribers = this.subscribers.filter((s) => !(s.address === rinfo.address && s.port === rinfo.port && s.path === path));
326
+ if (this.subscribers.length < initial) this.logger.info(LogCategory.SUB, `Unsubscribed`, {
327
+ path,
328
+ ip: rinfo.address
329
+ });
330
+ }
331
+ };
332
+
333
+ //#endregion
334
+ //#region src/domain/services/strategies/NodeDiscoveryStrategy.ts
335
+ /**
336
+ * Handles the /node command for tree traversal/discovery.
337
+ * Returns a list of child nodes for a given path.
338
+ */
339
+ var NodeDiscoveryStrategy = class {
340
+ constructor(schemaRegistry) {
341
+ this.schemaRegistry = schemaRegistry;
342
+ }
343
+ canHandle(address) {
344
+ return address === "/node";
345
+ }
346
+ execute(msg, _source) {
347
+ const queryPath = msg.args[0];
348
+ if (!queryPath) return [];
349
+ const children = /* @__PURE__ */ new Set();
350
+ const prefix = queryPath.endsWith("/") ? queryPath : `${queryPath}/`;
351
+ for (const key of this.schemaRegistry.getAllPaths()) if (key.startsWith(prefix)) {
352
+ const segment = key.slice(prefix.length).split("/")[0];
353
+ if (segment) children.add(segment);
354
+ }
355
+ return [{
356
+ address: "/node",
357
+ args: [queryPath, ...children]
358
+ }];
359
+ }
360
+ };
361
+
362
+ //#endregion
363
+ //#region src/domain/services/strategies/StaticResponseStrategy.ts
364
+ /**
365
+ * Handles discovery and static information queries.
366
+ * e.g., /status, /xinfo, /-prefs/...
367
+ */
368
+ var StaticResponseStrategy = class {
369
+ constructor(serverIp, serverName, serverModel, staticResponseService) {
370
+ this.serverIp = serverIp;
371
+ this.serverName = serverName;
372
+ this.serverModel = serverModel;
373
+ this.staticResponseService = staticResponseService;
374
+ }
375
+ canHandle(address) {
376
+ return !!this.staticResponseService.getResponse(address);
377
+ }
378
+ execute(msg, _source) {
379
+ const rawResponse = this.staticResponseService.getResponse(msg.address);
380
+ if (!rawResponse) return [];
381
+ const args = rawResponse.map((arg) => {
382
+ if (typeof arg === "string") return arg.replace("{{ip}}", this.serverIp).replace("{{name}}", this.serverName).replace("{{model}}", this.serverModel);
383
+ return arg;
384
+ });
385
+ return [{
386
+ address: msg.address,
387
+ args
388
+ }];
389
+ }
390
+ };
391
+
392
+ //#endregion
393
+ //#region src/domain/services/strategies/SubscriptionStrategy.ts
394
+ var SubscriptionStrategy = class {
395
+ constructor(subscriptionManager, state, logger) {
396
+ this.subscriptionManager = subscriptionManager;
397
+ this.state = state;
398
+ this.logger = logger;
399
+ }
400
+ canHandle(address) {
401
+ return [
402
+ "/subscribe",
403
+ "/renew",
404
+ "/unsubscribe",
405
+ "/xremote"
406
+ ].includes(address);
407
+ }
408
+ /**
409
+ * Handles Session and Subscription lifecycle.
410
+ *
411
+ * ROUTES:
412
+ * - /xremote: Firehose subscription (requests all console updates for 10s).
413
+ * - /subscribe [path]: Requests updates for a specific node path for 10s.
414
+ * - /renew [path]: Resets the 10s watchdog timer for a path.
415
+ * - /unsubscribe [path]: Stops updates for a path.
416
+ * @param msg - Parsed OSC message.
417
+ * @param source - Source address and port of the packet.
418
+ * @returns Returns the current value of the path upon subscription.
419
+ */
420
+ execute(msg, source) {
421
+ const addr = msg.address;
422
+ if (addr === "/xremote") {
423
+ this.subscriptionManager.addPathSubscriber(source, "/xremote");
424
+ return [{
425
+ address: "/xremote",
426
+ args: []
427
+ }];
428
+ }
429
+ if (addr === "/unsubscribe") {
430
+ this.subscriptionManager.removeSubscriber(source, msg.args[0]);
431
+ this.logger.debug(LogCategory.SUB, `[UNSUB] ${msg.args[0]}`);
432
+ return [];
433
+ }
434
+ const target = msg.args[0];
435
+ this.subscriptionManager.addPathSubscriber(source, target);
436
+ const val = this.state.get(target);
437
+ if (val !== void 0) {
438
+ this.logger.debug(LogCategory.SUB, `[SUB] ${target} -> ${val}`);
439
+ return [{
440
+ address: target,
441
+ args: [val]
442
+ }];
443
+ }
444
+ return [];
445
+ }
446
+ };
447
+
448
+ //#endregion
449
+ //#region src/domain/services/strategies/BatchStrategy.ts
450
+ /**
451
+ * Handles bulk data subscription requests from advanced clients like X32-Edit or Mixing Station.
452
+ *
453
+ * SUPPORTED COMMANDS:
454
+ * - /formatsubscribe: Requests a range of numbered nodes (e.g. /ch/01-32/mix/fader).
455
+ * - /batchsubscribe: Aggregates multiple parameters into a single binary blob response.
456
+ */
457
+ var BatchStrategy = class {
458
+ /**
459
+ * Initializes the strategy.
460
+ * @param subscriptionManager - Session manager.
461
+ * @param logger - Logger instance.
462
+ */
463
+ constructor(subscriptionManager, logger) {
464
+ this.subscriptionManager = subscriptionManager;
465
+ this.logger = logger;
466
+ }
467
+ /** @inheritdoc */
468
+ canHandle(address) {
469
+ return ["/batchsubscribe", "/formatsubscribe"].includes(address);
470
+ }
471
+ /** @inheritdoc */
472
+ execute(msg, source) {
473
+ const addr = msg.address;
474
+ if (addr === "/formatsubscribe") {
475
+ const alias = msg.args[0];
476
+ const pattern = msg.args[1];
477
+ const start = msg.args[2];
478
+ const end = msg.args[3];
479
+ const factor = msg.args[4];
480
+ const count = end - start + 1;
481
+ this.subscriptionManager.addFormatSubscriber(source, alias, pattern, start, count, factor);
482
+ this.logger.debug(LogCategory.SUB, `[FORMAT] ${alias} factor: ${factor}`);
483
+ return [];
484
+ }
485
+ if (addr === "/batchsubscribe") {
486
+ const alias = msg.args[0];
487
+ const firstIntIndex = msg.args.findIndex((a) => typeof a === "number");
488
+ if (firstIntIndex === -1) return [];
489
+ const paths = msg.args.slice(1, firstIntIndex);
490
+ const intArgs = msg.args.slice(firstIntIndex);
491
+ const factor = intArgs.pop();
492
+ const cmdArgs = intArgs;
493
+ this.subscriptionManager.addBatchSubscriber(source, alias, paths, 0, 1, factor, cmdArgs);
494
+ this.logger.debug(LogCategory.SUB, `[BATCH] ${alias} paths: ${JSON.stringify(paths)} args: ${JSON.stringify(cmdArgs)} factor: ${factor}`);
495
+ return [];
496
+ }
497
+ return [];
498
+ }
499
+ };
500
+
501
+ //#endregion
502
+ //#region src/domain/services/strategies/MeterStrategy.ts
503
+ /**
504
+ * Handles high-frequency metering data requests.
505
+ * Meters are sent as binary blobs to optimize network bandwidth.
506
+ * Supports both direct address requests (/meters/1) and argument-based requests (/meters ,s "/meters/1").
507
+ */
508
+ var MeterStrategy = class {
509
+ /**
510
+ * Initializes the strategy.
511
+ * @param subscriptionManager - Session manager.
512
+ * @param state - Current mixer state.
513
+ * @param meterService - Service to generate meter data.
514
+ */
515
+ constructor(subscriptionManager, state, meterService) {
516
+ this.subscriptionManager = subscriptionManager;
517
+ this.state = state;
518
+ this.meterService = meterService;
519
+ }
520
+ /** @inheritdoc */
521
+ canHandle(address) {
522
+ return address.startsWith("/meters");
523
+ }
524
+ /** @inheritdoc */
525
+ execute(msg, source) {
526
+ let meterPath = msg.address;
527
+ if (msg.address === "/meters" && typeof msg.args[0] === "string" && msg.args[0].startsWith("/meters")) meterPath = msg.args[0];
528
+ this.subscriptionManager.addMeterSubscriber(source, meterPath);
529
+ const blob = this.meterService.generateMeterData(meterPath, this.state).toBlob();
530
+ return [{
531
+ address: meterPath,
532
+ args: [blob]
533
+ }];
534
+ }
535
+ };
536
+
537
+ //#endregion
538
+ //#region src/domain/services/strategies/StateAccessStrategy.ts
539
+ /**
540
+ * General-purpose strategy for reading and writing console parameters.
541
+ * acts as the fallback for any command that matches the schema.
542
+ *
543
+ * LOGIC:
544
+ * - If no arguments: Treats as a QUERY (GET) and returns the current value.
545
+ * - If arguments provided: Treats as an UPDATE (SET) and stores the new value.
546
+ */
547
+ var StateAccessStrategy = class {
548
+ /**
549
+ * Initializes the strategy.
550
+ * @param state - Current mixer state.
551
+ * @param logger - Logger instance.
552
+ * @param schemaRegistry - Registry to validate addresses.
553
+ */
554
+ constructor(state, logger, schemaRegistry) {
555
+ this.state = state;
556
+ this.logger = logger;
557
+ this.schemaRegistry = schemaRegistry;
558
+ }
559
+ /** @inheritdoc */
560
+ canHandle(address) {
561
+ return this.schemaRegistry.has(address);
562
+ }
563
+ /** @inheritdoc */
564
+ execute(msg, _source) {
565
+ const addr = msg.address;
566
+ const node = this.schemaRegistry.getNode(addr);
567
+ if (msg.args.length === 0) {
568
+ const val$1 = this.state.get(addr);
569
+ if (val$1 !== void 0) return [{
570
+ address: addr,
571
+ args: [val$1]
572
+ }];
573
+ return [];
574
+ }
575
+ const val = msg.args[0];
576
+ if (node) {
577
+ if (!node.validate(val)) {
578
+ this.logger.warn(LogCategory.DISPATCH, `[TYPE ERR] ${addr} expected ${node.type}, got ${typeof val}`);
579
+ return [];
580
+ }
581
+ this.state.set(addr, val);
582
+ this.logger.debug(LogCategory.STATE, `[SET] ${addr} -> ${val}`);
583
+ if (addr.startsWith("/config/mute/")) {
584
+ const groupIdx = parseInt(addr.split("/").pop(), 10);
585
+ if (!isNaN(groupIdx)) this.state.handleMuteGroupChange(groupIdx, val);
586
+ }
587
+ }
588
+ return [];
589
+ }
590
+ };
591
+
592
+ //#endregion
593
+ //#region src/domain/services/OscMessageHandler.ts
594
+ /**
595
+ * The central dispatcher for incoming OSC messages.
596
+ * Uses a Chain of Responsibility (Strategy pattern) to delegate handling.
597
+ * Order of strategies is critical for correct prioritization.
598
+ */
599
+ var OscMessageHandler = class {
600
+ /** List of active command strategies. */
601
+ strategies;
602
+ /**
603
+ * Initializes the handler with all available strategies.
604
+ * @param state - Global mixer state.
605
+ * @param subscriptionManager - Active client sessions.
606
+ * @param logger - Logger instance.
607
+ * @param serverIp - Host IP reported in handshakes.
608
+ * @param serverName - Console name reported in handshakes.
609
+ * @param serverModel - Console model reported in handshakes.
610
+ * @param meterService - Service for metering.
611
+ * @param schemaRegistry - Service for schema validation.
612
+ * @param staticResponseService - Service for static responses.
613
+ */
614
+ constructor(state, subscriptionManager, logger, serverIp, serverName, serverModel, meterService, schemaRegistry, staticResponseService) {
615
+ this.logger = logger;
616
+ this.meterService = meterService;
617
+ this.schemaRegistry = schemaRegistry;
618
+ this.staticResponseService = staticResponseService;
619
+ this.strategies = [
620
+ new NodeDiscoveryStrategy(this.schemaRegistry),
621
+ new StaticResponseStrategy(serverIp, serverName, serverModel, this.staticResponseService),
622
+ new SubscriptionStrategy(subscriptionManager, state, logger),
623
+ new BatchStrategy(subscriptionManager, logger),
624
+ new MeterStrategy(subscriptionManager, state, this.meterService),
625
+ new StateAccessStrategy(state, logger, this.schemaRegistry)
626
+ ];
627
+ }
628
+ /**
629
+ * Dispatches an incoming message to the first matching strategy.
630
+ * @param msg - Parsed OSC message.
631
+ * @param source - Source address and port of the packet.
632
+ * @returns Array of replies generated by the strategy.
633
+ */
634
+ handle(msg, source) {
635
+ const addr = msg.address;
636
+ if (!addr.startsWith("/meters")) this.logger.debug(LogCategory.DISPATCH, `Handling`, {
637
+ addr,
638
+ args: msg.args
639
+ });
640
+ for (const strategy of this.strategies) if (strategy.canHandle(addr)) {
641
+ const replies = strategy.execute(msg, source);
642
+ if (replies.length > 0) this.logger.debug(LogCategory.DISPATCH, `Strategy ${strategy.constructor.name} replied`, { count: replies.length });
643
+ return replies;
644
+ }
645
+ this.logger.warn(LogCategory.DISPATCH, `Unknown Command`, {
646
+ addr,
647
+ args: msg.args,
648
+ ip: source.address
649
+ });
650
+ return [];
651
+ }
652
+ };
653
+
654
+ //#endregion
655
+ //#region src/domain/models/MeterData.ts
656
+ /**
657
+ * Represents a set of meter values for a specific meter path.
658
+ * Handles the storage of float values and serialization to X32-specific binary blobs.
659
+ */
660
+ var MeterData = class {
661
+ _path;
662
+ _values;
663
+ /**
664
+ * Creates a new MeterData instance.
665
+ * @param path - The meter OSC path (e.g., "/meters/1").
666
+ * @param values - The array of float values (0.0 - 1.0 or similar).
667
+ */
668
+ constructor(path, values) {
669
+ this._path = path;
670
+ this._values = values;
671
+ }
672
+ /**
673
+ * Gets the meter path.
674
+ */
675
+ get path() {
676
+ return this._path;
677
+ }
678
+ /**
679
+ * Gets the values.
680
+ */
681
+ get values() {
682
+ return [...this._values];
683
+ }
684
+ /**
685
+ * Generates an optimized binary blob for X32 meters (mixed endianness).
686
+ * Structure: [Size (Int32BE)] [Count (Int32LE)] [Float1 (LE)] [Float2 (LE)] ...
687
+ * @returns Buffer containing the OSC blob.
688
+ */
689
+ toBlob() {
690
+ const count = this._values.length;
691
+ const totalSize = 8 + count * 4;
692
+ const blob = Buffer.alloc(totalSize);
693
+ blob.writeInt32BE(totalSize, 0);
694
+ blob.writeInt32LE(count, 4);
695
+ for (let i = 0; i < count; i++) blob.writeFloatLE(this._values[i], 8 + i * 4);
696
+ return blob;
697
+ }
698
+ };
699
+
700
+ //#endregion
701
+ //#region src/domain/models/MeterConfig.ts
702
+ /**
703
+ * Configuration for meter counts per path.
704
+ */
705
+ const METER_COUNTS = {
706
+ "/meters/0": 70,
707
+ "/meters/1": 96,
708
+ "/meters/2": 49,
709
+ "/meters/3": 22,
710
+ "/meters/4": 82,
711
+ "/meters/5": 27,
712
+ "/meters/6": 4,
713
+ "/meters/7": 16,
714
+ "/meters/8": 6,
715
+ "/meters/9": 32,
716
+ "/meters/10": 32,
717
+ "/meters/11": 5,
718
+ "/meters/12": 4,
719
+ "/meters/13": 48,
720
+ "/meters/14": 80,
721
+ "/meters/15": 50,
722
+ "/meters/16": 48
723
+ };
724
+
725
+ //#endregion
726
+ //#region src/domain/services/MeterService.ts
727
+ /**
728
+ * Service responsible for generating meter values and data.
729
+ * Mimics the physics/audio engine of the X32.
730
+ */
731
+ var MeterService = class {
732
+ /**
733
+ * Generates a MeterData object for a given path.
734
+ * @param path - The meter OSC path (e.g., "/meters/1").
735
+ * @param state - The current X32 state (for correlating faders to meters).
736
+ * @returns MeterData object containing the values.
737
+ */
738
+ generateMeterData(path, state) {
739
+ const count = METER_COUNTS[path];
740
+ if (count === void 0) return new MeterData(path, []);
741
+ const values = [];
742
+ for (let i = 0; i < count; i++) {
743
+ let val = Math.random() * .05;
744
+ if (state && path === "/meters/1") {
745
+ if (i < 32) {
746
+ const faderPath = `/ch/${(i + 1).toString().padStart(2, "0")}/mix/fader`;
747
+ const fader = state.get(faderPath);
748
+ if (typeof fader === "number") val = fader > .01 ? fader * (.9 + Math.random() * .1) : 0;
749
+ }
750
+ }
751
+ values.push(val);
752
+ }
753
+ return new MeterData(path, values);
754
+ }
755
+ };
756
+
757
+ //#endregion
758
+ //#region src/domain/services/StaticResponseService.ts
759
+ /**
760
+ * Static OSC responses for X32 Discovery and System status.
761
+ */
762
+ const STATIC_RESPONSES_DATA = {
763
+ "/xinfo": [
764
+ "{{ip}}",
765
+ "{{name}}",
766
+ "{{model}}",
767
+ "4.06"
768
+ ],
769
+ "/info": [
770
+ "V2.05",
771
+ "{{name}}",
772
+ "{{model}}",
773
+ "4.06"
774
+ ],
775
+ "/status": [
776
+ "active",
777
+ "{{ip}}",
778
+ "{{name}}"
779
+ ],
780
+ "/-prefs/invertmutes": [0],
781
+ "/-prefs/fine": [0],
782
+ "/-prefs/bright": [50],
783
+ "/-prefs/contrast": [50],
784
+ "/-prefs/led_bright": [50],
785
+ "/-prefs/lcd_bright": [50],
786
+ "/-stat/userpar/1/value": [0],
787
+ "/-stat/userpar/2/value": [0],
788
+ "/-stat/tape/state": [0],
789
+ "/-stat/aes50/A": [0],
790
+ "/-stat/aes50/B": [0],
791
+ "/-stat/solo": [0],
792
+ "/-stat/rtasource": [0],
793
+ "/-urec/errorcode": [0],
794
+ "/-prefs/rta/gain": [0],
795
+ "/-prefs/rta/autogain": [0],
796
+ "/-prefs/hardmute": [0],
797
+ "/-prefs/dcamute": [0],
798
+ "/config/mono/mode": [0],
799
+ "/config/amixenable/X": [0],
800
+ "/config/amixenable/Y": [0],
801
+ "/-show/prepos/current": [0],
802
+ "/-show/showfile/current": [""],
803
+ "/-action/setrtasrc": [0],
804
+ "/-action/playtrack": [0],
805
+ "/-action/goscene": [0],
806
+ "/-action/setscene": [0]
807
+ };
808
+ /**
809
+ * Service providing static system responses.
810
+ * Mimics the fixed firmware responses of the X32.
811
+ */
812
+ var StaticResponseService = class {
813
+ responses = STATIC_RESPONSES_DATA;
814
+ /**
815
+ * Retrieves a static response for a given path.
816
+ * @param path - The OSC path.
817
+ * @returns The response arguments or undefined.
818
+ */
819
+ getResponse(path) {
820
+ return this.responses[path];
821
+ }
822
+ };
823
+
824
+ //#endregion
825
+ //#region src/application/use-cases/SimulationService.ts
826
+ /**
827
+ * Gets the first non-internal IPv4 address found on this machine.
828
+ * @returns Detected IP or localhost.
829
+ */
830
+ function getLocalIp() {
831
+ const interfaces = os.networkInterfaces();
832
+ for (const name of Object.keys(interfaces)) for (const iface of interfaces[name]) if (iface.family === "IPv4" && !iface.internal) return iface.address;
833
+ return "127.0.0.1";
834
+ }
835
+ var SimulationService = class {
836
+ subscriptionManager;
837
+ messageHandler;
838
+ processPacket;
839
+ broadcastUpdates;
840
+ manageSessions;
841
+ meterService;
842
+ staticResponseService;
843
+ updateInterval = null;
844
+ cleanupInterval = null;
845
+ constructor(gateway, logger, stateRepo, schemaRegistry, port = 10023, ip = "0.0.0.0", name = "osc-server", model = "X32") {
846
+ this.gateway = gateway;
847
+ this.logger = logger;
848
+ this.stateRepo = stateRepo;
849
+ this.schemaRegistry = schemaRegistry;
850
+ this.port = port;
851
+ this.ip = ip;
852
+ const state = this.stateRepo.getState();
853
+ this.subscriptionManager = new SubscriptionManager(logger);
854
+ this.meterService = new MeterService();
855
+ this.staticResponseService = new StaticResponseService();
856
+ const reportedIp = this.ip === "0.0.0.0" ? getLocalIp() : this.ip;
857
+ this.messageHandler = new OscMessageHandler(state, this.subscriptionManager, logger, reportedIp, name, model, this.meterService, this.schemaRegistry, this.staticResponseService);
858
+ this.processPacket = new ProcessPacketUseCase(this.messageHandler, gateway);
859
+ this.broadcastUpdates = new BroadcastUpdatesUseCase(this.subscriptionManager, state, gateway, logger, this.meterService, this.schemaRegistry);
860
+ this.manageSessions = new ManageSessionsUseCase(this.subscriptionManager);
861
+ state.on("change", (evt) => {
862
+ this.logger.info(LogCategory.STATE, `State Changed`, evt);
863
+ this.broadcastUpdates.broadcastSingleChange(evt.address, evt.value);
864
+ });
865
+ this.gateway.onPacket((packet, source) => this.processPacket.execute(packet, source));
866
+ }
867
+ async start() {
868
+ await this.gateway.start(this.port, this.ip);
869
+ this.cleanupInterval = setInterval(() => {
870
+ this.manageSessions.cleanup();
871
+ }, 5e3);
872
+ this.updateInterval = setInterval(() => {
873
+ this.broadcastUpdates.execute();
874
+ }, 100);
875
+ }
876
+ async stop() {
877
+ if (this.updateInterval) clearInterval(this.updateInterval);
878
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
879
+ await this.gateway.stop();
880
+ }
881
+ resetState() {
882
+ this.stateRepo.reset();
883
+ }
884
+ };
885
+
886
+ //#endregion
887
+ //#region src/domain/entities/X32State.ts
888
+ /**
889
+ * Manages the internal "Digital Twin" state of the X32 console.
890
+ * acts as a single source of truth for all parameters.
891
+ * Emits 'change' events whenever a value is updated.
892
+ */
893
+ var X32State = class extends EventEmitter {
894
+ /** Map storing all OSC paths and their current values. */
895
+ state = /* @__PURE__ */ new Map();
896
+ defaultState = /* @__PURE__ */ new Map();
897
+ /**
898
+ * Initializes the state with default values from the schema.
899
+ * @param schema - The schema definition map.
900
+ */
901
+ constructor(schema) {
902
+ super();
903
+ for (const [addr, def] of Object.entries(schema)) this.defaultState.set(addr, def.default);
904
+ this.reset();
905
+ }
906
+ /**
907
+ * Resets all state parameters to their default values defined in the schema.
908
+ */
909
+ reset() {
910
+ this.state.clear();
911
+ this.defaultState.forEach((val, key) => {
912
+ this.state.set(key, val);
913
+ });
914
+ }
915
+ /**
916
+ * Retrieves the value of a specific OSC node.
917
+ * @param address - The full OSC address path.
918
+ * @returns The stored value (number or string) or undefined if not found.
919
+ */
920
+ get(address) {
921
+ return this.state.get(address);
922
+ }
923
+ /**
924
+ * Updates the value of a specific OSC node and notifies subscribers.
925
+ * @param address - The full OSC address path.
926
+ * @param value - The new value to store.
927
+ */
928
+ set(address, value) {
929
+ this.state.set(address, value);
930
+ this.emit("change", {
931
+ address,
932
+ value
933
+ });
934
+ }
935
+ /**
936
+ * Specialized logic to handle X32 Mute Groups side-effects.
937
+ * When a mute group is toggled, it iterates through all channels
938
+ * assigned to that group and updates their individual mute status.
939
+ * @param groupIdx - The index of the mute group (1-6).
940
+ * @param isOn - The new state of the group master switch (0 or 1).
941
+ */
942
+ handleMuteGroupChange(groupIdx, isOn) {
943
+ for (let i = 1; i <= 32; i++) {
944
+ const ch = i.toString().padStart(2, "0");
945
+ const grpVal = this.get(`/ch/${ch}/grp/mute`);
946
+ if (typeof grpVal === "number") {
947
+ if ((grpVal & 1 << groupIdx - 1) !== 0) {
948
+ const targetMute = isOn === 1 ? 0 : 1;
949
+ const muteAddr = `/ch/${ch}/mix/on`;
950
+ this.set(muteAddr, targetMute);
951
+ }
952
+ }
953
+ }
954
+ }
955
+ };
956
+
957
+ //#endregion
958
+ //#region src/infrastructure/repositories/InMemoryStateRepository.ts
959
+ var InMemoryStateRepository = class {
960
+ state;
961
+ constructor(logger, schemaRegistry) {
962
+ this.logger = logger;
963
+ this.state = new X32State(schemaRegistry.getSchema());
964
+ }
965
+ getState() {
966
+ return this.state;
967
+ }
968
+ reset() {
969
+ this.logger.info(LogCategory.SYSTEM, "Resetting state to defaults");
970
+ this.state.reset();
971
+ }
972
+ };
973
+
974
+ //#endregion
975
+ //#region src/infrastructure/services/ConsoleLogger.ts
976
+ /**
977
+ * Log levels for the application.
978
+ */
979
+ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
980
+ LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
981
+ LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
982
+ LogLevel$1[LogLevel$1["WARN"] = 2] = "WARN";
983
+ LogLevel$1[LogLevel$1["ERROR"] = 3] = "ERROR";
984
+ LogLevel$1[LogLevel$1["NONE"] = 4] = "NONE";
985
+ return LogLevel$1;
986
+ }({});
987
+ /**
988
+ * Enhanced logger with category support and environment-based filtering.
989
+ */
990
+ var ConsoleLogger = class ConsoleLogger {
991
+ static instance;
992
+ level = LogLevel.DEBUG;
993
+ hiddenPatterns = [];
994
+ enabledCategories = new Set([
995
+ LogCategory.SYSTEM,
996
+ LogCategory.OSC_IN,
997
+ LogCategory.OSC_OUT,
998
+ LogCategory.DISPATCH,
999
+ LogCategory.STATE,
1000
+ LogCategory.SUB
1001
+ ]);
1002
+ constructor() {
1003
+ if (process.env.HIDDEN_LOG) this.hiddenPatterns = process.env.HIDDEN_LOG.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1004
+ }
1005
+ /**
1006
+ * Gets the singleton instance.
1007
+ * @returns ConsoleLogger instance.
1008
+ */
1009
+ static getInstance() {
1010
+ if (!ConsoleLogger.instance) ConsoleLogger.instance = new ConsoleLogger();
1011
+ return ConsoleLogger.instance;
1012
+ }
1013
+ /**
1014
+ * Sets the minimum log level.
1015
+ * @param level - LogLevel.
1016
+ */
1017
+ setLevel(level) {
1018
+ this.level = level;
1019
+ }
1020
+ /**
1021
+ * Enables a category for output.
1022
+ * @param cat - Category name.
1023
+ */
1024
+ enableCategory(cat) {
1025
+ this.enabledCategories.add(cat);
1026
+ }
1027
+ /**
1028
+ * Disables a category for output.
1029
+ * @param cat - Category name.
1030
+ */
1031
+ disableCategory(cat) {
1032
+ this.enabledCategories.delete(cat);
1033
+ }
1034
+ /**
1035
+ * Logs a debug message.
1036
+ * @param category - Log category.
1037
+ * @param msg - Message string.
1038
+ * @param data - Optional metadata.
1039
+ */
1040
+ debug(category, msg, data) {
1041
+ if (this.shouldHide(msg, data)) return;
1042
+ if (this.level <= LogLevel.DEBUG && this.enabledCategories.has(category)) console.log(this.format("DEBUG", category, msg, data));
1043
+ }
1044
+ /**
1045
+ * Logs an info message.
1046
+ * @param category - Log category.
1047
+ * @param msg - Message string.
1048
+ * @param data - Optional metadata.
1049
+ */
1050
+ info(category, msg, data) {
1051
+ if (this.shouldHide(msg, data)) return;
1052
+ if (this.level <= LogLevel.INFO && this.enabledCategories.has(category)) console.log(this.format("INFO ", category, msg, data));
1053
+ }
1054
+ /**
1055
+ * Logs a warning message.
1056
+ * @param category - Log category.
1057
+ * @param msg - Message string.
1058
+ * @param data - Optional metadata.
1059
+ */
1060
+ warn(category, msg, data) {
1061
+ if (this.shouldHide(msg, data)) return;
1062
+ if (this.level <= LogLevel.WARN && this.enabledCategories.has(category)) console.log(this.format("WARN ", category, msg, data));
1063
+ }
1064
+ /**
1065
+ * Logs an error message.
1066
+ * @param category - Log category.
1067
+ * @param msg - Message string.
1068
+ * @param err - Optional error object.
1069
+ */
1070
+ error(category, msg, err) {
1071
+ if (this.shouldHide(msg, err)) return;
1072
+ if (this.level <= LogLevel.ERROR) console.log(this.format("ERROR", category, msg, err));
1073
+ }
1074
+ /**
1075
+ * Checks if a log should be hidden based on patterns.
1076
+ * @param msg - Message.
1077
+ * @param data - Metadata.
1078
+ * @returns True if log should be suppressed.
1079
+ */
1080
+ shouldHide(msg, data) {
1081
+ if (this.hiddenPatterns.length === 0) return false;
1082
+ if (this.hiddenPatterns.some((p) => msg.includes(p))) return true;
1083
+ if (data !== void 0) {
1084
+ const strData = Buffer.isBuffer(data) ? data.toString("hex") : typeof data === "object" ? JSON.stringify(data) : String(data);
1085
+ if (this.hiddenPatterns.some((p) => strData.includes(p))) return true;
1086
+ }
1087
+ return false;
1088
+ }
1089
+ /**
1090
+ * Formats the log message.
1091
+ * @param level - Level label.
1092
+ * @param category - Category label.
1093
+ * @param msg - Message.
1094
+ * @param data - Metadata.
1095
+ * @returns Formatted string.
1096
+ */
1097
+ format(level, category, msg, data) {
1098
+ const now = /* @__PURE__ */ new Date();
1099
+ let out = `\x1b[90m[${`${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}.${now.getMilliseconds().toString().padStart(3, "0")}`}]\x1b[0m `;
1100
+ switch (level) {
1101
+ case "DEBUG":
1102
+ out += `\x1b[36m[DEBUG]\x1b[0m`;
1103
+ break;
1104
+ case "INFO ":
1105
+ out += `\x1b[32m[INFO ]\x1b[0m`;
1106
+ break;
1107
+ case "WARN ":
1108
+ out += `\x1b[33m[WARN ]\x1b[0m`;
1109
+ break;
1110
+ case "ERROR":
1111
+ out += `\x1b[31m[ERROR]\x1b[0m`;
1112
+ break;
1113
+ }
1114
+ out += ` \x1b[35m[${category.padEnd(8)}]\x1b[0m ${msg}`;
1115
+ if (data !== void 0) try {
1116
+ if (Buffer.isBuffer(data)) out += ` \x1b[90m<Buffer ${data.length}b>\x1b[0m`;
1117
+ else if (typeof data === "object" && data !== null) out += ` \x1b[90m${JSON.stringify(data)}\x1b[0m`;
1118
+ else out += ` \x1b[90m${data}\x1b[0m`;
1119
+ } catch {
1120
+ out += ` [Circular/Error]`;
1121
+ }
1122
+ return out;
1123
+ }
1124
+ };
1125
+
1126
+ //#endregion
1127
+ //#region src/infrastructure/services/UdpNetworkGateway.ts
1128
+ var UdpNetworkGateway = class {
1129
+ socket;
1130
+ isRunning = false;
1131
+ packetCallback = null;
1132
+ constructor(logger, codec) {
1133
+ this.logger = logger;
1134
+ this.codec = codec;
1135
+ this.socket = dgram.createSocket("udp4");
1136
+ }
1137
+ onPacket(callback) {
1138
+ this.packetCallback = callback;
1139
+ }
1140
+ start(port, ip) {
1141
+ return new Promise((resolve, reject) => {
1142
+ this.socket.on("error", (err) => {
1143
+ this.logger.error(LogCategory.SYSTEM, "Socket Error", err);
1144
+ reject(err);
1145
+ });
1146
+ this.socket.on("message", (msg, rinfo) => {
1147
+ try {
1148
+ this.logger.debug(LogCategory.OSC_IN, `Packet received ${msg.length}b`, { ip: rinfo.address });
1149
+ const decoded = this.codec.decode(msg);
1150
+ if (this.packetCallback) this.packetCallback(decoded, {
1151
+ address: rinfo.address,
1152
+ port: rinfo.port
1153
+ });
1154
+ } catch (err) {
1155
+ this.logger.error(LogCategory.OSC_IN, "Decode Error", err instanceof Error ? err : new Error(String(err)));
1156
+ }
1157
+ });
1158
+ this.socket.bind(port, ip, () => {
1159
+ this.logger.info(LogCategory.SYSTEM, `Server bound to ${ip}:${port}`);
1160
+ this.isRunning = true;
1161
+ resolve();
1162
+ });
1163
+ });
1164
+ }
1165
+ stop() {
1166
+ return new Promise((resolve) => {
1167
+ if (!this.isRunning) return resolve();
1168
+ this.socket.close(() => {
1169
+ this.isRunning = false;
1170
+ this.logger.info(LogCategory.SYSTEM, "Server stopped");
1171
+ resolve();
1172
+ });
1173
+ });
1174
+ }
1175
+ send(target, address, args) {
1176
+ const buf = this.codec.encode(address, args);
1177
+ const cat = address.startsWith("/meters") ? LogCategory.METER : LogCategory.OSC_OUT;
1178
+ this.logger.debug(cat, `Sending ${address}`, {
1179
+ ip: target.address,
1180
+ args
1181
+ });
1182
+ this.socket.send(buf, target.port, target.address, (err) => {
1183
+ if (err) this.logger.error(LogCategory.OSC_OUT, "Send Error", err);
1184
+ });
1185
+ }
1186
+ };
1187
+
1188
+ //#endregion
1189
+ export { SimulationService as a, InMemoryStateRepository as i, ConsoleLogger as n, STATIC_RESPONSES_DATA as o, LogLevel as r, LogCategory as s, UdpNetworkGateway as t };