@djodjonx/x32-simulator 0.0.3 → 0.0.5

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 (79) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +22 -0
  3. package/dist/server.cjs +4 -5
  4. package/dist/server.d.cts +1 -10
  5. package/dist/server.d.mts +1 -10
  6. package/dist/server.mjs +5 -3
  7. package/package.json +5 -1
  8. package/.commitlintrc.json +0 -3
  9. package/.github/workflows/publish.yml +0 -38
  10. package/.husky/commit-msg +0 -1
  11. package/.husky/pre-commit +0 -1
  12. package/.oxlintrc.json +0 -56
  13. package/INSTALL.md +0 -107
  14. package/docs/OSC-Communication.md +0 -184
  15. package/docs/X32-INTERNAL.md +0 -262
  16. package/docs/X32-OSC.pdf +0 -0
  17. package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
  18. package/src/application/use-cases/BroadcastUpdatesUseCase.ts +0 -120
  19. package/src/application/use-cases/ManageSessionsUseCase.ts +0 -9
  20. package/src/application/use-cases/ProcessPacketUseCase.ts +0 -26
  21. package/src/application/use-cases/SimulationService.ts +0 -146
  22. package/src/domain/entities/SubscriptionManager.ts +0 -126
  23. package/src/domain/entities/X32State.ts +0 -78
  24. package/src/domain/models/MeterConfig.ts +0 -22
  25. package/src/domain/models/MeterData.ts +0 -59
  26. package/src/domain/models/OscMessage.ts +0 -93
  27. package/src/domain/models/X32Address.ts +0 -72
  28. package/src/domain/models/X32Node.ts +0 -43
  29. package/src/domain/models/types.ts +0 -86
  30. package/src/domain/ports/ILogger.ts +0 -27
  31. package/src/domain/ports/INetworkGateway.ts +0 -8
  32. package/src/domain/ports/IStateRepository.ts +0 -16
  33. package/src/domain/services/MeterService.ts +0 -46
  34. package/src/domain/services/OscMessageHandler.ts +0 -88
  35. package/src/domain/services/SchemaFactory.ts +0 -308
  36. package/src/domain/services/SchemaRegistry.ts +0 -67
  37. package/src/domain/services/StaticResponseService.ts +0 -52
  38. package/src/domain/services/strategies/BatchStrategy.ts +0 -74
  39. package/src/domain/services/strategies/MeterStrategy.ts +0 -45
  40. package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +0 -36
  41. package/src/domain/services/strategies/OscCommandStrategy.ts +0 -22
  42. package/src/domain/services/strategies/StateAccessStrategy.ts +0 -71
  43. package/src/domain/services/strategies/StaticResponseStrategy.ts +0 -42
  44. package/src/domain/services/strategies/SubscriptionStrategy.ts +0 -56
  45. package/src/infrastructure/mappers/OscCodec.ts +0 -54
  46. package/src/infrastructure/repositories/InMemoryStateRepository.ts +0 -37
  47. package/src/infrastructure/services/ConsoleLogger.ts +0 -177
  48. package/src/infrastructure/services/UdpNetworkGateway.ts +0 -100
  49. package/src/presentation/cli/server.ts +0 -194
  50. package/src/presentation/library/library.ts +0 -139
  51. package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +0 -104
  52. package/tests/application/use-cases/ManageSessionsUseCase.test.ts +0 -12
  53. package/tests/application/use-cases/ProcessPacketUseCase.test.ts +0 -49
  54. package/tests/application/use-cases/SimulationService.test.ts +0 -77
  55. package/tests/domain/entities/SubscriptionManager.test.ts +0 -50
  56. package/tests/domain/entities/X32State.test.ts +0 -52
  57. package/tests/domain/models/MeterData.test.ts +0 -23
  58. package/tests/domain/models/OscMessage.test.ts +0 -38
  59. package/tests/domain/models/X32Address.test.ts +0 -30
  60. package/tests/domain/models/X32Node.test.ts +0 -30
  61. package/tests/domain/services/MeterService.test.ts +0 -27
  62. package/tests/domain/services/OscMessageHandler.test.ts +0 -51
  63. package/tests/domain/services/SchemaRegistry.test.ts +0 -47
  64. package/tests/domain/services/StaticResponseService.test.ts +0 -15
  65. package/tests/domain/services/strategies/BatchStrategy.test.ts +0 -41
  66. package/tests/domain/services/strategies/MeterStrategy.test.ts +0 -19
  67. package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +0 -22
  68. package/tests/domain/services/strategies/StateAccessStrategy.test.ts +0 -49
  69. package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +0 -15
  70. package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +0 -45
  71. package/tests/infrastructure/mappers/OscCodec.test.ts +0 -41
  72. package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +0 -29
  73. package/tests/infrastructure/services/ConsoleLogger.test.ts +0 -74
  74. package/tests/infrastructure/services/UdpNetworkGateway.test.ts +0 -61
  75. package/tests/presentation/cli/server.test.ts +0 -178
  76. package/tests/presentation/library/library.test.ts +0 -13
  77. package/tsconfig.json +0 -21
  78. package/tsdown.config.ts +0 -15
  79. package/vitest.config.ts +0 -9
