lightman-agent 1.0.4 → 1.0.6
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/agent.config.template.json +30 -30
- package/package.json +52 -52
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -0
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -0
- package/public/assets/index-legacy-DWtNM8y7.js +41 -0
- package/public/assets/museum-map-CwVDA2z1.svg +4182 -0
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -0
- package/public/index.html +7 -2
- package/public/templates/custom08/elements/back-button.svg +20 -0
- package/public/templates/custom08/elements/base-map-background.svg +37 -0
- package/public/templates/custom08/elements/base-map.svg +1191 -0
- package/public/templates/custom08/elements/gallery-1-2-3-info-panel.svg +236 -0
- package/public/templates/custom08/elements/gallery-4-5-6-7-info-panel.svg +266 -0
- package/public/templates/custom08/elements/gallery-8-9-info-panel.svg +274 -0
- package/public/templates/custom08/elements/gallery-labels/_nav-map-styles.css +554 -0
- package/public/templates/custom08/elements/gallery-labels/_styles.css +556 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-1.svg +35 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-2.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-3.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-4.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-5.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-6.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-7.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-8.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-9.svg +34 -0
- package/public/templates/custom08/elements/hand-hint.png +0 -0
- package/public/templates/custom08/elements/idle-screen-bg.svg +5 -0
- package/public/templates/custom08/elements/idle-screen-map.svg +627 -0
- package/public/templates/custom08/elements/idle-screen-text.svg +350 -0
- package/public/templates/custom08/elements/key-map-1.svg +986 -0
- package/public/templates/custom08/elements/key-map-2.svg +1018 -0
- package/public/templates/custom08/elements/key-map-3.svg +1019 -0
- package/public/templates/custom08/elements/key-map-combined.svg +1001 -0
- package/public/templates/custom08/elements/map-highlight-marker.svg +11 -0
- package/public/templates/custom08/elements/map-pin-marker.svg +15 -0
- package/public/templates/custom08/elements/map-teardrop-star-marker.svg +13 -0
- package/public/templates/custom08/elements/nav-circle-galleries-1-3.svg +21 -0
- package/public/templates/custom08/elements/nav-circle-galleries-4-7.svg +24 -0
- package/public/templates/custom08/elements/nav-circle-galleries-8-9.svg +20 -0
- package/public/templates/custom08/elements/section1-map.svg +1435 -0
- package/public/templates/custom08/elements/section2-map.svg +1724 -0
- package/public/templates/custom08/elements/section3-map.svg +1295 -0
- package/public/templates/custom08/fonts/CabinetGrotesk-Variable.ttf +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.12_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.56_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.24.24_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.31.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.36_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.48_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.59_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.15_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.27_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.34_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.42_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.50_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.04_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.20_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.57_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.03_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.16_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.23_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/prologue-highlight.png +0 -0
- package/scripts/guardian.ps1 +75 -75
- package/scripts/install-linux.sh +134 -134
- package/scripts/install-rpi.sh +117 -117
- package/scripts/install-windows.ps1 +513 -512
- package/scripts/launch-kiosk.vbs +101 -101
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/lightman-shell.bat +107 -107
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +116 -116
- package/scripts/setup.sh +115 -115
- package/scripts/sync-display.mjs +20 -0
- package/scripts/uninstall-linux.sh +50 -50
- package/scripts/uninstall-windows.ps1 +54 -54
- package/src/commands/display.ts +177 -177
- package/src/commands/kiosk.ts +113 -113
- package/src/commands/maintenance.ts +106 -106
- package/src/commands/network.ts +129 -129
- package/src/commands/power.ts +163 -163
- package/src/commands/rpi.ts +45 -45
- package/src/commands/screenshot.ts +166 -166
- package/src/commands/serial.ts +17 -17
- package/src/commands/update.ts +124 -124
- package/src/index.ts +652 -652
- package/src/lib/config.ts +69 -69
- package/src/lib/identity.ts +40 -40
- package/src/lib/logger.ts +137 -137
- package/src/lib/platform.ts +10 -10
- package/src/lib/rpi.ts +180 -180
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -176
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/kiosk.ts +384 -384
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +324 -324
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -120
- package/src/services/serialBridge.ts +230 -230
- package/src/services/serviceLauncher.ts +183 -183
- package/src/services/staticServer.ts +226 -226
- package/src/services/updater.ts +249 -249
- package/src/services/watchdog.ts +310 -310
- package/src/services/websocket.ts +152 -152
- package/tsconfig.json +28 -28
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
import { createSocket, type Socket } from 'dgram';
|
|
2
|
-
import type { WsClient } from './websocket.js';
|
|
3
|
-
import type { Logger } from '../lib/logger.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* OscBridge — listens for OSC messages over UDP and converts them to
|
|
7
|
-
* hardware events, mirroring the SerialBridge pattern.
|
|
8
|
-
*
|
|
9
|
-
* When the configured OSC address is received with arg === 1, it emits
|
|
10
|
-
* an `osc:trigger` event via the local event broadcaster and the server WS.
|
|
11
|
-
*
|
|
12
|
-
* Uses Node.js built-in `dgram` — no external dependencies.
|
|
13
|
-
*
|
|
14
|
-
* OSC wire format (minimal parser):
|
|
15
|
-
* - Address: null-terminated string padded to 4-byte boundary
|
|
16
|
-
* - Type tag: "," + type chars, null-terminated padded to 4-byte boundary
|
|
17
|
-
* - Arguments: int32 (big-endian) for 'i', float32 for 'f'
|
|
18
|
-
*/
|
|
19
|
-
export class OscBridge {
|
|
20
|
-
private wsClient: WsClient;
|
|
21
|
-
private logger: Logger;
|
|
22
|
-
private port: number;
|
|
23
|
-
private host: string;
|
|
24
|
-
private address: string;
|
|
25
|
-
private running = false;
|
|
26
|
-
private socket: Socket | null = null;
|
|
27
|
-
private onEvent?: (event: Record<string, unknown>) => void;
|
|
28
|
-
|
|
29
|
-
constructor(opts: {
|
|
30
|
-
wsClient: WsClient;
|
|
31
|
-
logger: Logger;
|
|
32
|
-
port: number;
|
|
33
|
-
host?: string;
|
|
34
|
-
address: string;
|
|
35
|
-
onEvent?: (event: Record<string, unknown>) => void;
|
|
36
|
-
}) {
|
|
37
|
-
this.wsClient = opts.wsClient;
|
|
38
|
-
this.logger = opts.logger;
|
|
39
|
-
this.port = opts.port;
|
|
40
|
-
this.host = opts.host || '0.0.0.0';
|
|
41
|
-
this.address = opts.address;
|
|
42
|
-
this.onEvent = opts.onEvent;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
start(): void {
|
|
46
|
-
if (this.running) return;
|
|
47
|
-
this.running = true;
|
|
48
|
-
|
|
49
|
-
this.logger.info(`[OSC] Starting bridge — listening on UDP ${this.host}:${this.port} for address "${this.address}"`);
|
|
50
|
-
|
|
51
|
-
this.socket = createSocket('udp4');
|
|
52
|
-
|
|
53
|
-
this.socket.on('message', (msg: Buffer) => {
|
|
54
|
-
try {
|
|
55
|
-
const parsed = parseOscMessage(msg);
|
|
56
|
-
if (!parsed) return;
|
|
57
|
-
|
|
58
|
-
this.logger.debug(`[OSC] Received: ${parsed.address} args=${JSON.stringify(parsed.args)}`);
|
|
59
|
-
|
|
60
|
-
// Match address
|
|
61
|
-
if (parsed.address === this.address) {
|
|
62
|
-
const firstArg = parsed.args[0];
|
|
63
|
-
// Trigger on arg === 1 (int or float)
|
|
64
|
-
if (firstArg === 1 || firstArg === 1.0) {
|
|
65
|
-
this.emitTrigger();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch (err) {
|
|
69
|
-
this.logger.debug('[OSC] Failed to parse message:', err);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
this.socket.on('error', (err) => {
|
|
74
|
-
this.logger.error('[OSC] Socket error:', err);
|
|
75
|
-
if (this.running) {
|
|
76
|
-
this.socket?.close();
|
|
77
|
-
this.socket = null;
|
|
78
|
-
this.logger.info('[OSC] Restarting in 3s...');
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
if (this.running) this.start();
|
|
81
|
-
}, 3000);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
this.socket.bind(this.port, this.host, () => {
|
|
86
|
-
this.logger.info(`[OSC] Bridge listening on UDP ${this.host}:${this.port}`);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
stop(): void {
|
|
91
|
-
this.running = false;
|
|
92
|
-
if (this.socket) {
|
|
93
|
-
try { this.socket.close(); } catch { /* ignore */ }
|
|
94
|
-
this.socket = null;
|
|
95
|
-
}
|
|
96
|
-
this.logger.info('[OSC] Bridge stopped');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
isRunning(): boolean {
|
|
100
|
-
return this.running;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private emitTrigger(): void {
|
|
104
|
-
const event: Record<string, unknown> = {
|
|
105
|
-
type: 'osc:trigger',
|
|
106
|
-
address: this.address,
|
|
107
|
-
timestamp: Date.now(),
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
this.logger.info(`[OSC] Trigger event: ${this.address}`);
|
|
111
|
-
|
|
112
|
-
// Send to server
|
|
113
|
-
this.wsClient.send({
|
|
114
|
-
type: 'osc-bridge:event',
|
|
115
|
-
payload: event,
|
|
116
|
-
timestamp: Date.now(),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Broadcast locally to Chrome display
|
|
120
|
-
this.onEvent?.(event);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ==========================================
|
|
125
|
-
// Minimal OSC message parser
|
|
126
|
-
// ==========================================
|
|
127
|
-
|
|
128
|
-
interface OscMessage {
|
|
129
|
-
address: string;
|
|
130
|
-
args: (number | string)[];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parseOscMessage(buf: Buffer): OscMessage | null {
|
|
134
|
-
let offset = 0;
|
|
135
|
-
|
|
136
|
-
// Read address string
|
|
137
|
-
const address = readOscString(buf, offset);
|
|
138
|
-
if (!address.value || address.value[0] !== '/') return null;
|
|
139
|
-
offset = address.next;
|
|
140
|
-
|
|
141
|
-
// Read type tag string
|
|
142
|
-
const typeTags = readOscString(buf, offset);
|
|
143
|
-
offset = typeTags.next;
|
|
144
|
-
|
|
145
|
-
const tags = typeTags.value || '';
|
|
146
|
-
// Type tag starts with ','
|
|
147
|
-
const types = tags.startsWith(',') ? tags.slice(1) : tags;
|
|
148
|
-
|
|
149
|
-
// Read arguments
|
|
150
|
-
const args: (number | string)[] = [];
|
|
151
|
-
for (const t of types) {
|
|
152
|
-
if (offset >= buf.length) break;
|
|
153
|
-
|
|
154
|
-
switch (t) {
|
|
155
|
-
case 'i': // int32
|
|
156
|
-
args.push(buf.readInt32BE(offset));
|
|
157
|
-
offset += 4;
|
|
158
|
-
break;
|
|
159
|
-
case 'f': // float32
|
|
160
|
-
args.push(buf.readFloatBE(offset));
|
|
161
|
-
offset += 4;
|
|
162
|
-
break;
|
|
163
|
-
case 's': { // string
|
|
164
|
-
const s = readOscString(buf, offset);
|
|
165
|
-
args.push(s.value);
|
|
166
|
-
offset = s.next;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
default:
|
|
170
|
-
// Unknown type, stop parsing
|
|
171
|
-
return { address: address.value, args };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { address: address.value, args };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function readOscString(buf: Buffer, offset: number): { value: string; next: number } {
|
|
179
|
-
let end = offset;
|
|
180
|
-
while (end < buf.length && buf[end] !== 0) end++;
|
|
181
|
-
const value = buf.toString('utf-8', offset, end);
|
|
182
|
-
// OSC strings are padded to 4-byte boundary (including null terminator)
|
|
183
|
-
const padded = end + 1;
|
|
184
|
-
const next = padded + ((4 - (padded % 4)) % 4);
|
|
185
|
-
return { value, next };
|
|
186
|
-
}
|
|
1
|
+
import { createSocket, type Socket } from 'dgram';
|
|
2
|
+
import type { WsClient } from './websocket.js';
|
|
3
|
+
import type { Logger } from '../lib/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OscBridge — listens for OSC messages over UDP and converts them to
|
|
7
|
+
* hardware events, mirroring the SerialBridge pattern.
|
|
8
|
+
*
|
|
9
|
+
* When the configured OSC address is received with arg === 1, it emits
|
|
10
|
+
* an `osc:trigger` event via the local event broadcaster and the server WS.
|
|
11
|
+
*
|
|
12
|
+
* Uses Node.js built-in `dgram` — no external dependencies.
|
|
13
|
+
*
|
|
14
|
+
* OSC wire format (minimal parser):
|
|
15
|
+
* - Address: null-terminated string padded to 4-byte boundary
|
|
16
|
+
* - Type tag: "," + type chars, null-terminated padded to 4-byte boundary
|
|
17
|
+
* - Arguments: int32 (big-endian) for 'i', float32 for 'f'
|
|
18
|
+
*/
|
|
19
|
+
export class OscBridge {
|
|
20
|
+
private wsClient: WsClient;
|
|
21
|
+
private logger: Logger;
|
|
22
|
+
private port: number;
|
|
23
|
+
private host: string;
|
|
24
|
+
private address: string;
|
|
25
|
+
private running = false;
|
|
26
|
+
private socket: Socket | null = null;
|
|
27
|
+
private onEvent?: (event: Record<string, unknown>) => void;
|
|
28
|
+
|
|
29
|
+
constructor(opts: {
|
|
30
|
+
wsClient: WsClient;
|
|
31
|
+
logger: Logger;
|
|
32
|
+
port: number;
|
|
33
|
+
host?: string;
|
|
34
|
+
address: string;
|
|
35
|
+
onEvent?: (event: Record<string, unknown>) => void;
|
|
36
|
+
}) {
|
|
37
|
+
this.wsClient = opts.wsClient;
|
|
38
|
+
this.logger = opts.logger;
|
|
39
|
+
this.port = opts.port;
|
|
40
|
+
this.host = opts.host || '0.0.0.0';
|
|
41
|
+
this.address = opts.address;
|
|
42
|
+
this.onEvent = opts.onEvent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
start(): void {
|
|
46
|
+
if (this.running) return;
|
|
47
|
+
this.running = true;
|
|
48
|
+
|
|
49
|
+
this.logger.info(`[OSC] Starting bridge — listening on UDP ${this.host}:${this.port} for address "${this.address}"`);
|
|
50
|
+
|
|
51
|
+
this.socket = createSocket('udp4');
|
|
52
|
+
|
|
53
|
+
this.socket.on('message', (msg: Buffer) => {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = parseOscMessage(msg);
|
|
56
|
+
if (!parsed) return;
|
|
57
|
+
|
|
58
|
+
this.logger.debug(`[OSC] Received: ${parsed.address} args=${JSON.stringify(parsed.args)}`);
|
|
59
|
+
|
|
60
|
+
// Match address
|
|
61
|
+
if (parsed.address === this.address) {
|
|
62
|
+
const firstArg = parsed.args[0];
|
|
63
|
+
// Trigger on arg === 1 (int or float)
|
|
64
|
+
if (firstArg === 1 || firstArg === 1.0) {
|
|
65
|
+
this.emitTrigger();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.logger.debug('[OSC] Failed to parse message:', err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.socket.on('error', (err) => {
|
|
74
|
+
this.logger.error('[OSC] Socket error:', err);
|
|
75
|
+
if (this.running) {
|
|
76
|
+
this.socket?.close();
|
|
77
|
+
this.socket = null;
|
|
78
|
+
this.logger.info('[OSC] Restarting in 3s...');
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (this.running) this.start();
|
|
81
|
+
}, 3000);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.socket.bind(this.port, this.host, () => {
|
|
86
|
+
this.logger.info(`[OSC] Bridge listening on UDP ${this.host}:${this.port}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
stop(): void {
|
|
91
|
+
this.running = false;
|
|
92
|
+
if (this.socket) {
|
|
93
|
+
try { this.socket.close(); } catch { /* ignore */ }
|
|
94
|
+
this.socket = null;
|
|
95
|
+
}
|
|
96
|
+
this.logger.info('[OSC] Bridge stopped');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isRunning(): boolean {
|
|
100
|
+
return this.running;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private emitTrigger(): void {
|
|
104
|
+
const event: Record<string, unknown> = {
|
|
105
|
+
type: 'osc:trigger',
|
|
106
|
+
address: this.address,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.logger.info(`[OSC] Trigger event: ${this.address}`);
|
|
111
|
+
|
|
112
|
+
// Send to server
|
|
113
|
+
this.wsClient.send({
|
|
114
|
+
type: 'osc-bridge:event',
|
|
115
|
+
payload: event,
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Broadcast locally to Chrome display
|
|
120
|
+
this.onEvent?.(event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==========================================
|
|
125
|
+
// Minimal OSC message parser
|
|
126
|
+
// ==========================================
|
|
127
|
+
|
|
128
|
+
interface OscMessage {
|
|
129
|
+
address: string;
|
|
130
|
+
args: (number | string)[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseOscMessage(buf: Buffer): OscMessage | null {
|
|
134
|
+
let offset = 0;
|
|
135
|
+
|
|
136
|
+
// Read address string
|
|
137
|
+
const address = readOscString(buf, offset);
|
|
138
|
+
if (!address.value || address.value[0] !== '/') return null;
|
|
139
|
+
offset = address.next;
|
|
140
|
+
|
|
141
|
+
// Read type tag string
|
|
142
|
+
const typeTags = readOscString(buf, offset);
|
|
143
|
+
offset = typeTags.next;
|
|
144
|
+
|
|
145
|
+
const tags = typeTags.value || '';
|
|
146
|
+
// Type tag starts with ','
|
|
147
|
+
const types = tags.startsWith(',') ? tags.slice(1) : tags;
|
|
148
|
+
|
|
149
|
+
// Read arguments
|
|
150
|
+
const args: (number | string)[] = [];
|
|
151
|
+
for (const t of types) {
|
|
152
|
+
if (offset >= buf.length) break;
|
|
153
|
+
|
|
154
|
+
switch (t) {
|
|
155
|
+
case 'i': // int32
|
|
156
|
+
args.push(buf.readInt32BE(offset));
|
|
157
|
+
offset += 4;
|
|
158
|
+
break;
|
|
159
|
+
case 'f': // float32
|
|
160
|
+
args.push(buf.readFloatBE(offset));
|
|
161
|
+
offset += 4;
|
|
162
|
+
break;
|
|
163
|
+
case 's': { // string
|
|
164
|
+
const s = readOscString(buf, offset);
|
|
165
|
+
args.push(s.value);
|
|
166
|
+
offset = s.next;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
default:
|
|
170
|
+
// Unknown type, stop parsing
|
|
171
|
+
return { address: address.value, args };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { address: address.value, args };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readOscString(buf: Buffer, offset: number): { value: string; next: number } {
|
|
179
|
+
let end = offset;
|
|
180
|
+
while (end < buf.length && buf[end] !== 0) end++;
|
|
181
|
+
const value = buf.toString('utf-8', offset, end);
|
|
182
|
+
// OSC strings are padded to 4-byte boundary (including null terminator)
|
|
183
|
+
const padded = end + 1;
|
|
184
|
+
const next = padded + ((4 - (padded % 4)) % 4);
|
|
185
|
+
return { value, next };
|
|
186
|
+
}
|