lightman-agent 1.0.18 → 1.0.21
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.json +22 -23
- package/agent.config.template.json +30 -31
- package/bin/cms-agent.js +269 -248
- package/package.json +1 -1
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -1
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -1
- package/public/assets/index-legacy-DWtNM8y7.js +41 -41
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -4
- package/scripts/guardian.ps1 +50 -124
- package/scripts/install-windows.ps1 +60 -116
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +17 -22
- package/scripts/sync-display.mjs +20 -20
- 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 +173 -90
- package/src/lib/config.ts +2 -3
- 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/screenMap.ts +135 -0
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -177
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +116 -83
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -122
- 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
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { loadConfig } from './lib/config.js';
|
|
4
4
|
import { Logger } from './lib/logger.js';
|
|
@@ -10,9 +10,11 @@ import { KioskManager } from './services/kiosk.js';
|
|
|
10
10
|
import { registerPowerCommands } from './commands/power.js';
|
|
11
11
|
import { registerKioskCommands, registerMultiScreenKioskCommands } from './commands/kiosk.js';
|
|
12
12
|
import { registerScreenshotCommands } from './commands/screenshot.js';
|
|
13
|
-
import { MultiScreenKioskManager } from './services/multiScreenKiosk.js';
|
|
14
|
-
import { detectScreens } from './lib/screens.js';
|
|
15
|
-
import {
|
|
13
|
+
import { MultiScreenKioskManager } from './services/multiScreenKiosk.js';
|
|
14
|
+
import { detectScreens } from './lib/screens.js';
|
|
15
|
+
import type { DetectedScreen } from './lib/screens.js';
|
|
16
|
+
import { resolveScreenMap } from './lib/screenMap.js';
|
|
17
|
+
import { registerDisplayCommands } from './commands/display.js';
|
|
16
18
|
import { registerNetworkCommands } from './commands/network.js';
|
|
17
19
|
import { Updater } from './services/updater.js';
|
|
18
20
|
import { registerUpdateCommands } from './commands/update.js';
|
|
@@ -30,7 +32,7 @@ import { SerialBridge } from './services/serialBridge.js';
|
|
|
30
32
|
import { OscBridge } from './services/oscBridge.js';
|
|
31
33
|
import { LocalEventServer } from './services/localEvents.js';
|
|
32
34
|
import { PresenceSensor } from './services/presenceSensor.js';
|
|
33
|
-
import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
|
|
35
|
+
import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
|
|
34
36
|
|
|
35
37
|
function getAgentVersion(): string {
|
|
36
38
|
try {
|
|
@@ -71,7 +73,7 @@ async function main(): Promise<void> {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
// 2. Provision (get identity)
|
|
74
|
-
// Provision with retry
|
|
76
|
+
// Provision with retry — never crash, just keep trying.
|
|
75
77
|
// This prevents NSSM restart loops that kill Chrome (blinking screen).
|
|
76
78
|
let identity: Identity | null = null;
|
|
77
79
|
const MAX_PROVISION_ATTEMPTS = 999;
|
|
@@ -97,7 +99,7 @@ async function main(): Promise<void> {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
if (!identity) {
|
|
100
|
-
logger.error('Provisioning failed
|
|
102
|
+
logger.error('Provisioning failed — no identity. Exiting.');
|
|
101
103
|
process.exit(1);
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -161,14 +163,65 @@ async function main(): Promise<void> {
|
|
|
161
163
|
const kioskManager = new KioskManager(kioskConfig, logger);
|
|
162
164
|
registerKioskCommands(commandExecutor.register.bind(commandExecutor), kioskManager, logger);
|
|
163
165
|
|
|
164
|
-
// Multi-screen kiosk manager
|
|
166
|
+
// Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
|
|
165
167
|
const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
|
|
166
168
|
const getIdentity = () => identity!;
|
|
167
169
|
registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
|
|
168
170
|
|
|
169
|
-
// Detect physical screens and
|
|
170
|
-
|
|
171
|
-
multiScreenKiosk.setDetectedScreens(detectedScreens);
|
|
171
|
+
// Detect physical screens and keep them fresh (multi-display setups can change after boot).
|
|
172
|
+
let detectedScreens = detectScreens(logger);
|
|
173
|
+
multiScreenKiosk.setDetectedScreens(detectedScreens);
|
|
174
|
+
|
|
175
|
+
const toScreenPayload = (screens: DetectedScreen[]) => (
|
|
176
|
+
screens.map((s) => ({
|
|
177
|
+
hardwareId: s.hardwareId,
|
|
178
|
+
name: s.name,
|
|
179
|
+
index: s.index,
|
|
180
|
+
width: s.width,
|
|
181
|
+
height: s.height,
|
|
182
|
+
x: s.x,
|
|
183
|
+
y: s.y,
|
|
184
|
+
primary: s.primary,
|
|
185
|
+
}))
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const sendAgentRegister = () => {
|
|
189
|
+
wsClient.send({
|
|
190
|
+
type: 'agent:register',
|
|
191
|
+
payload: {
|
|
192
|
+
agentVersion: getAgentVersion(),
|
|
193
|
+
screens: toScreenPayload(detectedScreens),
|
|
194
|
+
},
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const refreshDetectedScreens = async (reason: string) => {
|
|
200
|
+
const latest = detectScreens(logger);
|
|
201
|
+
if (!haveScreensChanged(detectedScreens, latest)) return;
|
|
202
|
+
|
|
203
|
+
logger.info(`[Screens] Topology changed (${reason}): ${detectedScreens.length} -> ${latest.length}`);
|
|
204
|
+
detectedScreens = latest;
|
|
205
|
+
multiScreenKiosk.setDetectedScreens(detectedScreens);
|
|
206
|
+
|
|
207
|
+
if (wsClient.isConnected()) {
|
|
208
|
+
sendAgentRegister();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (multiScreenKiosk.hasDesiredScreenMap()) {
|
|
212
|
+
try {
|
|
213
|
+
await multiScreenKiosk.reapplyDesiredMap(identity);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
logger.error('[MultiKiosk] Failed to reapply desired screen map after topology change:', err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const screenRefreshInterval = setInterval(() => {
|
|
221
|
+
refreshDetectedScreens('periodic-refresh').catch((err) => {
|
|
222
|
+
logger.error('[Screens] Periodic refresh failed:', err);
|
|
223
|
+
});
|
|
224
|
+
}, 20_000);
|
|
172
225
|
|
|
173
226
|
// Create Watchdog (Phase 20)
|
|
174
227
|
const watchdog = new Watchdog(
|
|
@@ -210,11 +263,11 @@ async function main(): Promise<void> {
|
|
|
210
263
|
// Register serial/COM port commands (works on all platforms)
|
|
211
264
|
registerSerialCommands(commandExecutor.register.bind(commandExecutor), logger);
|
|
212
265
|
|
|
213
|
-
// Local hardware event server
|
|
266
|
+
// Local hardware event server — broadcasts directly to Chrome on this device
|
|
214
267
|
const localEventServer = new LocalEventServer(config.localEventsPort || 3402, logger);
|
|
215
268
|
localEventServer.start();
|
|
216
269
|
|
|
217
|
-
// OSC bridge
|
|
270
|
+
// OSC bridge — listens on UDP for OSC messages and forwards triggers to display
|
|
218
271
|
let oscBridge: OscBridge | null = null;
|
|
219
272
|
|
|
220
273
|
const startOscBridge = (oscPort: number, oscAddress: string, oscHost?: string) => {
|
|
@@ -223,7 +276,7 @@ async function main(): Promise<void> {
|
|
|
223
276
|
oscBridge.stop();
|
|
224
277
|
oscBridge = null;
|
|
225
278
|
}
|
|
226
|
-
logger.info(`[OSC] Starting bridge
|
|
279
|
+
logger.info(`[OSC] Starting bridge — UDP ${oscHost || '0.0.0.0'}:${oscPort} address: ${oscAddress}`);
|
|
227
280
|
oscBridge = new OscBridge({
|
|
228
281
|
wsClient,
|
|
229
282
|
logger,
|
|
@@ -239,7 +292,7 @@ async function main(): Promise<void> {
|
|
|
239
292
|
if (oscBridge) { oscBridge.stop(); oscBridge = null; }
|
|
240
293
|
};
|
|
241
294
|
|
|
242
|
-
// Serial bridge
|
|
295
|
+
// Serial bridge — reads COM port chars (* → pickup, # → hangup) and forwards to server
|
|
243
296
|
let serialBridge: SerialBridge | null = null;
|
|
244
297
|
|
|
245
298
|
/** Start or restart the serial bridge with given COM port and controllerId */
|
|
@@ -260,14 +313,14 @@ async function main(): Promise<void> {
|
|
|
260
313
|
onEvent: (event) => localEventServer.broadcast({ type: 'hardware:event', payload: event }),
|
|
261
314
|
});
|
|
262
315
|
serialBridge.start();
|
|
263
|
-
logger.info(`[SERIAL] Bridge started
|
|
316
|
+
logger.info(`[SERIAL] Bridge started — waiting for hardware events on ${comPort}`);
|
|
264
317
|
};
|
|
265
318
|
|
|
266
319
|
const stopSerialBridge = () => {
|
|
267
320
|
if (serialBridge) { serialBridge.stop(); serialBridge = null; }
|
|
268
321
|
};
|
|
269
322
|
|
|
270
|
-
// Presence sensor
|
|
323
|
+
// Presence sensor — auto-detects HLK-LD2410B via USB serial
|
|
271
324
|
let presenceSensor: PresenceSensor | null = null;
|
|
272
325
|
|
|
273
326
|
const startPresenceSensor = (port?: string) => {
|
|
@@ -411,27 +464,11 @@ async function main(): Promise<void> {
|
|
|
411
464
|
watchdog.start();
|
|
412
465
|
powerScheduler.start();
|
|
413
466
|
|
|
414
|
-
// Send registration message once connected, then auto-launch kiosk
|
|
415
|
-
const registerInterval = setInterval(() => {
|
|
416
|
-
if (wsClient.isConnected()) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
payload: {
|
|
420
|
-
agentVersion: getAgentVersion(),
|
|
421
|
-
screens: detectedScreens.map(s => ({
|
|
422
|
-
hardwareId: s.hardwareId,
|
|
423
|
-
name: s.name,
|
|
424
|
-
index: s.index,
|
|
425
|
-
width: s.width,
|
|
426
|
-
height: s.height,
|
|
427
|
-
x: s.x,
|
|
428
|
-
y: s.y,
|
|
429
|
-
primary: s.primary,
|
|
430
|
-
})),
|
|
431
|
-
},
|
|
432
|
-
timestamp: Date.now(),
|
|
433
|
-
});
|
|
434
|
-
clearInterval(registerInterval);
|
|
467
|
+
// Send registration message once connected, then auto-launch kiosk
|
|
468
|
+
const registerInterval = setInterval(() => {
|
|
469
|
+
if (wsClient.isConnected()) {
|
|
470
|
+
sendAgentRegister();
|
|
471
|
+
clearInterval(registerInterval);
|
|
435
472
|
|
|
436
473
|
// Fetch device config first to decide single vs multi-screen kiosk
|
|
437
474
|
fetchDeviceConfig(config.serverUrl, identity, logger).then((deviceCfg) => {
|
|
@@ -441,39 +478,53 @@ async function main(): Promise<void> {
|
|
|
441
478
|
const controllerId = deviceCfg.controllerId || comPort;
|
|
442
479
|
const bridgeBaud = deviceCfg.baudRate || 115200;
|
|
443
480
|
logger.info(`[SERIAL] com_port found: ${comPort} | controllerId: ${controllerId} | baud: ${bridgeBaud}`);
|
|
444
|
-
logger.info(`[SERIAL] Starting serial bridge
|
|
481
|
+
logger.info(`[SERIAL] Starting serial bridge — listening on ${comPort}...`);
|
|
445
482
|
startSerialBridge(comPort, controllerId, bridgeBaud);
|
|
446
483
|
} else {
|
|
447
|
-
logger.info('[SERIAL] No com_port configured on this device
|
|
484
|
+
logger.info('[SERIAL] No com_port configured on this device — serial bridge not started');
|
|
448
485
|
}
|
|
449
486
|
|
|
450
|
-
// OSC bridge
|
|
487
|
+
// OSC bridge — auto-start if app config has inputSource === 'osc'
|
|
451
488
|
if (deviceCfg && deviceCfg.oscPort && deviceCfg.oscAddress) {
|
|
452
489
|
logger.info(`[OSC] Config found: port=${deviceCfg.oscPort} address=${deviceCfg.oscAddress}`);
|
|
453
490
|
startOscBridge(deviceCfg.oscPort, deviceCfg.oscAddress, deviceCfg.oscHost);
|
|
454
491
|
} else {
|
|
455
|
-
logger.info('[OSC] No OSC config on this device
|
|
492
|
+
logger.info('[OSC] No OSC config on this device — OSC bridge not started');
|
|
456
493
|
}
|
|
457
494
|
|
|
458
|
-
// Presence sensor
|
|
495
|
+
// Presence sensor — auto-start if template is presence-enabled (e.g. wipro-timeline)
|
|
459
496
|
if (deviceCfg && deviceCfg.templateType === 'custom01-wipro-timeline') {
|
|
460
|
-
logger.info('[Presence] Template is custom01-wipro-timeline
|
|
497
|
+
logger.info('[Presence] Template is custom01-wipro-timeline — auto-starting presence sensor');
|
|
461
498
|
startPresenceSensor();
|
|
462
499
|
} else {
|
|
463
|
-
logger.info('[Presence] Template is not presence-enabled
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Multi-screen: if screenMap exists, use MultiScreenKioskManager — do NOT launch single kiosk
|
|
467
|
-
if (deviceCfg && deviceCfg.screenMap && deviceCfg.screenMap.length > 0) {
|
|
468
|
-
logger.info(`[MultiKiosk] Found screenMap in device config: ${deviceCfg.screenMap.length} mapping(s) — skipping single kiosk`);
|
|
469
|
-
watchdog.setMultiScreenActive(true);
|
|
470
|
-
multiScreenKiosk.applyScreenMap(deviceCfg.screenMap, identity).catch((err) => {
|
|
471
|
-
logger.error('[MultiKiosk] Failed to apply screenMap from config:', err);
|
|
472
|
-
});
|
|
473
|
-
return;
|
|
500
|
+
logger.info('[Presence] Template is not presence-enabled — sensor not started');
|
|
474
501
|
}
|
|
475
|
-
|
|
476
|
-
//
|
|
502
|
+
// Multi-screen handling:
|
|
503
|
+
// 1) Use explicit screenMap when present.
|
|
504
|
+
// 2) For multi-screen apps without a saved map, auto-create placeholders.
|
|
505
|
+
const requestedScreenMap = deviceCfg?.screenMap || [];
|
|
506
|
+
const isMultiScreenApp = !!deviceCfg && deviceCfg.totalScreens > 1;
|
|
507
|
+
const effectiveRequestedMap = requestedScreenMap.length > 0
|
|
508
|
+
? requestedScreenMap
|
|
509
|
+
: (isMultiScreenApp ? createPlaceholderScreenMap(deviceCfg.totalScreens) : []);
|
|
510
|
+
|
|
511
|
+
if (effectiveRequestedMap.length > 0) {
|
|
512
|
+
const resolved = resolveScreenMap({
|
|
513
|
+
requestedScreenMap: effectiveRequestedMap,
|
|
514
|
+
detectedScreens,
|
|
515
|
+
totalScreens: Math.max(deviceCfg?.totalScreens || 0, effectiveRequestedMap.length),
|
|
516
|
+
});
|
|
517
|
+
logger.info(
|
|
518
|
+
`[MultiKiosk] Effective map ready: requested=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${deviceCfg?.totalScreens || 0}`
|
|
519
|
+
);
|
|
520
|
+
watchdog.setMultiScreenActive(true);
|
|
521
|
+
multiScreenKiosk.applyScreenMap(effectiveRequestedMap, identity).catch((err) => {
|
|
522
|
+
logger.error('[MultiKiosk] Failed to apply effective screenMap from config:', err);
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// No effective multi-screen map - launch single-screen kiosk as before
|
|
477
528
|
if (config.kiosk) {
|
|
478
529
|
if (config.kiosk.shellMode) {
|
|
479
530
|
logger.info('Shell mode: skipping Chrome launch (managed by Windows shell)');
|
|
@@ -501,9 +552,10 @@ async function main(): Promise<void> {
|
|
|
501
552
|
}, 1000);
|
|
502
553
|
|
|
503
554
|
// 7. Graceful shutdown
|
|
504
|
-
const shutdown = async (signal: string): Promise<void> => {
|
|
505
|
-
logger.info(`${signal} received. Shutting down...`);
|
|
506
|
-
clearInterval(registerInterval);
|
|
555
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
556
|
+
logger.info(`${signal} received. Shutting down...`);
|
|
557
|
+
clearInterval(registerInterval);
|
|
558
|
+
clearInterval(screenRefreshInterval);
|
|
507
559
|
|
|
508
560
|
// Wait for updater to finish if it's busy (max 60s)
|
|
509
561
|
if (updater.isBusy()) {
|
|
@@ -551,11 +603,36 @@ async function main(): Promise<void> {
|
|
|
551
603
|
process.exit(1);
|
|
552
604
|
});
|
|
553
605
|
|
|
554
|
-
logger.info('LIGHTMAN Agent running.');
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function
|
|
558
|
-
|
|
606
|
+
logger.info('LIGHTMAN Agent running.');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
|
|
610
|
+
const count = Math.max(0, Math.floor(totalScreens || 0));
|
|
611
|
+
return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boolean {
|
|
615
|
+
if (prev.length !== next.length) return true;
|
|
616
|
+
for (let i = 0; i < prev.length; i++) {
|
|
617
|
+
const a = prev[i];
|
|
618
|
+
const b = next[i];
|
|
619
|
+
if (
|
|
620
|
+
a.hardwareId !== b.hardwareId
|
|
621
|
+
|| a.index !== b.index
|
|
622
|
+
|| a.x !== b.x
|
|
623
|
+
|| a.y !== b.y
|
|
624
|
+
|| a.width !== b.width
|
|
625
|
+
|| a.height !== b.height
|
|
626
|
+
|| a.primary !== b.primary
|
|
627
|
+
) {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function handleServerMessage(
|
|
635
|
+
msg: WsMessage,
|
|
559
636
|
commandExecutor: CommandExecutor,
|
|
560
637
|
logger: Logger,
|
|
561
638
|
powerScheduler?: PowerScheduler,
|
|
@@ -589,11 +666,11 @@ function handleServerMessage(
|
|
|
589
666
|
const comPort = msg.payload.com_port as string | undefined;
|
|
590
667
|
if (comPort && startSerialBridge) {
|
|
591
668
|
const controllerId = (msg.payload.controllerId as string) || comPort;
|
|
592
|
-
logger.info(`[SERIAL] Admin updated com_port
|
|
669
|
+
logger.info(`[SERIAL] Admin updated com_port → ${comPort} | Restarting serial bridge...`);
|
|
593
670
|
startSerialBridge(comPort, controllerId);
|
|
594
671
|
logger.info(`[SERIAL] Serial bridge now listening on ${comPort}`);
|
|
595
672
|
} else if (comPort === '' && stopSerialBridge) {
|
|
596
|
-
logger.info('[SERIAL] Admin cleared com_port
|
|
673
|
+
logger.info('[SERIAL] Admin cleared com_port — stopping serial bridge');
|
|
597
674
|
stopSerialBridge();
|
|
598
675
|
}
|
|
599
676
|
|
|
@@ -603,20 +680,20 @@ function handleServerMessage(
|
|
|
603
680
|
const inputSource = msg.payload.inputSource as string | undefined;
|
|
604
681
|
if (inputSource === 'osc' && oscPort && oscAddress && startOscBridgeFn) {
|
|
605
682
|
const oscHost = (msg.payload.oscHost as string) || '0.0.0.0';
|
|
606
|
-
logger.info(`[OSC] Admin updated OSC config
|
|
683
|
+
logger.info(`[OSC] Admin updated OSC config → port=${oscPort} address=${oscAddress} — restarting bridge...`);
|
|
607
684
|
startOscBridgeFn(oscPort, oscAddress, oscHost);
|
|
608
685
|
} else if (inputSource === 'com' && stopOscBridgeFn) {
|
|
609
|
-
logger.info('[OSC] Admin switched to COM input
|
|
686
|
+
logger.info('[OSC] Admin switched to COM input — stopping OSC bridge');
|
|
610
687
|
stopOscBridgeFn();
|
|
611
688
|
}
|
|
612
689
|
|
|
613
|
-
// Presence sensor
|
|
690
|
+
// Presence sensor — auto-start/stop based on template type change
|
|
614
691
|
const templateType = msg.payload.templateType as string | undefined;
|
|
615
692
|
if (templateType === 'custom01-wipro-timeline' && startPresenceSensorFn) {
|
|
616
|
-
logger.info('[Presence] Template changed to custom01-wipro-timeline
|
|
693
|
+
logger.info('[Presence] Template changed to custom01-wipro-timeline — starting sensor');
|
|
617
694
|
startPresenceSensorFn();
|
|
618
695
|
} else if (templateType && templateType !== 'custom01-wipro-timeline' && stopPresenceSensorFn) {
|
|
619
|
-
logger.info('[Presence] Template changed away from wipro-timeline
|
|
696
|
+
logger.info('[Presence] Template changed away from wipro-timeline — stopping sensor');
|
|
620
697
|
stopPresenceSensorFn();
|
|
621
698
|
}
|
|
622
699
|
|
|
@@ -624,15 +701,15 @@ function handleServerMessage(
|
|
|
624
701
|
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
625
702
|
if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
|
|
626
703
|
if (screenMap.length > 0) {
|
|
627
|
-
logger.info(`[MultiKiosk] Received screenMap update: ${screenMap.length} mapping(s)
|
|
704
|
+
logger.info(`[MultiKiosk] Received screenMap update: ${screenMap.length} mapping(s) — killing single kiosk`);
|
|
628
705
|
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
629
706
|
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
630
707
|
multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
|
|
631
708
|
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
632
709
|
});
|
|
633
710
|
} else {
|
|
634
|
-
// Empty screenMap
|
|
635
|
-
logger.info('[MultiKiosk] Empty screenMap received
|
|
711
|
+
// Empty screenMap — deactivate multi-screen, resume single kiosk
|
|
712
|
+
logger.info('[MultiKiosk] Empty screenMap received — deactivating multi-screen');
|
|
636
713
|
multiScreenKiosk.killAll().catch(() => {});
|
|
637
714
|
if (watchdog) watchdog.setMultiScreenActive(false);
|
|
638
715
|
}
|
|
@@ -645,14 +722,14 @@ function handleServerMessage(
|
|
|
645
722
|
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
646
723
|
if (screenMap && Array.isArray(screenMap)) {
|
|
647
724
|
if (screenMap.length > 0) {
|
|
648
|
-
logger.info(`[MultiKiosk] Received agent:screenMap: ${screenMap.length} mapping(s)
|
|
725
|
+
logger.info(`[MultiKiosk] Received agent:screenMap: ${screenMap.length} mapping(s) — killing single kiosk`);
|
|
649
726
|
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
650
727
|
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
651
728
|
multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
|
|
652
729
|
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
653
730
|
});
|
|
654
731
|
} else {
|
|
655
|
-
logger.info('[MultiKiosk] Empty agent:screenMap
|
|
732
|
+
logger.info('[MultiKiosk] Empty agent:screenMap — deactivating multi-screen');
|
|
656
733
|
multiScreenKiosk.killAll().catch(() => {});
|
|
657
734
|
if (watchdog) watchdog.setMultiScreenActive(false);
|
|
658
735
|
}
|
|
@@ -679,16 +756,17 @@ async function fetchDeviceConfig(
|
|
|
679
756
|
serverUrl: string,
|
|
680
757
|
identity: Identity,
|
|
681
758
|
logger: Logger
|
|
682
|
-
): Promise<{
|
|
683
|
-
comPort: string;
|
|
684
|
-
controllerId: string;
|
|
685
|
-
baudRate: number;
|
|
686
|
-
screenMap: ScreenMapping[];
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
759
|
+
): Promise<{
|
|
760
|
+
comPort: string;
|
|
761
|
+
controllerId: string;
|
|
762
|
+
baudRate: number;
|
|
763
|
+
screenMap: ScreenMapping[];
|
|
764
|
+
totalScreens: number;
|
|
765
|
+
oscPort: number;
|
|
766
|
+
oscAddress: string;
|
|
767
|
+
oscHost: string;
|
|
768
|
+
templateType: string;
|
|
769
|
+
} | null> {
|
|
692
770
|
try {
|
|
693
771
|
const url = `${serverUrl}/api/devices/${identity.deviceId}/config`;
|
|
694
772
|
const res = await fetch(url, {
|
|
@@ -710,8 +788,12 @@ async function fetchDeviceConfig(
|
|
|
710
788
|
const controllerId = (appConfig.controllerId as string) || comPort;
|
|
711
789
|
const baudRate = (device?.baud_rate as number) || 115200;
|
|
712
790
|
|
|
713
|
-
// Screen map from device config (set by admin)
|
|
714
|
-
const screenMap = (device?.screenMap as ScreenMapping[]) || [];
|
|
791
|
+
// Screen map from device config (set by admin)
|
|
792
|
+
const screenMap = (device?.screenMap as ScreenMapping[]) || [];
|
|
793
|
+
const appScreens = (appConfig.screens as Array<Record<string, unknown>>) || [];
|
|
794
|
+
const totalScreens = appScreens.length > 0
|
|
795
|
+
? appScreens.length
|
|
796
|
+
: ((appConfig.totalScreens as number) || 0);
|
|
715
797
|
|
|
716
798
|
// OSC settings from app config (custom07-osc template)
|
|
717
799
|
const inputSource = appConfig.inputSource as string;
|
|
@@ -721,7 +803,7 @@ async function fetchDeviceConfig(
|
|
|
721
803
|
|
|
722
804
|
const templateType = (assignedApp?.templateType as string) || '';
|
|
723
805
|
|
|
724
|
-
return { comPort, controllerId, baudRate, screenMap, oscPort, oscAddress, oscHost, templateType };
|
|
806
|
+
return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
|
|
725
807
|
} catch (err) {
|
|
726
808
|
logger.debug('Failed to fetch device config:', err);
|
|
727
809
|
return null;
|
|
@@ -732,3 +814,4 @@ main().catch((err) => {
|
|
|
732
814
|
console.error('Fatal error:', err);
|
|
733
815
|
process.exit(1);
|
|
734
816
|
});
|
|
817
|
+
|
package/src/lib/config.ts
CHANGED
|
@@ -32,9 +32,8 @@ const configSchema = z.object({
|
|
|
32
32
|
healthIntervalMs: z.number().int().min(5000).default(60000),
|
|
33
33
|
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
34
34
|
logFile: z.string().default('agent.log'),
|
|
35
|
-
identityFile: z.string().default('.lightman-identity.json'),
|
|
36
|
-
|
|
37
|
-
localServices: z.boolean().default(true),
|
|
35
|
+
identityFile: z.string().default('.lightman-identity.json'),
|
|
36
|
+
localServices: z.boolean().default(true),
|
|
38
37
|
kiosk: kioskSchema.optional(),
|
|
39
38
|
screenshot: screenshotSchema.optional(),
|
|
40
39
|
powerSchedule: powerScheduleSchema.optional(),
|
package/src/lib/identity.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import { mkdirSync } from 'fs';
|
|
4
|
-
import type { Identity } from './types.js';
|
|
5
|
-
|
|
6
|
-
export function readIdentity(filePath: string): Identity | null {
|
|
7
|
-
const fullPath = resolve(process.cwd(), filePath);
|
|
8
|
-
|
|
9
|
-
if (!existsSync(fullPath)) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
15
|
-
if (raw.deviceId && raw.apiKey) {
|
|
16
|
-
return { deviceId: raw.deviceId, apiKey: raw.apiKey };
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function writeIdentity(filePath: string, identity: Identity): void {
|
|
25
|
-
const fullPath = resolve(process.cwd(), filePath);
|
|
26
|
-
const dir = dirname(fullPath);
|
|
27
|
-
|
|
28
|
-
if (!existsSync(dir)) {
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
33
|
-
|
|
34
|
-
// Ensure permissions are correct even if file already existed
|
|
35
|
-
try {
|
|
36
|
-
chmodSync(fullPath, 0o600);
|
|
37
|
-
} catch {
|
|
38
|
-
// Ignore permission errors on Windows
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { mkdirSync } from 'fs';
|
|
4
|
+
import type { Identity } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function readIdentity(filePath: string): Identity | null {
|
|
7
|
+
const fullPath = resolve(process.cwd(), filePath);
|
|
8
|
+
|
|
9
|
+
if (!existsSync(fullPath)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
15
|
+
if (raw.deviceId && raw.apiKey) {
|
|
16
|
+
return { deviceId: raw.deviceId, apiKey: raw.apiKey };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeIdentity(filePath: string, identity: Identity): void {
|
|
25
|
+
const fullPath = resolve(process.cwd(), filePath);
|
|
26
|
+
const dir = dirname(fullPath);
|
|
27
|
+
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
33
|
+
|
|
34
|
+
// Ensure permissions are correct even if file already existed
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(fullPath, 0o600);
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore permission errors on Windows
|
|
39
|
+
}
|
|
40
|
+
}
|