@djodjonx/x32-simulator 0.0.2 → 0.0.4
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.
- package/CHANGELOG.md +10 -0
- package/README.md +28 -0
- package/dist/{UdpNetworkGateway-BrroQ6-Q.mjs → SchemaRegistry-BRVgnyaA.mjs} +990 -2
- package/dist/{UdpNetworkGateway-Ccdd7Us5.cjs → SchemaRegistry-CfDtw84j.cjs} +1033 -3
- package/dist/index.cjs +160 -6
- package/dist/index.d.cts +61 -11
- package/dist/index.d.mts +61 -11
- package/dist/index.mjs +146 -2
- package/dist/server.cjs +8 -927
- package/dist/server.mjs +1 -920
- package/package.json +5 -1
- package/.commitlintrc.json +0 -3
- package/.github/workflows/publish.yml +0 -38
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.oxlintrc.json +0 -56
- package/INSTALL.md +0 -107
- package/docs/OSC-Communication.md +0 -184
- package/docs/X32-INTERNAL.md +0 -262
- package/docs/X32-OSC.pdf +0 -0
- package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
- package/src/application/use-cases/BroadcastUpdatesUseCase.ts +0 -120
- package/src/application/use-cases/ManageSessionsUseCase.ts +0 -9
- package/src/application/use-cases/ProcessPacketUseCase.ts +0 -26
- package/src/application/use-cases/SimulationService.ts +0 -122
- package/src/domain/entities/SubscriptionManager.ts +0 -126
- package/src/domain/entities/X32State.ts +0 -78
- package/src/domain/models/MeterConfig.ts +0 -22
- package/src/domain/models/MeterData.ts +0 -59
- package/src/domain/models/OscMessage.ts +0 -93
- package/src/domain/models/X32Address.ts +0 -78
- package/src/domain/models/X32Node.ts +0 -43
- package/src/domain/models/types.ts +0 -96
- package/src/domain/ports/ILogger.ts +0 -27
- package/src/domain/ports/INetworkGateway.ts +0 -8
- package/src/domain/ports/IStateRepository.ts +0 -16
- package/src/domain/services/MeterService.ts +0 -46
- package/src/domain/services/OscMessageHandler.ts +0 -88
- package/src/domain/services/SchemaFactory.ts +0 -308
- package/src/domain/services/SchemaRegistry.ts +0 -67
- package/src/domain/services/StaticResponseService.ts +0 -52
- package/src/domain/services/strategies/BatchStrategy.ts +0 -74
- package/src/domain/services/strategies/MeterStrategy.ts +0 -45
- package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +0 -36
- package/src/domain/services/strategies/OscCommandStrategy.ts +0 -22
- package/src/domain/services/strategies/StateAccessStrategy.ts +0 -71
- package/src/domain/services/strategies/StaticResponseStrategy.ts +0 -42
- package/src/domain/services/strategies/SubscriptionStrategy.ts +0 -56
- package/src/infrastructure/mappers/OscCodec.ts +0 -54
- package/src/infrastructure/repositories/InMemoryStateRepository.ts +0 -21
- package/src/infrastructure/services/ConsoleLogger.ts +0 -177
- package/src/infrastructure/services/UdpNetworkGateway.ts +0 -71
- package/src/presentation/cli/server.ts +0 -194
- package/src/presentation/library/library.ts +0 -9
- package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +0 -104
- package/tests/application/use-cases/ManageSessionsUseCase.test.ts +0 -12
- package/tests/application/use-cases/ProcessPacketUseCase.test.ts +0 -49
- package/tests/application/use-cases/SimulationService.test.ts +0 -77
- package/tests/domain/entities/SubscriptionManager.test.ts +0 -50
- package/tests/domain/entities/X32State.test.ts +0 -52
- package/tests/domain/models/MeterData.test.ts +0 -23
- package/tests/domain/models/OscMessage.test.ts +0 -38
- package/tests/domain/models/X32Address.test.ts +0 -30
- package/tests/domain/models/X32Node.test.ts +0 -30
- package/tests/domain/services/MeterService.test.ts +0 -27
- package/tests/domain/services/OscMessageHandler.test.ts +0 -51
- package/tests/domain/services/SchemaRegistry.test.ts +0 -47
- package/tests/domain/services/StaticResponseService.test.ts +0 -15
- package/tests/domain/services/strategies/BatchStrategy.test.ts +0 -41
- package/tests/domain/services/strategies/MeterStrategy.test.ts +0 -19
- package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +0 -22
- package/tests/domain/services/strategies/StateAccessStrategy.test.ts +0 -49
- package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +0 -15
- package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +0 -45
- package/tests/infrastructure/mappers/OscCodec.test.ts +0 -41
- package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +0 -29
- package/tests/infrastructure/services/ConsoleLogger.test.ts +0 -74
- package/tests/infrastructure/services/UdpNetworkGateway.test.ts +0 -61
- package/tests/presentation/cli/server.test.ts +0 -178
- package/tests/presentation/library/library.test.ts +0 -13
- package/tsconfig.json +0 -21
- package/tsdown.config.ts +0 -15
- package/vitest.config.ts +0 -9
|
@@ -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,21 +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
|
-
export class InMemoryStateRepository implements IStateRepository {
|
|
7
|
-
private state: X32State;
|
|
8
|
-
|
|
9
|
-
constructor(private logger: ILogger, schemaRegistry: SchemaRegistry) {
|
|
10
|
-
this.state = new X32State(schemaRegistry.getSchema());
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public getState(): X32State {
|
|
14
|
-
return this.state;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
public reset(): void {
|
|
18
|
-
this.logger.info(LogCategory.SYSTEM, 'Resetting state to defaults');
|
|
19
|
-
this.state.reset();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -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,71 +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
|
-
export class UdpNetworkGateway implements INetworkGateway {
|
|
8
|
-
private socket: dgram.Socket;
|
|
9
|
-
private isRunning = false;
|
|
10
|
-
private packetCallback: ((packet: OscPacket, source: RemoteClient) => void) | null = null;
|
|
11
|
-
|
|
12
|
-
constructor(private logger: ILogger, private codec: OscCodec) {
|
|
13
|
-
this.socket = dgram.createSocket('udp4');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public onPacket(callback: (packet: OscPacket, source: RemoteClient) => void): void {
|
|
17
|
-
this.packetCallback = callback;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public start(port: number, ip: string): Promise<void> {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
this.socket.on('error', (err) => {
|
|
23
|
-
this.logger.error(LogCategory.SYSTEM, 'Socket Error', err);
|
|
24
|
-
reject(err);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
this.socket.on('message', (msg, rinfo) => {
|
|
28
|
-
try {
|
|
29
|
-
this.logger.debug(LogCategory.OSC_IN, `Packet received ${msg.length}b`, { ip: rinfo.address });
|
|
30
|
-
const decoded = this.codec.decode(msg);
|
|
31
|
-
if (this.packetCallback) {
|
|
32
|
-
this.packetCallback(decoded, { address: rinfo.address, port: rinfo.port });
|
|
33
|
-
}
|
|
34
|
-
} catch (err) {
|
|
35
|
-
this.logger.error(LogCategory.OSC_IN, 'Decode Error', err instanceof Error ? err : new Error(String(err)));
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
this.socket.bind(port, ip, () => {
|
|
40
|
-
this.logger.info(LogCategory.SYSTEM, `Server bound to ${ip}:${port}`);
|
|
41
|
-
this.isRunning = true;
|
|
42
|
-
resolve();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public stop(): Promise<void> {
|
|
48
|
-
return new Promise((resolve) => {
|
|
49
|
-
if (!this.isRunning) return resolve();
|
|
50
|
-
this.socket.close(() => {
|
|
51
|
-
this.isRunning = false;
|
|
52
|
-
this.logger.info(LogCategory.SYSTEM, 'Server stopped');
|
|
53
|
-
resolve();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public send(target: RemoteClient, address: string, args: any[]): void {
|
|
59
|
-
const buf = this.codec.encode(address, args);
|
|
60
|
-
const cat = address.startsWith('/meters') ? LogCategory.METER : LogCategory.OSC_OUT;
|
|
61
|
-
|
|
62
|
-
// Skip noisy meter logging if needed, or rely on logger level
|
|
63
|
-
this.logger.debug(cat, `Sending ${address}`, { ip: target.address, args });
|
|
64
|
-
|
|
65
|
-
this.socket.send(buf, target.port, target.address, (err) => {
|
|
66
|
-
if (err) {
|
|
67
|
-
this.logger.error(LogCategory.OSC_OUT, 'Send Error', err);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as readline from 'node:readline';
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
|
-
import * as path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { SimulationService } from '../../application/use-cases/SimulationService';
|
|
7
|
-
import { ConsoleLogger, LogLevel } from '../../infrastructure/services/ConsoleLogger';
|
|
8
|
-
import { UdpNetworkGateway } from '../../infrastructure/services/UdpNetworkGateway';
|
|
9
|
-
import { InMemoryStateRepository } from '../../infrastructure/repositories/InMemoryStateRepository';
|
|
10
|
-
import { SchemaRegistry } from '../../domain/services/SchemaRegistry';
|
|
11
|
-
import { SchemaFactory } from '../../domain/services/SchemaFactory';
|
|
12
|
-
import { OscCodec } from '../../infrastructure/mappers/OscCodec';
|
|
13
|
-
|
|
14
|
-
// Basic .env loader
|
|
15
|
-
export const loadEnv = () => {
|
|
16
|
-
try {
|
|
17
|
-
const envPath = path.resolve(process.cwd(), '.env');
|
|
18
|
-
if (fs.existsSync(envPath)) {
|
|
19
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
20
|
-
content.split('\n').forEach(line => {
|
|
21
|
-
const match = line.match(/^([^=]+)=(.*)$/);
|
|
22
|
-
if (match) {
|
|
23
|
-
const key = match[1].trim();
|
|
24
|
-
const value = match[2].trim();
|
|
25
|
-
if (!process.env[key]) {
|
|
26
|
-
process.env[key] = value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
} catch (err) {
|
|
32
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
-
console.warn('Failed to load .env file', message);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const parseArgs = (argv: string[]) => {
|
|
38
|
-
const args = argv.slice(2);
|
|
39
|
-
let cliPort: number | undefined;
|
|
40
|
-
let cliHost: string | undefined;
|
|
41
|
-
|
|
42
|
-
for (let i = 0; i < args.length; i++) {
|
|
43
|
-
const arg = args[i];
|
|
44
|
-
|
|
45
|
-
// Handle flags
|
|
46
|
-
if (arg === '--port' || arg === '-p') {
|
|
47
|
-
const next = args[i + 1];
|
|
48
|
-
if (next && /^\d+$/.test(next)) {
|
|
49
|
-
cliPort = parseInt(next, 10);
|
|
50
|
-
i++; // Skip next
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (arg === '--ip' || arg === '-h' || arg === '--host') {
|
|
55
|
-
const next = args[i + 1];
|
|
56
|
-
if (next) {
|
|
57
|
-
cliHost = next;
|
|
58
|
-
i++; // Skip next
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Handle positional arguments (simple heuristic)
|
|
64
|
-
// If it looks like an IP (x.x.x.x), treat as Host
|
|
65
|
-
if (!cliHost && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(arg)) {
|
|
66
|
-
cliHost = arg;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// If it looks like a port number (and we haven't set port yet), treat as Port
|
|
71
|
-
if (!cliPort && /^\d+$/.test(arg)) {
|
|
72
|
-
cliPort = parseInt(arg, 10);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const PORT = cliPort || parseInt(process.env.X32_PORT || '10023', 10);
|
|
78
|
-
const HOST = cliHost || process.env.X32_IP || '0.0.0.0';
|
|
79
|
-
|
|
80
|
-
return { PORT, HOST };
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export const bootstrap = async () => {
|
|
84
|
-
loadEnv();
|
|
85
|
-
const { PORT, HOST } = parseArgs(process.argv);
|
|
86
|
-
|
|
87
|
-
// 0. Configure Logging
|
|
88
|
-
const logger = ConsoleLogger.getInstance();
|
|
89
|
-
logger.setLevel(LogLevel.DEBUG);
|
|
90
|
-
|
|
91
|
-
// 1. Initialize Domain Services (Registry) & Mappers
|
|
92
|
-
const schemaFactory = new SchemaFactory();
|
|
93
|
-
const schemaRegistry = new SchemaRegistry(schemaFactory);
|
|
94
|
-
const oscCodec = new OscCodec(schemaRegistry);
|
|
95
|
-
|
|
96
|
-
// 2. Initialize Infrastructure
|
|
97
|
-
const gateway = new UdpNetworkGateway(logger, oscCodec);
|
|
98
|
-
const stateRepo = new InMemoryStateRepository(logger, schemaRegistry);
|
|
99
|
-
|
|
100
|
-
// 3. Initialize Application Service
|
|
101
|
-
const service = new SimulationService(
|
|
102
|
-
gateway,
|
|
103
|
-
logger,
|
|
104
|
-
stateRepo,
|
|
105
|
-
schemaRegistry,
|
|
106
|
-
PORT,
|
|
107
|
-
HOST
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
await service.start();
|
|
112
|
-
console.log(`
|
|
113
|
-
╔════════════════════════════════════════╗
|
|
114
|
-
║ 🚀 X32 SIMULATION SERVER ACTIVE ║
|
|
115
|
-
╚════════════════════════════════════════╝
|
|
116
|
-
• IP: ${HOST}
|
|
117
|
-
• Port: ${PORT}
|
|
118
|
-
• Protocol: UDP (Strict X32 Schema)
|
|
119
|
-
• Logging: ENHANCED (DEBUG)
|
|
120
|
-
|
|
121
|
-
👉 Type "exit" or "stop" to shut down.
|
|
122
|
-
👉 Type "reset" to reset faders to default.
|
|
123
|
-
`);
|
|
124
|
-
} catch (err: any) {
|
|
125
|
-
if (err.code === 'EADDRNOTAVAIL') {
|
|
126
|
-
console.error(`\n\x1b[31m❌ FAILED TO BIND TO IP: ${HOST}\x1b[0m`);
|
|
127
|
-
console.error(`\nThe IP address you requested does not exist on this machine.`);
|
|
128
|
-
console.error(`You likely need to create a network alias (Loopback) for it.\n`);
|
|
129
|
-
|
|
130
|
-
const isMac = process.platform === 'darwin';
|
|
131
|
-
const isLinux = process.platform === 'linux';
|
|
132
|
-
const isWin = process.platform === 'win32';
|
|
133
|
-
|
|
134
|
-
if (isMac || isLinux || isWin) {
|
|
135
|
-
console.error(`To fix this, try running the following command:\n`);
|
|
136
|
-
if (isMac) {
|
|
137
|
-
console.error(` \x1b[36msudo ifconfig lo0 alias ${HOST}\x1b[0m`);
|
|
138
|
-
} else if (isLinux) {
|
|
139
|
-
console.error(` \x1b[36msudo ip addr add ${HOST}/32 dev lo\x1b[0m`);
|
|
140
|
-
} else if (isWin) {
|
|
141
|
-
console.error(` \x1b[36mnetsh interface ip add address "Loopback" ${HOST} 255.255.255.255\x1b[0m`);
|
|
142
|
-
console.error(` (Note: Requires 'Microsoft Loopback Adapter' installed and named "Loopback")`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
console.error(`\nAlternatively, remove the IP argument to listen on all interfaces (0.0.0.0).`);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
console.error("Failed to start server:", err);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 3. Setup CLI Input
|
|
154
|
-
const rl = readline.createInterface({
|
|
155
|
-
input: process.stdin,
|
|
156
|
-
output: process.stdout
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
rl.on('line', async (input) => {
|
|
160
|
-
const command = input.trim().toLowerCase();
|
|
161
|
-
|
|
162
|
-
switch (command) {
|
|
163
|
-
case 'exit':
|
|
164
|
-
case 'stop':
|
|
165
|
-
case 'quit':
|
|
166
|
-
console.log('Shutting down...');
|
|
167
|
-
await service.stop();
|
|
168
|
-
process.exit(0);
|
|
169
|
-
break;
|
|
170
|
-
|
|
171
|
-
case 'reset':
|
|
172
|
-
service.resetState();
|
|
173
|
-
console.log('✨ Console state reset to defaults.');
|
|
174
|
-
break;
|
|
175
|
-
|
|
176
|
-
default:
|
|
177
|
-
if (command) console.log(`Unknown command: "${command}"`);
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Handle Ctrl+C
|
|
183
|
-
process.on('SIGINT', async () => {
|
|
184
|
-
console.log('\n(SIGINT received)');
|
|
185
|
-
await service.stop();
|
|
186
|
-
process.exit(0);
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Check if main module
|
|
191
|
-
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
|
|
192
|
-
if (isMainModule) {
|
|
193
|
-
bootstrap();
|
|
194
|
-
}
|