@@ -1,36 +0,0 @@
1
- import { OscCommandStrategy } from './OscCommandStrategy';
2
- import { OscMsg, OscReply, RemoteClient } from '../../models/types';
3
- import { SchemaRegistry } from '../SchemaRegistry';
4
-
5
- /**
6
- * Handles the /node command for tree traversal/discovery.
7
- * Returns a list of child nodes for a given path.
8
- */
9
- export class NodeDiscoveryStrategy implements OscCommandStrategy {
10
- constructor(private schemaRegistry: SchemaRegistry) {}
11
-
12
- canHandle(address: string): boolean {
13
- return address === '/node';
14
- }
15
-
16
- execute(msg: OscMsg, _source: RemoteClient): OscReply[] {
17
- const queryPath = msg.args[0] as string;
18
- if (!queryPath) return [];
19
-
20
- const children = new Set<string>();
21
- const prefix = queryPath.endsWith('/') ? queryPath : `${queryPath}/`;
22
-
23
- for (const key of this.schemaRegistry.getAllPaths()) {
24
- if (key.startsWith(prefix)) {
25
- const rest = key.slice(prefix.length);
26
- const segment = rest.split('/')[0];
27
- if (segment) children.add(segment);
28
- }
29
- }
30
-
31
- return [{
32
- address: '/node',
33
- args: [queryPath, ...children]
34
- }];
35
- }
36
- }
@@ -1,22 +0,0 @@
1
- import { OscMsg, RemoteClient, OscReply } from '../../models/types';
2
-
3
- /**
4
- * Interface for all OSC command handlers.
5
- * implementing the Strategy pattern for modular message processing.
6
- */
7
- export interface OscCommandStrategy {
8
- /**
9
- * Determines if this strategy can handle the given address.
10
- * @param address - OSC address pattern.
11
- * @returns True if the strategy matches.
12
- */
13
- canHandle(address: string): boolean;
14
-
15
- /**
16
- * Processes the message and generates appropriate replies.
17
- * @param msg - The input message.
18
- * @param source - Client remote info (address and port).
19
- * @returns Array of responses to send back.
20
- */
21
- execute(msg: OscMsg, source: RemoteClient): OscReply[];
22
- }
@@ -1,71 +0,0 @@
1
- import { OscCommandStrategy } from './OscCommandStrategy';
2
- import { OscMsg, OscReply, RemoteClient } from '../../models/types';
3
- import { X32State } from '../../entities/X32State';
4
- import { SchemaRegistry } from '../SchemaRegistry';
5
- import { ILogger, LogCategory } from '../../ports/ILogger';
6
-
7
- /**
8
- * General-purpose strategy for reading and writing console parameters.
9
- * acts as the fallback for any command that matches the schema.
10
- *
11
- * LOGIC:
12
- * - If no arguments: Treats as a QUERY (GET) and returns the current value.
13
- * - If arguments provided: Treats as an UPDATE (SET) and stores the new value.
14
- */
15
- export class StateAccessStrategy implements OscCommandStrategy {
16
- /**
17
- * Initializes the strategy.
18
- * @param state - Current mixer state.
19
- * @param logger - Logger instance.
20
- * @param schemaRegistry - Registry to validate addresses.
21
- */
22
- constructor(
23
- private state: X32State,
24
- private logger: ILogger,
25
- private schemaRegistry: SchemaRegistry
26
- ) {}
27
-
28
- /** @inheritdoc */
29
- public canHandle(address: string): boolean {
30
- // This is a catch-all for schema items.
31
- return this.schemaRegistry.has(address);
32
- }
33
-
34
- /** @inheritdoc */
35
- public execute(msg: OscMsg, _source: RemoteClient): OscReply[] {
36
- const addr = msg.address;
37
- const node = this.schemaRegistry.getNode(addr);
38
-
39
- // 1. GET (Query): If no args provided, client is asking for the value.
40
- if (msg.args.length === 0) {
41
- const val = this.state.get(addr);
42
- if (val !== undefined) {
43
- return [{ address: addr, args: [val] }];
44
- }
45
- return [];
46
- }
47
-
48
- // 2. SET (Update): If args provided, client is changing the value.
49
- const val = msg.args[0];
50
- if (node) {
51
- // Type Enforcement using new Node logic
52
- if (!node.validate(val)) {
53
- this.logger.warn(LogCategory.DISPATCH, `[TYPE ERR] ${addr} expected ${node.type}, got ${typeof val}`);
54
- return [];
55
- }
56
-
57
- this.state.set(addr, val as number | string);
58
- this.logger.debug(LogCategory.STATE, `[SET] ${addr} -> ${val}`);
59
-
60
- // Side Effects: Mute Groups
61
- if (addr.startsWith('/config/mute/')) {
62
- const groupIdx = parseInt(addr.split('/').pop()!, 10);
63
- if (!isNaN(groupIdx)) {
64
- this.state.handleMuteGroupChange(groupIdx, val as number);
65
- }
66
- }
67
- }
68
-
69
- return [];
70
- }
71
- }
@@ -1,42 +0,0 @@
1
- import { OscCommandStrategy } from './OscCommandStrategy';
2
- import { OscMsg, OscReply, RemoteClient } from '../../models/types';
3
- import { StaticResponseService } from '../StaticResponseService';
4
-
5
- /**
6
- * Handles discovery and static information queries.
7
- * e.g., /status, /xinfo, /-prefs/...
8
- */
9
- export class StaticResponseStrategy implements OscCommandStrategy {
10
- constructor(
11
- private serverIp: string,
12
- private serverName: string,
13
- private serverModel: string,
14
- private staticResponseService: StaticResponseService
15
- ) {}
16
-
17
- canHandle(address: string): boolean {
18
- return !!this.staticResponseService.getResponse(address);
19
- }
20
-
21
- execute(msg: OscMsg, _source: RemoteClient): OscReply[] {
22
- const rawResponse = this.staticResponseService.getResponse(msg.address);
23
-
24
- if (!rawResponse) return [];
25
-
26
- // Replace template variables
27
- const args = rawResponse.map(arg => {
28
- if (typeof arg === 'string') {
29
- return arg
30
- .replace('{{ip}}', this.serverIp)
31
- .replace('{{name}}', this.serverName)
32
- .replace('{{model}}', this.serverModel);
33
- }
34
- return arg;
35
- });
36
-
37
- return [{
38
- address: msg.address,
39
- args
40
- }];
41
- }
42
- }
@@ -1,56 +0,0 @@
1
- import { OscCommandStrategy } from './OscCommandStrategy';
2
- import { OscMsg, RemoteClient, OscReply } from '../../models/types';
3
- import { SubscriptionManager } from '../../entities/SubscriptionManager';
4
- import { X32State } from '../../entities/X32State';
5
- import { ILogger, LogCategory } from '../../ports/ILogger';
6
-
7
- export class SubscriptionStrategy implements OscCommandStrategy {
8
- constructor(
9
- private subscriptionManager: SubscriptionManager,
10
- private state: X32State,
11
- private logger: ILogger
12
- ) {}
13
-
14
- canHandle(address: string): boolean {
15
- return ['/subscribe', '/renew', '/unsubscribe', '/xremote'].includes(address);
16
- }
17
-
18
- /**
19
- * Handles Session and Subscription lifecycle.
20
- *
21
- * ROUTES:
22
- * - /xremote: Firehose subscription (requests all console updates for 10s).
23
- * - /subscribe [path]: Requests updates for a specific node path for 10s.
24
- * - /renew [path]: Resets the 10s watchdog timer for a path.
25
- * - /unsubscribe [path]: Stops updates for a path.
26
- * @param msg - Parsed OSC message.
27
- * @param source - Source address and port of the packet.
28
- * @returns Returns the current value of the path upon subscription.
29
- */
30
- execute(msg: OscMsg, source: RemoteClient): OscReply[] {
31
- const addr = msg.address;
32
-
33
- if (addr === '/xremote') {
34
- this.subscriptionManager.addPathSubscriber(source, '/xremote');
35
- return [{ address: '/xremote', args: [] }];
36
- }
37
-
38
- if (addr === '/unsubscribe') {
39
- this.subscriptionManager.removeSubscriber(source, msg.args[0] as string);
40
- this.logger.debug(LogCategory.SUB, `[UNSUB] ${msg.args[0]}`);
41
- return [];
42
- }
43
-
44
- // Subscribe / Renew
45
- const target = msg.args[0] as string;
46
- this.subscriptionManager.addPathSubscriber(source, target);
47
- const val = this.state.get(target);
48
-
49
- if (val !== undefined) {
50
- this.logger.debug(LogCategory.SUB, `[SUB] ${target} -> ${val}`);
51
- return [{ address: target, args: [val] }];
52
- }
53
-
54
- return [];
55
- }
56
- }
@@ -1,54 +0,0 @@
1
- import { Message, decode, encode } from 'node-osc';
2
- import { OscPacket, OscArgument } from '../../domain/models/types';
3
- import { SchemaRegistry } from '../../domain/services/SchemaRegistry';
4
-
5
- /**
6
- * Handles encoding and decoding of X32 OSC messages.
7
- */
8
- export class OscCodec {
9
-
10
- constructor(private schemaRegistry: SchemaRegistry) {}
11
-
12
- /**
13
- * Decodes a binary OSC message into a packet object.
14
- * @param msg - Raw UDP buffer.
15
- * @returns Decoded OscPacket.
16
- */
17
- public decode(msg: Buffer): OscPacket {
18
- return decode(msg) as OscPacket;
19
- }
20
-
21
- /**
22
- * Encodes an address and arguments into a binary OSC message.
23
- * @param address - OSC address pattern.
24
- * @param args - Array of arguments.
25
- * @returns Binary buffer.
26
- */
27
- public encode(address: string, args: OscArgument[]): Buffer {
28
- const msg = this.createMessage(address, args);
29
- return encode(msg);
30
- }
31
-
32
- /**
33
- * Creates a typed node-osc Message based on X32 schema.
34
- * @param address - Target address.
35
- * @param args - Untyped arguments.
36
- * @returns Typed Message.
37
- */
38
- private createMessage(address: string, args: OscArgument[]): Message {
39
- // Enforce types based on schema if possible
40
- const typedArgs = args.map((arg: OscArgument) => {
41
- if (typeof arg === 'object' && arg !== null && 'type' in arg && 'value' in arg) return arg;
42
- if (Buffer.isBuffer(arg)) return arg;
43
-
44
- const node = this.schemaRegistry.getNode(address);
45
- if (node) {
46
- if (node.type === 'f' && typeof arg === 'number') return { type: 'f', value: arg };
47
- if (node.type === 'i' && typeof arg === 'number') return { type: 'i', value: Math.round(arg) };
48
- if (node.type === 's' && typeof arg === 'string') return { type: 's', value: arg };
49
- }
50
- return arg;
51
- });
52
- return new Message(address, ...typedArgs);
53
- }
54
- }
@@ -1,37 +0,0 @@
1
- import { IStateRepository } from '../../domain/ports/IStateRepository';
2
- import { X32State } from '../../domain/entities/X32State';
3
- import { ILogger, LogCategory } from '../../domain/ports/ILogger';
4
- import { SchemaRegistry } from '../../domain/services/SchemaRegistry';
5
-
6
- /**
7
- * In-memory implementation of the state repository.
8
- * Stores the mixer state in volatile memory during the simulator's execution.
9
- */
10
- export class InMemoryStateRepository implements IStateRepository {
11
- private state: X32State;
12
-
13
- /**
14
- * Creates a new InMemoryStateRepository.
15
- * @param logger - Logger service.
16
- * @param schemaRegistry - Registry providing the initial state schema.
17
- */
18
- constructor(private logger: ILogger, schemaRegistry: SchemaRegistry) {
19
- this.state = new X32State(schemaRegistry.getSchema());
20
- }
21
-
22
- /**
23
- * Returns the current mixer state instance.
24
- * @returns The X32State entity.
25
- */
26
- public getState(): X32State {
27
- return this.state;
28
- }
29
-
30
- /**
31
- * Resets the entire state to its default values.
32
- */
33
- public reset(): void {
34
- this.logger.info(LogCategory.SYSTEM, 'Resetting state to defaults');
35
- this.state.reset();
36
- }
37
- }
@@ -1,177 +0,0 @@
1
- import { ILogger, LogCategory, LogData } from '../../domain/ports/ILogger';
2
-
3
- /**
4
- * Log levels for the application.
5
- */
6
- export enum LogLevel {
7
- DEBUG = 0,
8
- INFO = 1,
9
- WARN = 2,
10
- ERROR = 3,
11
- NONE = 4
12
- }
13
-
14
- /**
15
- * Enhanced logger with category support and environment-based filtering.
16
- */
17
- export class ConsoleLogger implements ILogger {
18
- private static instance: ConsoleLogger;
19
- private level: LogLevel = LogLevel.DEBUG;
20
- private hiddenPatterns: string[] = [];
21
- private enabledCategories: Set<string> = new Set([
22
- LogCategory.SYSTEM,
23
- LogCategory.OSC_IN,
24
- LogCategory.OSC_OUT,
25
- LogCategory.DISPATCH,
26
- LogCategory.STATE,
27
- LogCategory.SUB,
28
- ]);
29
-
30
- constructor() {
31
- if (process.env.HIDDEN_LOG) {
32
- this.hiddenPatterns = process.env.HIDDEN_LOG.split(',').map(s => s.trim()).filter(s => s.length > 0);
33
- }
34
- }
35
-
36
- /**
37
- * Gets the singleton instance.
38
- * @returns ConsoleLogger instance.
39
- */
40
- public static getInstance(): ConsoleLogger {
41
- if (!ConsoleLogger.instance) {
42
- ConsoleLogger.instance = new ConsoleLogger();
43
- }
44
- return ConsoleLogger.instance;
45
- }
46
-
47
- /**
48
- * Sets the minimum log level.
49
- * @param level - LogLevel.
50
- */
51
- public setLevel(level: LogLevel) {
52
- this.level = level;
53
- }
54
-
55
- /**
56
- * Enables a category for output.
57
- * @param cat - Category name.
58
- */
59
- public enableCategory(cat: string) {
60
- this.enabledCategories.add(cat);
61
- }
62
-
63
- /**
64
- * Disables a category for output.
65
- * @param cat - Category name.
66
- */
67
- public disableCategory(cat: string) {
68
- this.enabledCategories.delete(cat);
69
- }
70
-
71
- /**
72
- * Logs a debug message.
73
- * @param category - Log category.
74
- * @param msg - Message string.
75
- * @param data - Optional metadata.
76
- */
77
- public debug(category: string, msg: string, data?: LogData) {
78
- if (this.shouldHide(msg, data)) return;
79
- if (this.level <= LogLevel.DEBUG && this.enabledCategories.has(category)) {
80
- console.log(this.format('DEBUG', category, msg, data));
81
- }
82
- }
83
-
84
- /**
85
- * Logs an info message.
86
- * @param category - Log category.
87
- * @param msg - Message string.
88
- * @param data - Optional metadata.
89
- */
90
- public info(category: string, msg: string, data?: LogData) {
91
- if (this.shouldHide(msg, data)) return;
92
- if (this.level <= LogLevel.INFO && this.enabledCategories.has(category)) {
93
- console.log(this.format('INFO ', category, msg, data));
94
- }
95
- }
96
-
97
- /**
98
- * Logs a warning message.
99
- * @param category - Log category.
100
- * @param msg - Message string.
101
- * @param data - Optional metadata.
102
- */
103
- public warn(category: string, msg: string, data?: LogData) {
104
- if (this.shouldHide(msg, data)) return;
105
- if (this.level <= LogLevel.WARN && this.enabledCategories.has(category)) {
106
- console.log(this.format('WARN ', category, msg, data));
107
- }
108
- }
109
-
110
- /**
111
- * Logs an error message.
112
- * @param category - Log category.
113
- * @param msg - Message string.
114
- * @param err - Optional error object.
115
- */
116
- public error(category: string, msg: string, err?: LogData) {
117
- if (this.shouldHide(msg, err)) return;
118
- if (this.level <= LogLevel.ERROR) {
119
- console.log(this.format('ERROR', category, msg, err));
120
- }
121
- }
122
-
123
- /**
124
- * Checks if a log should be hidden based on patterns.
125
- * @param msg - Message.
126
- * @param data - Metadata.
127
- * @returns True if log should be suppressed.
128
- */
129
- private shouldHide(msg: string, data?: LogData): boolean {
130
- if (this.hiddenPatterns.length === 0) return false;
131
- if (this.hiddenPatterns.some(p => msg.includes(p))) return true;
132
- if (data !== undefined) {
133
- const strData = Buffer.isBuffer(data) ? data.toString('hex') : (typeof data === 'object' ? JSON.stringify(data) : String(data));
134
- if (this.hiddenPatterns.some(p => strData.includes(p))) return true;
135
- }
136
- return false;
137
- }
138
-
139
- /**
140
- * Formats the log message.
141
- * @param level - Level label.
142
- * @param category - Category label.
143
- * @param msg - Message.
144
- * @param data - Metadata.
145
- * @returns Formatted string.
146
- */
147
- private format(level: string, category: string, msg: string, data?: LogData): string {
148
- const now = new Date();
149
- const time = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}.${now.getMilliseconds().toString().padStart(3,'0')}`;
150
-
151
- let out = `\x1b[90m[${time}]\x1b[0m `; // Gray timestamp
152
-
153
- switch(level) {
154
- case 'DEBUG': out += `\x1b[36m[DEBUG]\x1b[0m`; break;
155
- case 'INFO ': out += `\x1b[32m[INFO ]\x1b[0m`; break;
156
- case 'WARN ': out += `\x1b[33m[WARN ]\x1b[0m`; break;
157
- case 'ERROR': out += `\x1b[31m[ERROR]\x1b[0m`; break;
158
- }
159
-
160
- out += ` \x1b[35m[${category.padEnd(8)}]\x1b[0m ${msg}`;
161
-
162
- if (data !== undefined) {
163
- try {
164
- if (Buffer.isBuffer(data)) {
165
- out += ` \x1b[90m<Buffer ${data.length}b>\x1b[0m`;
166
- } else if (typeof data === 'object' && data !== null) {
167
- out += ` \x1b[90m${JSON.stringify(data)}\x1b[0m`;
168
- } else {
169
- out += ` \x1b[90m${data}\x1b[0m`;
170
- }
171
- } catch {
172
- out += ` [Circular/Error]`;
173
- }
174
- }
175
- return out;
176
- }
177
- }
@@ -1,100 +0,0 @@
1
- import * as dgram from 'node:dgram';
2
- import { INetworkGateway } from '../../domain/ports/INetworkGateway';
3
- import { OscPacket, RemoteClient } from '../../domain/models/types';
4
- import { OscCodec } from '../mappers/OscCodec';
5
- import { ILogger, LogCategory } from '../../domain/ports/ILogger';
6
-
7
- /**
8
- * Node.js UDP implementation of the network gateway.
9
- * Handles the low-level socket communication and OSC packet routing.
10
- */
11
- export class UdpNetworkGateway implements INetworkGateway {
12
- private socket: dgram.Socket;
13
- private isRunning = false;
14
- private packetCallback: ((packet: OscPacket, source: RemoteClient) => void) | null = null;
15
-
16
- /**
17
- * Creates a new UdpNetworkGateway.
18
- * @param logger - Logger service.
19
- * @param codec - OSC codec for encoding/decoding.
20
- */
21
- constructor(private logger: ILogger, private codec: OscCodec) {
22
- this.socket = dgram.createSocket('udp4');
23
- }
24
-
25
- /**
26
- * Registers a callback to be executed whenever a new OSC packet arrives.
27
- * @param callback - Function called with decoded packet and source info.
28
- */
29
- public onPacket(callback: (packet: OscPacket, source: RemoteClient) => void): void {
30
- this.packetCallback = callback;
31
- }
32
-
33
- /**
34
- * Starts the UDP server on the specified port and IP.
35
- * @param port - UDP port.
36
- * @param ip - IP address to bind to.
37
- * @returns Promise resolving when the socket is bound.
38
- */
39
- public start(port: number, ip: string): Promise<void> {
40
- return new Promise((resolve, reject) => {
41
- this.socket.on('error', (err) => {
42
- this.logger.error(LogCategory.SYSTEM, 'Socket Error', err);
43
- reject(err);
44
- });
45
-
46
- this.socket.on('message', (msg, rinfo) => {
47
- try {
48
- this.logger.debug(LogCategory.OSC_IN, `Packet received ${msg.length}b`, { ip: rinfo.address });
49
- const decoded = this.codec.decode(msg);
50
- if (this.packetCallback) {
51
- this.packetCallback(decoded, { address: rinfo.address, port: rinfo.port });
52
- }
53
- } catch (err) {
54
- this.logger.error(LogCategory.OSC_IN, 'Decode Error', err instanceof Error ? err : new Error(String(err)));
55
- }
56
- });
57
-
58
- this.socket.bind(port, ip, () => {
59
- this.logger.info(LogCategory.SYSTEM, `Server bound to ${ip}:${port}`);
60
- this.isRunning = true;
61
- resolve();
62
- });
63
- });
64
- }
65
-
66
- /**
67
- * Shuts down the UDP server.
68
- * @returns Promise resolving when the socket is closed.
69
- */
70
- public stop(): Promise<void> {
71
- return new Promise((resolve) => {
72
- if (!this.isRunning) return resolve();
73
- this.socket.close(() => {
74
- this.isRunning = false;
75
- this.logger.info(LogCategory.SYSTEM, 'Server stopped');
76
- resolve();
77
- });
78
- });
79
- }
80
-
81
- /**
82
- * Sends an OSC message to a specific remote client.
83
- * @param target - Target client info (IP/port).
84
- * @param address - OSC address pattern.
85
- * @param args - Array of arguments.
86
- */
87
- public send(target: RemoteClient, address: string, args: any[]): void {
88
- const buf = this.codec.encode(address, args);
89
- const cat = address.startsWith('/meters') ? LogCategory.METER : LogCategory.OSC_OUT;
90
-
91
- // Skip noisy meter logging if needed, or rely on logger level
92
- this.logger.debug(cat, `Sending ${address}`, { ip: target.address, args });
93
-
94
- this.socket.send(buf, target.port, target.address, (err) => {
95
- if (err) {
96
- this.logger.error(LogCategory.OSC_OUT, 'Send Error', err);
97
- }
98
- });
99
- }
100
- }