lightman-agent 1.0.22 → 1.0.24
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/bin/cms-agent.js +53 -87
- package/package.json +1 -1
- package/scripts/guardian.ps1 +25 -6
- package/scripts/install-linux.sh +4 -4
- package/scripts/install-windows.ps1 +165 -58
- package/scripts/launch-kiosk.vbs +23 -7
- package/scripts/lightman-shell.bat +30 -9
- package/scripts/setup.ps1 +25 -9
- package/scripts/setup.sh +4 -4
- package/src/index.ts +214 -156
- package/src/lib/screenMap.ts +135 -135
- package/src/services/multiScreenKiosk.ts +356 -356
|
@@ -15,6 +15,7 @@ REM ================================================================
|
|
|
15
15
|
|
|
16
16
|
set INSTALL_DIR=C:\Program Files\Lightman\Agent
|
|
17
17
|
set CONFIG_FILE=%INSTALL_DIR%\agent.config.json
|
|
18
|
+
set URL_SIDECAR=C:\ProgramData\Lightman\kiosk-url.txt
|
|
18
19
|
set CHROME_DATA=C:\ProgramData\Lightman\chrome-kiosk
|
|
19
20
|
set LOG_FILE=C:\ProgramData\Lightman\logs\shell.log
|
|
20
21
|
|
|
@@ -68,14 +69,27 @@ if not "%DEVICE_SLUG%"=="" (
|
|
|
68
69
|
echo [%date% %time%] WARNING: No slug in config! >> "%LOG_FILE%"
|
|
69
70
|
)
|
|
70
71
|
|
|
72
|
+
REM If agent wrote a URL sidecar (includes deviceId/apiKey), prefer it.
|
|
73
|
+
if exist "%URL_SIDECAR%" (
|
|
74
|
+
for /f "usebackq delims=" %%u in ("%URL_SIDECAR%") do set SIDE_URL=%%u
|
|
75
|
+
if not "%SIDE_URL%"=="" (
|
|
76
|
+
set URL=%SIDE_URL%
|
|
77
|
+
echo [%date% %time%] Using sidecar URL >> "%LOG_FILE%"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
71
81
|
REM Fallback browser
|
|
72
82
|
if "%BROWSER%"=="" (
|
|
73
83
|
if exist "C:\Program Files\Google\Chrome\Application\chrome.exe" (
|
|
74
84
|
set "BROWSER=C:\Program Files\Google\Chrome\Application\chrome.exe"
|
|
75
85
|
) else if exist "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" (
|
|
76
86
|
set "BROWSER=C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
|
|
87
|
+
) else if exist "C:\Program Files\Microsoft\Edge\Application\msedge.exe" (
|
|
88
|
+
set "BROWSER=C:\Program Files\Microsoft\Edge\Application\msedge.exe"
|
|
89
|
+
) else if exist "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" (
|
|
90
|
+
set "BROWSER=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
|
77
91
|
) else (
|
|
78
|
-
echo [%date% %time%] ERROR:
|
|
92
|
+
echo [%date% %time%] ERROR: No supported kiosk browser found! >> "%LOG_FILE%"
|
|
79
93
|
start explorer.exe
|
|
80
94
|
exit /b 1
|
|
81
95
|
)
|
|
@@ -109,20 +123,27 @@ REM ----------------------------------------------------------------
|
|
|
109
123
|
REM Infinite Chrome loop
|
|
110
124
|
REM ----------------------------------------------------------------
|
|
111
125
|
:loop
|
|
112
|
-
REM
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
REM Prefer sidecar URL for auth params/device routing; fallback to slug URL.
|
|
127
|
+
set SIDE_URL=
|
|
128
|
+
if exist "%URL_SIDECAR%" (
|
|
129
|
+
for /f "usebackq delims=" %%u in ("%URL_SIDECAR%") do set SIDE_URL=%%u
|
|
130
|
+
)
|
|
131
|
+
if not "%SIDE_URL%"=="" (
|
|
132
|
+
set URL=%SIDE_URL%
|
|
133
|
+
) else (
|
|
134
|
+
REM Re-read slug from config on every loop iteration.
|
|
135
|
+
if exist "%CONFIG_FILE%" (
|
|
136
|
+
for /f "delims=" %%a in ('node -e "try{console.log(JSON.parse(require('fs').readFileSync(String.raw`%CONFIG_FILE%`,'utf8')).deviceSlug)}catch(e){console.log('')}" 2^>nul') do (
|
|
137
|
+
if not "%%a"=="" set URL=http://localhost:3403/display/%%a
|
|
138
|
+
)
|
|
118
139
|
)
|
|
119
140
|
)
|
|
120
141
|
|
|
121
|
-
echo [%date% %time%] Launching
|
|
142
|
+
echo [%date% %time%] Launching browser: %URL% >> "%LOG_FILE%"
|
|
122
143
|
|
|
123
144
|
start /wait "" "%BROWSER%" --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble --no-first-run --no-default-browser-check --start-fullscreen --disable-translate --disable-extensions --autoplay-policy=no-user-gesture-required --disable-features=TranslateUI --user-data-dir="%CHROME_DATA%" "%URL%"
|
|
124
145
|
|
|
125
|
-
echo [%date% %time%]
|
|
146
|
+
echo [%date% %time%] Browser exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
|
|
126
147
|
timeout /t 3 /nobreak >nul
|
|
127
148
|
|
|
128
149
|
goto loop
|
package/scripts/setup.ps1
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# Generates agent.config.json for this specific device.
|
|
3
3
|
#
|
|
4
4
|
# Usage (run from agent directory or scripts directory):
|
|
5
|
-
# powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.
|
|
6
|
-
# powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.
|
|
5
|
+
# powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.10.100:3401"
|
|
6
|
+
# powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.10.100:3401" -Timezone "Asia/Kolkata"
|
|
7
7
|
#
|
|
8
8
|
# This script MUST be run once on every new device installation.
|
|
9
9
|
# It clears any cached identity so the device provisions fresh.
|
|
@@ -42,6 +42,23 @@ Write-Host " Install dir: $InstallDir"
|
|
|
42
42
|
Write-Host " Timezone: $Timezone"
|
|
43
43
|
Write-Host ""
|
|
44
44
|
|
|
45
|
+
function Get-KioskBrowserPath {
|
|
46
|
+
$candidates = @(
|
|
47
|
+
"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
48
|
+
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
49
|
+
(Join-Path $env:LOCALAPPDATA "Google\Chrome\Application\chrome.exe"),
|
|
50
|
+
"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
|
51
|
+
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
52
|
+
(Join-Path $env:LOCALAPPDATA "Microsoft\Edge\Application\msedge.exe")
|
|
53
|
+
) | Where-Object { $_ -and (Test-Path $_) }
|
|
54
|
+
|
|
55
|
+
if ($candidates.Count -gt 0) {
|
|
56
|
+
return $candidates[0]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw "Neither Google Chrome nor Microsoft Edge was found. Install one of them and re-run setup."
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
# 1. Clear cached identity (CRITICAL - prevents old device credentials leaking)
|
|
46
63
|
$IdentityFile = Join-Path $InstallDir ".lightman-identity.json"
|
|
47
64
|
if (Test-Path $IdentityFile) {
|
|
@@ -55,13 +72,12 @@ if (Test-Path $IdentityFile) {
|
|
|
55
72
|
$KioskUrl = "http://localhost:3403/display/$Slug"
|
|
56
73
|
|
|
57
74
|
# 3. Detect browser path
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
$BrowserPath
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Write-Host "[WARN] Chrome not found - using 'chromium-browser'" -ForegroundColor Yellow
|
|
75
|
+
try {
|
|
76
|
+
$BrowserPath = Get-KioskBrowserPath
|
|
77
|
+
Write-Host "[OK] Using kiosk browser: $BrowserPath" -ForegroundColor Green
|
|
78
|
+
} catch {
|
|
79
|
+
Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
|
|
80
|
+
exit 1
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
$ChromeDataDir = "C:\ProgramData\Lightman\chrome-kiosk"
|
package/scripts/setup.sh
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# Generates agent.config.json for this specific device.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# sudo bash setup.sh --slug f-av01 --server http://192.168.
|
|
7
|
-
# sudo bash setup.sh --slug f-av01 --server http://192.168.
|
|
6
|
+
# sudo bash setup.sh --slug f-av01 --server http://192.168.10.100:3401
|
|
7
|
+
# sudo bash setup.sh --slug f-av01 --server http://192.168.10.100:3401 --timezone Asia/Kolkata --dir /opt/lightman/agent
|
|
8
8
|
#
|
|
9
9
|
# This script MUST be run once on every new device installation.
|
|
10
10
|
# It clears any cached identity so the device provisions fresh.
|
|
@@ -37,13 +37,13 @@ done
|
|
|
37
37
|
|
|
38
38
|
if [[ -z "$SLUG" ]]; then
|
|
39
39
|
echo "Error: --slug is required"
|
|
40
|
-
echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.
|
|
40
|
+
echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.10.100:3401"
|
|
41
41
|
exit 1
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
if [[ -z "$SERVER" ]]; then
|
|
45
45
|
echo "Error: --server is required"
|
|
46
|
-
echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.
|
|
46
|
+
echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.10.100:3401"
|
|
47
47
|
exit 1
|
|
48
48
|
fi
|
|
49
49
|
|
package/src/index.ts
CHANGED
|
@@ -10,11 +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 type { DetectedScreen } from './lib/screens.js';
|
|
16
|
-
import { resolveScreenMap } from './lib/screenMap.js';
|
|
17
|
-
import { registerDisplayCommands } from './commands/display.js';
|
|
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';
|
|
18
18
|
import { registerNetworkCommands } from './commands/network.js';
|
|
19
19
|
import { Updater } from './services/updater.js';
|
|
20
20
|
import { registerUpdateCommands } from './commands/update.js';
|
|
@@ -32,7 +32,7 @@ import { SerialBridge } from './services/serialBridge.js';
|
|
|
32
32
|
import { OscBridge } from './services/oscBridge.js';
|
|
33
33
|
import { LocalEventServer } from './services/localEvents.js';
|
|
34
34
|
import { PresenceSensor } from './services/presenceSensor.js';
|
|
35
|
-
import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
|
|
35
|
+
import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
|
|
36
36
|
|
|
37
37
|
function getAgentVersion(): string {
|
|
38
38
|
try {
|
|
@@ -111,10 +111,26 @@ async function main(): Promise<void> {
|
|
|
111
111
|
serverUrl: config.serverUrl,
|
|
112
112
|
identity,
|
|
113
113
|
logger,
|
|
114
|
-
onMessage: (msg: WsMessage) => {
|
|
115
|
-
handleServerMessage(
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
onMessage: (msg: WsMessage) => {
|
|
115
|
+
handleServerMessage(
|
|
116
|
+
msg,
|
|
117
|
+
commandExecutor,
|
|
118
|
+
logger,
|
|
119
|
+
powerScheduler,
|
|
120
|
+
startSerialBridge,
|
|
121
|
+
stopSerialBridge,
|
|
122
|
+
startOscBridge,
|
|
123
|
+
stopOscBridge,
|
|
124
|
+
multiScreenKiosk,
|
|
125
|
+
getIdentity,
|
|
126
|
+
kioskManager,
|
|
127
|
+
watchdog,
|
|
128
|
+
startPresenceSensor,
|
|
129
|
+
stopPresenceSensor,
|
|
130
|
+
() => lastKnownTotalScreens
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
118
134
|
|
|
119
135
|
// 4. Start health monitor
|
|
120
136
|
const healthMonitor = new HealthMonitor(
|
|
@@ -163,65 +179,66 @@ async function main(): Promise<void> {
|
|
|
163
179
|
const kioskManager = new KioskManager(kioskConfig, logger);
|
|
164
180
|
registerKioskCommands(commandExecutor.register.bind(commandExecutor), kioskManager, logger);
|
|
165
181
|
|
|
166
|
-
// Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
|
|
167
|
-
const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
|
|
168
|
-
const getIdentity = () => identity!;
|
|
169
|
-
registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
|
|
170
|
-
|
|
182
|
+
// Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
|
|
183
|
+
const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
|
|
184
|
+
const getIdentity = () => identity!;
|
|
185
|
+
registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
|
|
186
|
+
|
|
171
187
|
// Detect physical screens and keep them fresh (multi-display setups can change after boot).
|
|
188
|
+
let lastKnownTotalScreens = 0;
|
|
172
189
|
let detectedScreens = detectScreens(logger);
|
|
173
190
|
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);
|
|
191
|
+
|
|
192
|
+
const toScreenPayload = (screens: DetectedScreen[]) => (
|
|
193
|
+
screens.map((s) => ({
|
|
194
|
+
hardwareId: s.hardwareId,
|
|
195
|
+
name: s.name,
|
|
196
|
+
index: s.index,
|
|
197
|
+
width: s.width,
|
|
198
|
+
height: s.height,
|
|
199
|
+
x: s.x,
|
|
200
|
+
y: s.y,
|
|
201
|
+
primary: s.primary,
|
|
202
|
+
}))
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const sendAgentRegister = () => {
|
|
206
|
+
wsClient.send({
|
|
207
|
+
type: 'agent:register',
|
|
208
|
+
payload: {
|
|
209
|
+
agentVersion: getAgentVersion(),
|
|
210
|
+
screens: toScreenPayload(detectedScreens),
|
|
211
|
+
},
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const refreshDetectedScreens = async (reason: string) => {
|
|
217
|
+
const latest = detectScreens(logger);
|
|
218
|
+
if (!haveScreensChanged(detectedScreens, latest)) return;
|
|
219
|
+
|
|
220
|
+
logger.info(`[Screens] Topology changed (${reason}): ${detectedScreens.length} -> ${latest.length}`);
|
|
221
|
+
detectedScreens = latest;
|
|
222
|
+
multiScreenKiosk.setDetectedScreens(detectedScreens);
|
|
223
|
+
|
|
224
|
+
if (wsClient.isConnected()) {
|
|
225
|
+
sendAgentRegister();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (multiScreenKiosk.hasDesiredScreenMap()) {
|
|
229
|
+
try {
|
|
230
|
+
await multiScreenKiosk.reapplyDesiredMap(identity);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.error('[MultiKiosk] Failed to reapply desired screen map after topology change:', err);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const screenRefreshInterval = setInterval(() => {
|
|
238
|
+
refreshDetectedScreens('periodic-refresh').catch((err) => {
|
|
239
|
+
logger.error('[Screens] Periodic refresh failed:', err);
|
|
240
|
+
});
|
|
241
|
+
}, 20_000);
|
|
225
242
|
|
|
226
243
|
// Create Watchdog (Phase 20)
|
|
227
244
|
const watchdog = new Watchdog(
|
|
@@ -464,11 +481,11 @@ async function main(): Promise<void> {
|
|
|
464
481
|
watchdog.start();
|
|
465
482
|
powerScheduler.start();
|
|
466
483
|
|
|
467
|
-
// Send registration message once connected, then auto-launch kiosk
|
|
468
|
-
const registerInterval = setInterval(() => {
|
|
469
|
-
if (wsClient.isConnected()) {
|
|
470
|
-
sendAgentRegister();
|
|
471
|
-
clearInterval(registerInterval);
|
|
484
|
+
// Send registration message once connected, then auto-launch kiosk
|
|
485
|
+
const registerInterval = setInterval(() => {
|
|
486
|
+
if (wsClient.isConnected()) {
|
|
487
|
+
sendAgentRegister();
|
|
488
|
+
clearInterval(registerInterval);
|
|
472
489
|
|
|
473
490
|
// Fetch device config first to decide single vs multi-screen kiosk
|
|
474
491
|
fetchDeviceConfig(config.serverUrl, identity, logger).then((deviceCfg) => {
|
|
@@ -499,31 +516,30 @@ async function main(): Promise<void> {
|
|
|
499
516
|
} else {
|
|
500
517
|
logger.info('[Presence] Template is not presence-enabled — sensor not started');
|
|
501
518
|
}
|
|
502
|
-
// Multi-screen handling:
|
|
503
|
-
// 1) Use explicit screenMap when present.
|
|
504
|
-
// 2) For multi-screen apps without a saved map, auto-create placeholders.
|
|
519
|
+
// Multi-screen handling:
|
|
520
|
+
// 1) Use explicit screenMap when present.
|
|
521
|
+
// 2) For multi-screen apps without a saved map, auto-create placeholders.
|
|
505
522
|
const requestedScreenMap = deviceCfg?.screenMap || [];
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
: (isMultiScreenApp ? createPlaceholderScreenMap(deviceCfg.totalScreens) : []);
|
|
523
|
+
const totalScreens = Math.max(deviceCfg?.totalScreens || 0, requestedScreenMap.length);
|
|
524
|
+
lastKnownTotalScreens = totalScreens;
|
|
525
|
+
const effectiveRequestedMap = normalizeScreenMapForTotalScreens(requestedScreenMap, totalScreens);
|
|
510
526
|
|
|
511
527
|
if (effectiveRequestedMap.length > 0) {
|
|
512
528
|
const resolved = resolveScreenMap({
|
|
513
529
|
requestedScreenMap: effectiveRequestedMap,
|
|
514
530
|
detectedScreens,
|
|
515
|
-
totalScreens
|
|
531
|
+
totalScreens,
|
|
516
532
|
});
|
|
517
533
|
logger.info(
|
|
518
|
-
`[MultiKiosk] Effective map ready: requested=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${
|
|
534
|
+
`[MultiKiosk] Effective map ready: requested=${requestedScreenMap.length}, effective=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${totalScreens}`
|
|
519
535
|
);
|
|
520
536
|
watchdog.setMultiScreenActive(true);
|
|
521
537
|
multiScreenKiosk.applyScreenMap(effectiveRequestedMap, identity).catch((err) => {
|
|
522
538
|
logger.error('[MultiKiosk] Failed to apply effective screenMap from config:', err);
|
|
523
|
-
});
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
527
543
|
// No effective multi-screen map - launch single-screen kiosk as before
|
|
528
544
|
if (config.kiosk) {
|
|
529
545
|
if (config.kiosk.shellMode) {
|
|
@@ -552,10 +568,10 @@ async function main(): Promise<void> {
|
|
|
552
568
|
}, 1000);
|
|
553
569
|
|
|
554
570
|
// 7. Graceful shutdown
|
|
555
|
-
const shutdown = async (signal: string): Promise<void> => {
|
|
556
|
-
logger.info(`${signal} received. Shutting down...`);
|
|
557
|
-
clearInterval(registerInterval);
|
|
558
|
-
clearInterval(screenRefreshInterval);
|
|
571
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
572
|
+
logger.info(`${signal} received. Shutting down...`);
|
|
573
|
+
clearInterval(registerInterval);
|
|
574
|
+
clearInterval(screenRefreshInterval);
|
|
559
575
|
|
|
560
576
|
// Wait for updater to finish if it's busy (max 60s)
|
|
561
577
|
if (updater.isBusy()) {
|
|
@@ -603,50 +619,74 @@ async function main(): Promise<void> {
|
|
|
603
619
|
process.exit(1);
|
|
604
620
|
});
|
|
605
621
|
|
|
606
|
-
logger.info('LIGHTMAN Agent running.');
|
|
607
|
-
}
|
|
608
|
-
|
|
622
|
+
logger.info('LIGHTMAN Agent running.');
|
|
623
|
+
}
|
|
624
|
+
|
|
609
625
|
function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
|
|
610
626
|
const count = Math.max(0, Math.floor(totalScreens || 0));
|
|
611
627
|
return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
|
|
612
628
|
}
|
|
613
629
|
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|| a.height !== b.height
|
|
626
|
-
|| a.primary !== b.primary
|
|
627
|
-
) {
|
|
628
|
-
return true;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
630
|
+
function normalizeScreenMapForTotalScreens(
|
|
631
|
+
screenMap: ScreenMapping[] | undefined,
|
|
632
|
+
totalScreens: number
|
|
633
|
+
): ScreenMapping[] {
|
|
634
|
+
const requested = Array.isArray(screenMap)
|
|
635
|
+
? screenMap.map((m) => ({
|
|
636
|
+
hardwareId: String(m.hardwareId || ''),
|
|
637
|
+
url: String(m.url || ''),
|
|
638
|
+
...(m.label ? { label: String(m.label) } : {}),
|
|
639
|
+
}))
|
|
640
|
+
: [];
|
|
633
641
|
|
|
642
|
+
const targetCount = Math.max(requested.length, Math.max(0, Math.floor(totalScreens || 0)));
|
|
643
|
+
if (targetCount === 0) return [];
|
|
644
|
+
|
|
645
|
+
if (requested.length >= targetCount) return requested;
|
|
646
|
+
|
|
647
|
+
return [
|
|
648
|
+
...requested,
|
|
649
|
+
...createPlaceholderScreenMap(targetCount - requested.length),
|
|
650
|
+
];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boolean {
|
|
654
|
+
if (prev.length !== next.length) return true;
|
|
655
|
+
for (let i = 0; i < prev.length; i++) {
|
|
656
|
+
const a = prev[i];
|
|
657
|
+
const b = next[i];
|
|
658
|
+
if (
|
|
659
|
+
a.hardwareId !== b.hardwareId
|
|
660
|
+
|| a.index !== b.index
|
|
661
|
+
|| a.x !== b.x
|
|
662
|
+
|| a.y !== b.y
|
|
663
|
+
|| a.width !== b.width
|
|
664
|
+
|| a.height !== b.height
|
|
665
|
+
|| a.primary !== b.primary
|
|
666
|
+
) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
|
|
634
673
|
function handleServerMessage(
|
|
635
674
|
msg: WsMessage,
|
|
636
|
-
commandExecutor: CommandExecutor,
|
|
675
|
+
commandExecutor: CommandExecutor,
|
|
637
676
|
logger: Logger,
|
|
638
677
|
powerScheduler?: PowerScheduler,
|
|
639
678
|
startSerialBridge?: (comPort: string, controllerId: string, baudRate?: number) => void,
|
|
640
679
|
stopSerialBridge?: () => void,
|
|
641
680
|
startOscBridgeFn?: (oscPort: number, oscAddress: string, oscHost?: string) => void,
|
|
642
681
|
stopOscBridgeFn?: () => void,
|
|
643
|
-
multiScreenKiosk?: MultiScreenKioskManager,
|
|
644
|
-
getIdentity?: () => Identity,
|
|
645
|
-
kioskManager?: KioskManager,
|
|
646
|
-
watchdog?: Watchdog,
|
|
647
|
-
startPresenceSensorFn?: (port?: string) => void,
|
|
648
|
-
stopPresenceSensorFn?: () => void
|
|
649
|
-
)
|
|
682
|
+
multiScreenKiosk?: MultiScreenKioskManager,
|
|
683
|
+
getIdentity?: () => Identity,
|
|
684
|
+
kioskManager?: KioskManager,
|
|
685
|
+
watchdog?: Watchdog,
|
|
686
|
+
startPresenceSensorFn?: (port?: string) => void,
|
|
687
|
+
stopPresenceSensorFn?: () => void,
|
|
688
|
+
getTotalScreensHint?: () => number
|
|
689
|
+
): void {
|
|
650
690
|
switch (msg.type) {
|
|
651
691
|
case 'connected':
|
|
652
692
|
logger.info('Server acknowledged connection');
|
|
@@ -698,16 +738,25 @@ function handleServerMessage(
|
|
|
698
738
|
}
|
|
699
739
|
|
|
700
740
|
// Admin pushed updated screenMap via device config save
|
|
701
|
-
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
702
|
-
if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
741
|
+
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
742
|
+
if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
|
|
743
|
+
const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
|
|
744
|
+
const hintTotalScreens = Math.max(
|
|
745
|
+
Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
|
|
746
|
+
getTotalScreensHint ? getTotalScreensHint() : 0
|
|
747
|
+
);
|
|
748
|
+
const effectiveScreenMap = screenMap.length > 0
|
|
749
|
+
? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
|
|
750
|
+
: screenMap;
|
|
751
|
+
|
|
752
|
+
if (screenMap.length > 0) {
|
|
753
|
+
logger.info(`[MultiKiosk] Received screenMap update: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
|
|
754
|
+
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
755
|
+
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
756
|
+
multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
|
|
757
|
+
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
758
|
+
});
|
|
759
|
+
} else {
|
|
711
760
|
// Empty screenMap — deactivate multi-screen, resume single kiosk
|
|
712
761
|
logger.info('[MultiKiosk] Empty screenMap received — deactivating multi-screen');
|
|
713
762
|
multiScreenKiosk.killAll().catch(() => {});
|
|
@@ -719,16 +768,25 @@ function handleServerMessage(
|
|
|
719
768
|
case 'agent:screenMap':
|
|
720
769
|
// Direct screenMap push from server
|
|
721
770
|
if (msg.payload && multiScreenKiosk && getIdentity) {
|
|
722
|
-
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
723
|
-
if (screenMap && Array.isArray(screenMap)) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
771
|
+
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
772
|
+
if (screenMap && Array.isArray(screenMap)) {
|
|
773
|
+
const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
|
|
774
|
+
const hintTotalScreens = Math.max(
|
|
775
|
+
Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
|
|
776
|
+
getTotalScreensHint ? getTotalScreensHint() : 0
|
|
777
|
+
);
|
|
778
|
+
const effectiveScreenMap = screenMap.length > 0
|
|
779
|
+
? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
|
|
780
|
+
: screenMap;
|
|
781
|
+
|
|
782
|
+
if (screenMap.length > 0) {
|
|
783
|
+
logger.info(`[MultiKiosk] Received agent:screenMap: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
|
|
784
|
+
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
785
|
+
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
786
|
+
multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
|
|
787
|
+
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
788
|
+
});
|
|
789
|
+
} else {
|
|
732
790
|
logger.info('[MultiKiosk] Empty agent:screenMap — deactivating multi-screen');
|
|
733
791
|
multiScreenKiosk.killAll().catch(() => {});
|
|
734
792
|
if (watchdog) watchdog.setMultiScreenActive(false);
|
|
@@ -756,17 +814,17 @@ async function fetchDeviceConfig(
|
|
|
756
814
|
serverUrl: string,
|
|
757
815
|
identity: Identity,
|
|
758
816
|
logger: Logger
|
|
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> {
|
|
817
|
+
): Promise<{
|
|
818
|
+
comPort: string;
|
|
819
|
+
controllerId: string;
|
|
820
|
+
baudRate: number;
|
|
821
|
+
screenMap: ScreenMapping[];
|
|
822
|
+
totalScreens: number;
|
|
823
|
+
oscPort: number;
|
|
824
|
+
oscAddress: string;
|
|
825
|
+
oscHost: string;
|
|
826
|
+
templateType: string;
|
|
827
|
+
} | null> {
|
|
770
828
|
try {
|
|
771
829
|
const url = `${serverUrl}/api/devices/${identity.deviceId}/config`;
|
|
772
830
|
const res = await fetch(url, {
|
|
@@ -788,12 +846,12 @@ async function fetchDeviceConfig(
|
|
|
788
846
|
const controllerId = (appConfig.controllerId as string) || comPort;
|
|
789
847
|
const baudRate = (device?.baud_rate as number) || 115200;
|
|
790
848
|
|
|
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);
|
|
849
|
+
// Screen map from device config (set by admin)
|
|
850
|
+
const screenMap = (device?.screenMap as ScreenMapping[]) || [];
|
|
851
|
+
const appScreens = (appConfig.screens as Array<Record<string, unknown>>) || [];
|
|
852
|
+
const totalScreens = appScreens.length > 0
|
|
853
|
+
? appScreens.length
|
|
854
|
+
: ((appConfig.totalScreens as number) || 0);
|
|
797
855
|
|
|
798
856
|
// OSC settings from app config (custom07-osc template)
|
|
799
857
|
const inputSource = appConfig.inputSource as string;
|
|
@@ -803,7 +861,7 @@ async function fetchDeviceConfig(
|
|
|
803
861
|
|
|
804
862
|
const templateType = (assignedApp?.templateType as string) || '';
|
|
805
863
|
|
|
806
|
-
return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
|
|
864
|
+
return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
|
|
807
865
|
} catch (err) {
|
|
808
866
|
logger.debug('Failed to fetch device config:', err);
|
|
809
867
|
return null;
|