@trillboards/edge-sdk 0.2.4 → 0.2.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.
Files changed (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +55 -13
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
@@ -0,0 +1,828 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.setupHostFingerprint = setupHostFingerprint;
37
+ exports.buildSetupPortalUrl = buildSetupPortalUrl;
38
+ exports.openSetupPortal = openSetupPortal;
39
+ exports.normalizeDirectStreamUrl = normalizeDirectStreamUrl;
40
+ exports.mergeDiscoveredHosts = mergeDiscoveredHosts;
41
+ exports.mergeConfigFile = mergeConfigFile;
42
+ exports.runSetupMode = runSetupMode;
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const path = __importStar(require("path"));
46
+ const crypto_1 = require("crypto");
47
+ const child_process_1 = require("child_process");
48
+ const SetupSessionClient_1 = require("./SetupSessionClient");
49
+ const ChannelProber_1 = require("./ChannelProber");
50
+ const discovery_1 = require("./discovery");
51
+ const connectorCadence_1 = require("./connectorCadence");
52
+ const legacyRecorderProfile_1 = require("./legacyRecorderProfile");
53
+ const RecorderAddressRecovery_1 = require("./RecorderAddressRecovery");
54
+ /** Stable, non-PII connector fingerprint (same derivation as cli.ts pairing). */
55
+ function setupHostFingerprint() {
56
+ const raw = `${os.hostname()}|${process.platform}|${os.arch()}`;
57
+ return 'conn_' + (0, crypto_1.createHash)('sha256').update(raw).digest('hex').slice(0, 24);
58
+ }
59
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
60
+ const DEFAULT_PORTAL_BASE_URL = 'https://trillboards.com';
61
+ const SETUP_CODE_HASH_PARAM = 'cctvSetupCode';
62
+ /**
63
+ * Build the one-time browser handoff without sending the setup code to the web
64
+ * server. URL fragments stay client-side; the portal consumes and removes it.
65
+ */
66
+ function buildSetupPortalUrl(displayCode, portalBaseUrl = DEFAULT_PORTAL_BASE_URL) {
67
+ const normalized = String(displayCode || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
68
+ if (normalized.length !== 8)
69
+ throw new Error('setup code must contain 8 characters');
70
+ const base = new URL(portalBaseUrl);
71
+ if (base.protocol !== 'https:' && base.protocol !== 'http:') {
72
+ throw new Error('portal base URL must use http or https');
73
+ }
74
+ const formatted = `${normalized.slice(0, 4)}-${normalized.slice(4)}`;
75
+ const target = new URL('/earner/screens', base);
76
+ target.searchParams.set('addCamera', '1');
77
+ target.hash = new URLSearchParams({ [SETUP_CODE_HASH_PARAM]: formatted }).toString();
78
+ return target.toString();
79
+ }
80
+ /** Open the setup handoff in the owner's default browser. Never uses a shell. */
81
+ function openSetupPortal(url, platform = process.platform) {
82
+ const command = platform === 'darwin'
83
+ ? { executable: 'open', args: [url] }
84
+ : platform === 'win32'
85
+ ? { executable: 'explorer.exe', args: [url] }
86
+ : platform === 'linux'
87
+ ? { executable: 'xdg-open', args: [url] }
88
+ : null;
89
+ if (!command)
90
+ return Promise.resolve(false);
91
+ return new Promise((resolve) => {
92
+ let settled = false;
93
+ const finish = (opened) => {
94
+ if (settled)
95
+ return;
96
+ settled = true;
97
+ resolve(opened);
98
+ };
99
+ try {
100
+ const child = (0, child_process_1.spawn)(command.executable, command.args, {
101
+ detached: true,
102
+ stdio: 'ignore',
103
+ windowsHide: true,
104
+ });
105
+ child.once('spawn', () => {
106
+ child.unref();
107
+ finish(true);
108
+ });
109
+ child.once('error', () => finish(false));
110
+ }
111
+ catch {
112
+ finish(false);
113
+ }
114
+ });
115
+ }
116
+ function normalizeDirectStreamUrl(input) {
117
+ const streamUrl = String(input || '').trim();
118
+ if (!streamUrl || streamUrl.length > 2048) {
119
+ throw new Error('Enter an rtsp:// or rtsps:// camera stream link.');
120
+ }
121
+ let parsed;
122
+ try {
123
+ parsed = new URL(streamUrl);
124
+ }
125
+ catch {
126
+ throw new Error('Enter a valid rtsp:// or rtsps:// camera stream link.');
127
+ }
128
+ if (parsed.protocol !== 'rtsp:' && parsed.protocol !== 'rtsps:') {
129
+ throw new Error('Only rtsp:// and rtsps:// stream links are supported.');
130
+ }
131
+ if (!parsed.hostname) {
132
+ throw new Error('The stream link is missing a host.');
133
+ }
134
+ if (parsed.hash) {
135
+ parsed.hash = '';
136
+ return parsed.toString();
137
+ }
138
+ return streamUrl;
139
+ }
140
+ function directStreamInfo(streamUrl, preferredName) {
141
+ const u = new URL(streamUrl);
142
+ const host = u.hostname;
143
+ const port = Number(u.port) || 554;
144
+ const looksUbiquiti = u.port === '7441' || /enableSrtp/i.test(u.search);
145
+ return {
146
+ host,
147
+ port,
148
+ name: preferredName || (looksUbiquiti ? 'UniFi camera' : `Camera at ${host}`),
149
+ brand: looksUbiquiti ? 'ubiquiti' : 'direct-link',
150
+ };
151
+ }
152
+ function mergeDiscoveredHosts(onvifHosts, sweptHosts) {
153
+ const sweptByHost = new Map(sweptHosts
154
+ .filter((host) => typeof host?.host === 'string' && host.host)
155
+ .map((host) => [String(host.host), host]));
156
+ const seen = new Set();
157
+ const merged = onvifHosts.map((host) => {
158
+ const key = typeof host?.host === 'string' ? host.host : '';
159
+ if (!key)
160
+ return host;
161
+ seen.add(key);
162
+ const swept = sweptByHost.get(key);
163
+ if (!swept)
164
+ return host;
165
+ return {
166
+ ...host,
167
+ ...(Array.isArray(swept.openPorts) ? { openPorts: swept.openPorts } : {}),
168
+ ...(typeof swept.brandHint === 'string' ? { brandHint: swept.brandHint } : {}),
169
+ };
170
+ });
171
+ for (const swept of sweptByHost.values()) {
172
+ if (!seen.has(String(swept.host)))
173
+ merged.push(swept);
174
+ }
175
+ return merged;
176
+ }
177
+ function recorderIdentityFor(camera, credsByHost, probedByChannelKey, macByHost) {
178
+ const probed = probedByChannelKey.get(camera.channelKey);
179
+ const serial = String(probed?.serial || '').trim();
180
+ const mac = camera.host ? macByHost.get(camera.host) : undefined;
181
+ if (serial) {
182
+ return {
183
+ id: `onvif:${(0, crypto_1.createHash)('sha256').update(serial).digest('hex').slice(0, 24)}`,
184
+ serial,
185
+ ...(mac ? { mac } : {}),
186
+ };
187
+ }
188
+ if (!camera.host)
189
+ return undefined;
190
+ if (mac) {
191
+ return {
192
+ id: `mac:${(0, crypto_1.createHash)('sha256').update(mac).digest('hex').slice(0, 24)}`,
193
+ mac,
194
+ };
195
+ }
196
+ const username = credsByHost.get(camera.host)?.username || '';
197
+ const seed = `${camera.host}:${camera.port ?? 554}:${username}`;
198
+ return { id: `local:${(0, crypto_1.createHash)('sha256').update(seed).digest('hex').slice(0, 24)}` };
199
+ }
200
+ function cameraHost(camera) {
201
+ try {
202
+ const url = new URL(String(camera.rtspUrl || ''));
203
+ return url.hostname || null;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ function recorderGroupKey(camera) {
210
+ if (camera.sourceType !== 'recorder')
211
+ return null;
212
+ const recorder = camera.recorder;
213
+ if (recorder && typeof recorder === 'object' && typeof recorder.id === 'string') {
214
+ return `id:${recorder.id}`;
215
+ }
216
+ const host = cameraHost(camera);
217
+ return host ? `host:${host}` : null;
218
+ }
219
+ /**
220
+ * Merge a finalized setup into the local config.
221
+ *
222
+ * The finalized channel set is authoritative for each recorder touched by this
223
+ * setup. That prevents deleted/replaced portal cameras from remaining as hidden
224
+ * local sensing loops, while feeds from other recorders and direct RTSPS links
225
+ * remain intact.
226
+ */
227
+ function mergeConfigFile(existingRaw, incoming, credsByHost, directUrlsByChannelKey = new Map(), probedByChannelKey = new Map(), macByHost = new Map()) {
228
+ let existing = {};
229
+ if (existingRaw) {
230
+ try {
231
+ existing = JSON.parse(existingRaw);
232
+ }
233
+ catch {
234
+ existing = {};
235
+ }
236
+ }
237
+ const current = Array.isArray(existing.cameras)
238
+ ? existing.cameras
239
+ : [];
240
+ const incomingRecorderIdentities = new Map();
241
+ const incomingRecorderHosts = new Set();
242
+ for (const cam of incoming.cameras) {
243
+ if (cam.sourceType !== 'recorder')
244
+ continue;
245
+ const identity = recorderIdentityFor(cam, credsByHost, probedByChannelKey, macByHost);
246
+ if (identity)
247
+ incomingRecorderIdentities.set(cam.channelKey, identity);
248
+ if (cam.host)
249
+ incomingRecorderHosts.add(cam.host);
250
+ }
251
+ const incomingRecorderIds = new Set([...incomingRecorderIdentities.values()].map((identity) => identity.id));
252
+ const incomingRecorderGroups = new Set([
253
+ ...[...incomingRecorderIds].map((id) => `id:${id}`),
254
+ ...[...incomingRecorderHosts].map((host) => `host:${host}`),
255
+ ]);
256
+ const preserved = current.filter((camera) => {
257
+ const group = recorderGroupKey(camera);
258
+ if (!group)
259
+ return true;
260
+ if (incomingRecorderGroups.has(group))
261
+ return false;
262
+ const host = cameraHost(camera);
263
+ if (host && incomingRecorderHosts.has(host))
264
+ return false;
265
+ // Finalize contains only this setup's selection. An unmatched group may be
266
+ // another recorder already attached to the same connector, so retain it.
267
+ return true;
268
+ });
269
+ const byScreen = new Map(preserved.map((c) => [String(c.screenId || ''), c]));
270
+ const hasRecorder = incoming.cameras.some((cam) => cam.sourceType === 'recorder');
271
+ const existingCloudSensing = existing.cloudSensing && typeof existing.cloudSensing === 'object'
272
+ ? existing.cloudSensing
273
+ : {};
274
+ const existingLocalSensing = existing.localSensing && typeof existing.localSensing === 'object'
275
+ ? existing.localSensing
276
+ : {};
277
+ for (const cam of incoming.cameras) {
278
+ // Reassemble the full rtsp:// URL from LOCAL creds + the server's
279
+ // credential-free recipe. Percent-encoding handled by buildRtspUrl (the
280
+ // `pass@123` class). A camera whose host has no local creds keeps an empty
281
+ // rtspUrl (creds-stay-on-prem manual fill, same as legacy pairing).
282
+ const creds = cam.host ? credsByHost.get(cam.host) : undefined;
283
+ const directUrl = directUrlsByChannelKey.get(cam.channelKey);
284
+ const recorder = incomingRecorderIdentities.get(cam.channelKey);
285
+ const rtspUrl = directUrl ||
286
+ (cam.host && cam.pathTemplate && creds
287
+ ? (0, ChannelProber_1.buildRtspUrl)(cam.host, cam.port ?? 554, (0, ChannelProber_1.materializeCredentialPathTemplate)(cam.pathTemplate, creds.username, creds.password), creds.username, creds.password)
288
+ : (byScreen.get(cam.screenId)?.rtspUrl || ''));
289
+ byScreen.set(cam.screenId, {
290
+ ...(byScreen.get(cam.screenId) || {}),
291
+ name: cam.name,
292
+ screenId: cam.screenId,
293
+ deviceToken: cam.deviceToken,
294
+ rtspUrl,
295
+ channelKey: cam.channelKey,
296
+ ...(cam.sourceType ? { sourceType: cam.sourceType } : {}),
297
+ ...(cam.transport ? { transport: cam.transport } : {}),
298
+ ...(recorder ? { recorder } : {}),
299
+ });
300
+ }
301
+ const mergedCameras = [...byScreen.values()];
302
+ return {
303
+ ...existing,
304
+ apiBaseUrl: incoming.apiBaseUrl || existing.apiBaseUrl || 'https://api.trillboards.com',
305
+ cloudSensing: {
306
+ ...existingCloudSensing,
307
+ enabled: false,
308
+ // Still-scene cadence, sized to the feed count AFTER this merge.
309
+ //
310
+ // It has to be computed here, on the post-merge list, because a finalize
311
+ // batch is not the connector: adding one camera to a box that already
312
+ // runs 20 would otherwise size for 1 feed and leave 21 feeds demanding
313
+ // ~2,520 still-scene analyses/hour against a budget of 493. Only this
314
+ // side ever sees the total. See connectorCadence.ts for the measurement.
315
+ //
316
+ // Precedence: an operator's explicit local value wins, then anything the
317
+ // server explicitly sent, then the derived default.
318
+ ...(typeof existingCloudSensing.periodicIntervalMs === 'number'
319
+ ? {}
320
+ : {
321
+ periodicIntervalMs: typeof incoming.cloudSensing.periodicIntervalMs === 'number'
322
+ ? incoming.cloudSensing.periodicIntervalMs
323
+ : (0, connectorCadence_1.periodicIntervalMsForFeedCount)(mergedCameras.length),
324
+ }),
325
+ ...(hasRecorder && typeof existingCloudSensing.feedStaggerMs !== 'number'
326
+ ? { feedStaggerMs: legacyRecorderProfile_1.LEGACY_RECORDER_RUNTIME_PROFILE.feedStaggerMs }
327
+ : {}),
328
+ ...(hasRecorder && typeof existingCloudSensing.maxFeedStaggerMs !== 'number'
329
+ ? { maxFeedStaggerMs: legacyRecorderProfile_1.LEGACY_RECORDER_RUNTIME_PROFILE.maxFeedStaggerMs }
330
+ : {}),
331
+ ...(hasRecorder && typeof existingCloudSensing.cameraHealthFailureGraceCount !== 'number'
332
+ ? {
333
+ cameraHealthFailureGraceCount: legacyRecorderProfile_1.LEGACY_RECORDER_RUNTIME_PROFILE.cameraHealthFailureGraceCount,
334
+ }
335
+ : {}),
336
+ },
337
+ localSensing: {
338
+ ...existingLocalSensing,
339
+ enabled: true,
340
+ // Preserve an operator's explicit opt-in, but never introduce cloud
341
+ // inference during ordinary setup. Fresh installs remain local-only.
342
+ allowCloudFallback: existingLocalSensing.allowCloudFallback === true,
343
+ },
344
+ cameras: mergedCameras,
345
+ };
346
+ }
347
+ /**
348
+ * Run the interactive setup session. Returns 0 on config-written, 2 on
349
+ * session-expired (restart for a fresh code), 1 on hard errors.
350
+ */
351
+ async function runSetupMode(opts) {
352
+ const log = opts.log ?? ((l) => console.log(l));
353
+ const progress = (state) => {
354
+ try {
355
+ opts.progress?.(state);
356
+ }
357
+ catch {
358
+ // A UI update is advisory. Camera setup must remain functional if the
359
+ // native window closes or its state file becomes unavailable.
360
+ }
361
+ };
362
+ const sleep = opts.sleep ?? defaultSleep;
363
+ const fingerprint = setupHostFingerprint();
364
+ const client = opts.client ?? new SetupSessionClient_1.SetupSessionClient({ apiBaseUrl: opts.apiBaseUrl, connectorFingerprint: fingerprint });
365
+ const discover = opts.discover ?? discovery_1.discoverOnvifHosts;
366
+ const sweep = opts.sweep ?? discovery_1.sweepLanForRecorders;
367
+ const probe = opts.probe ?? ChannelProber_1.probeHost;
368
+ progress({
369
+ stage: 'starting',
370
+ title: 'Starting camera setup',
371
+ message: 'Connecting securely to Trillboards.',
372
+ });
373
+ let started;
374
+ try {
375
+ started = await client.start({ connectorVersion: opts.connectorVersion, platform: process.platform });
376
+ }
377
+ catch (err) {
378
+ const msg = err.message || '';
379
+ if (/HTTP 404/.test(msg)) {
380
+ // Server predates the setup-session API (deploy sequencing) — tell the
381
+ // wrapper to fall back to the classic portal-code flow (exit 3).
382
+ log('This Trillboards server does not support the new setup flow yet.');
383
+ log('Use the classic option: generate a code in the portal and paste it here.');
384
+ progress({
385
+ stage: 'error',
386
+ title: 'Setup needs an update',
387
+ message: 'The camera setup service is not available yet. Try again shortly.',
388
+ recoverable: true,
389
+ });
390
+ return 3;
391
+ }
392
+ log(`Could not reach Trillboards to begin setup: ${msg}`);
393
+ log('Check this computer\'s internet connection and run setup again.');
394
+ progress({
395
+ stage: 'error',
396
+ title: 'Could not start setup',
397
+ message: 'Check this computer\'s internet connection, then try again.',
398
+ recoverable: true,
399
+ });
400
+ return 1;
401
+ }
402
+ let codeExpiresAtMs = Date.parse(started.codeExpiresAt) || Date.now() + 15 * 60 * 1000;
403
+ let displayCodeFormatted = started.displayCodeFormatted;
404
+ log('');
405
+ log('==================================================');
406
+ log(' Your Trillboards setup code:');
407
+ log('');
408
+ log(` ${started.displayCodeFormatted}`);
409
+ log('');
410
+ log(' In the Trillboards portal, open Screens →');
411
+ log(' Add cameras and type this code. This window');
412
+ log(' finishes the rest automatically.');
413
+ log('==================================================');
414
+ log('');
415
+ // Default path: take the owner straight back to the signed-in portal and let
416
+ // it claim this one-time code from the URL fragment. If the OS/browser blocks
417
+ // the launch, the displayed code above remains the complete fallback.
418
+ let portalUrl;
419
+ try {
420
+ portalUrl = buildSetupPortalUrl(started.displayCodeFormatted, opts.portalBaseUrl || DEFAULT_PORTAL_BASE_URL);
421
+ const opened = await (opts.openPortal ?? openSetupPortal)(portalUrl);
422
+ log(opened
423
+ ? 'Opened Trillboards in your browser — sign in if asked. Connecting automatically…'
424
+ : 'Open Trillboards → Screens → Add cameras, then type the code shown above.');
425
+ }
426
+ catch {
427
+ log('Open Trillboards → Screens → Add cameras, then type the code shown above.');
428
+ }
429
+ log('');
430
+ progress({
431
+ stage: 'waiting_for_account',
432
+ title: 'Connect your Trillboards account',
433
+ message: portalUrl
434
+ ? 'Continue in the Trillboards portal. This app will finish setup automatically.'
435
+ : 'In Trillboards, open Screens, add CCTV cameras, and enter this setup code.',
436
+ displayCode: started.displayCodeFormatted,
437
+ ...(portalUrl ? { portalUrl } : {}),
438
+ });
439
+ const pollIntervalMs = Math.max(1500, Math.min(5000, started.pollIntervalMs || 2500));
440
+ const deadline = Date.now() + (opts.maxWaitMs ?? 2 * 60 * 60 * 1000);
441
+ const credsByHost = new Map();
442
+ const directUrlsByChannelKey = new Map();
443
+ const probedByChannelKey = new Map();
444
+ const macByHost = new Map();
445
+ let claimedAnnounced = false;
446
+ let lastClaimIssueAtMs = 0;
447
+ let discoveryReported = false;
448
+ const validatedChannelKeys = new Set();
449
+ const rejectedChannelKeys = new Set();
450
+ const reportValidationProgress = () => {
451
+ progress({
452
+ stage: 'validating',
453
+ title: 'Validating camera feeds',
454
+ message: validatedChannelKeys.size > 0
455
+ ? `${validatedChannelKeys.size} visible camera feed(s) are ready.`
456
+ : 'Checking each channel for a visible image.',
457
+ validatedFeeds: validatedChannelKeys.size,
458
+ rejectedFeeds: rejectedChannelKeys.size,
459
+ });
460
+ };
461
+ const runDiscovery = async () => {
462
+ log('Scanning your network for cameras and recorders…');
463
+ progress({
464
+ stage: 'discovering',
465
+ title: 'Finding cameras and recorders',
466
+ message: 'Scanning this local network. This usually takes less than a minute.',
467
+ });
468
+ let onvifHosts = [];
469
+ try {
470
+ onvifHosts = await discover({ timeoutMs: 4000 });
471
+ }
472
+ catch {
473
+ onvifHosts = [];
474
+ }
475
+ let swept = [];
476
+ try {
477
+ swept = await sweep({});
478
+ }
479
+ catch {
480
+ swept = [];
481
+ }
482
+ // Merge: ONVIF hits are richer; sweep adds no-ONVIF recorders (as bare
483
+ // host entries the portal renders as "Recorder at <ip> — tap to connect").
484
+ const discoveredHosts = mergeDiscoveredHosts(onvifHosts, swept);
485
+ try {
486
+ await client.reportDiscovered({ onvifHosts: discoveredHosts, cameras: [] });
487
+ log(`Found ${discoveredHosts.length} device(s) — pick yours in the portal.`);
488
+ const devicesFound = discoveredHosts.length;
489
+ progress({
490
+ stage: 'devices_found',
491
+ title: devicesFound === 1 ? 'Recorder found' : `${devicesFound} devices found`,
492
+ message: devicesFound > 0
493
+ ? 'Choose your recorder in the Trillboards portal.'
494
+ : 'No recorder was found automatically. You can enter its address in the portal.',
495
+ devicesFound,
496
+ });
497
+ }
498
+ catch (err) {
499
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError)
500
+ throw err;
501
+ log(`(Could not report the scan: ${err.message})`);
502
+ }
503
+ };
504
+ const executeCommand = async (command) => {
505
+ if (command.type === 'discover') {
506
+ await client.heartbeat('discovering');
507
+ await runDiscovery();
508
+ await client.ack(command.commandId);
509
+ return;
510
+ }
511
+ if (command.type === 'probe' && command.host && command.credentials) {
512
+ credsByHost.set(command.host, command.credentials);
513
+ await client.heartbeat('probing', command.host);
514
+ log(`Checking cameras on ${command.host}…`);
515
+ reportValidationProgress();
516
+ let sentAny = false;
517
+ const results = await probe({
518
+ host: command.host,
519
+ port: command.port,
520
+ username: command.credentials.username,
521
+ password: command.credentials.password,
522
+ brandHint: command.brandHint,
523
+ channelHint: command.channelHint,
524
+ }, {
525
+ ffmpegPath: opts.ffmpegPath,
526
+ onResult: async (r) => {
527
+ sentAny = true;
528
+ probedByChannelKey.set(r.channelKey, r);
529
+ try {
530
+ await client.reportResults([r]);
531
+ }
532
+ catch (err) {
533
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError)
534
+ throw err;
535
+ /* transient report failure: the final batch below retries */
536
+ }
537
+ log(r.status === 'validated'
538
+ ? ` ✓ channel ${r.channel ?? '?'} — working`
539
+ : ` ✗ channel ${r.channel ?? '?'} — ${r.failureClass || r.status}`);
540
+ if (r.status === 'validated') {
541
+ validatedChannelKeys.add(r.channelKey);
542
+ rejectedChannelKeys.delete(r.channelKey);
543
+ }
544
+ else {
545
+ rejectedChannelKeys.add(r.channelKey);
546
+ validatedChannelKeys.delete(r.channelKey);
547
+ }
548
+ reportValidationProgress();
549
+ },
550
+ });
551
+ try {
552
+ const mac = await (opts.lookupMac ?? RecorderAddressRecovery_1.lookupMacAddress)(command.host);
553
+ if (mac)
554
+ macByHost.set(command.host, mac);
555
+ }
556
+ catch {
557
+ // ONVIF serial remains preferred; a missing neighbor-table entry only
558
+ // disables automatic DHCP recovery for this legacy recorder.
559
+ }
560
+ // Final (possibly duplicate — server merges by channelKey) batch + ack.
561
+ try {
562
+ if (results.length > 0)
563
+ await client.reportResults(results, command.commandId);
564
+ else
565
+ await client.ack(command.commandId);
566
+ }
567
+ catch (err) {
568
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError)
569
+ throw err;
570
+ if (!sentAny)
571
+ log(`(Could not report results: ${err.message})`);
572
+ await client.ack(command.commandId).catch(() => { });
573
+ }
574
+ // Incremental channel results are progress, not completion. Keep a
575
+ // separate terminal marker so the portal cannot enable Finalize while a
576
+ // later recorder channel is still being validated.
577
+ await client.heartbeat('probe_complete', command.commandId).catch(() => { });
578
+ return;
579
+ }
580
+ if (command.type === 'probe_url' && command.streamUrl) {
581
+ const streamUrl = normalizeDirectStreamUrl(command.streamUrl);
582
+ const key = (0, ChannelProber_1.directStreamChannelKey)(streamUrl);
583
+ const info = directStreamInfo(streamUrl, command.name);
584
+ directUrlsByChannelKey.set(key, streamUrl);
585
+ await client.heartbeat('probing', 'direct stream link');
586
+ log(`Checking direct camera stream at ${(0, ChannelProber_1.sanitizeRtspUrlForLog)(streamUrl)}…`);
587
+ reportValidationProgress();
588
+ const grab = await (0, ChannelProber_1.grabOneFrameAnyTransport)(streamUrl, {
589
+ ffmpegPath: opts.ffmpegPath,
590
+ timeoutMs: 8000,
591
+ });
592
+ const result = grab.ok
593
+ ? {
594
+ channelKey: key,
595
+ status: 'validated',
596
+ sourceType: 'direct_url',
597
+ name: info.name,
598
+ brand: info.brand,
599
+ host: info.host,
600
+ port: info.port,
601
+ transport: grab.transport,
602
+ thumbnailBase64: grab.jpegBase64,
603
+ }
604
+ : {
605
+ channelKey: key,
606
+ status: 'failed',
607
+ sourceType: 'direct_url',
608
+ failureClass: grab.failureClass || 'no_frames',
609
+ message: grab.message ? (0, ChannelProber_1.sanitizeRtspUrlForLog)(grab.message) : 'The stream did not return a frame.',
610
+ name: info.name,
611
+ brand: info.brand,
612
+ host: info.host,
613
+ port: info.port,
614
+ };
615
+ try {
616
+ await client.reportResults([result], command.commandId);
617
+ }
618
+ catch (err) {
619
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError)
620
+ throw err;
621
+ log(`(Could not report direct stream result: ${err.message})`);
622
+ await client.ack(command.commandId).catch(() => { });
623
+ }
624
+ await client.heartbeat('probe_complete', command.commandId).catch(() => { });
625
+ log(result.status === 'validated'
626
+ ? ' ✓ direct camera stream — working'
627
+ : ` ✗ direct camera stream — ${result.failureClass || result.status}`);
628
+ if (result.status === 'validated') {
629
+ validatedChannelKeys.add(result.channelKey);
630
+ rejectedChannelKeys.delete(result.channelKey);
631
+ }
632
+ else {
633
+ rejectedChannelKeys.add(result.channelKey);
634
+ validatedChannelKeys.delete(result.channelKey);
635
+ }
636
+ reportValidationProgress();
637
+ return;
638
+ }
639
+ // Unknown/malformed command — ack it away so the queue can't wedge.
640
+ await client.ack(command.commandId).catch(() => { });
641
+ };
642
+ while (Date.now() < deadline) {
643
+ let polled;
644
+ try {
645
+ polled = await client.poll();
646
+ }
647
+ catch (err) {
648
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError) {
649
+ log('This setup session expired. Run setup again for a fresh code.');
650
+ progress({
651
+ stage: 'error',
652
+ title: 'Setup session expired',
653
+ message: 'Start setup again for a fresh code. No cameras were created.',
654
+ recoverable: true,
655
+ });
656
+ return 2;
657
+ }
658
+ await sleep(pollIntervalMs * 2); // transient network blip — back off once
659
+ continue;
660
+ }
661
+ if (!polled.claimed) {
662
+ // Refresh the display code shortly before it expires so the owner never
663
+ // reads a dead code off this window.
664
+ if (Date.now() > codeExpiresAtMs - 60000) {
665
+ try {
666
+ const fresh = await client.regenerateCode();
667
+ codeExpiresAtMs = Date.parse(fresh.codeExpiresAt) || Date.now() + 15 * 60 * 1000;
668
+ displayCodeFormatted = fresh.displayCodeFormatted;
669
+ log(`Code refreshed — new code: ${fresh.displayCodeFormatted}`);
670
+ portalUrl = buildSetupPortalUrl(fresh.displayCodeFormatted, opts.portalBaseUrl || DEFAULT_PORTAL_BASE_URL);
671
+ progress({
672
+ stage: polled.claimIssue ? 'account_attention' : 'waiting_for_account',
673
+ title: polled.claimIssue
674
+ ? 'Use an account that can add cameras'
675
+ : 'Connect your Trillboards account',
676
+ message: polled.claimIssue
677
+ ? polled.claimIssue.message
678
+ : 'The setup code was refreshed. Continue in the Trillboards portal.',
679
+ displayCode: fresh.displayCodeFormatted,
680
+ portalUrl,
681
+ ...(polled.claimIssue ? { recoverable: true } : {}),
682
+ });
683
+ }
684
+ catch (err) {
685
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError) {
686
+ log('This setup session expired. Run setup again for a fresh code.');
687
+ progress({
688
+ stage: 'error',
689
+ title: 'Setup session expired',
690
+ message: 'Start setup again for a fresh code. No cameras were created.',
691
+ recoverable: true,
692
+ });
693
+ return 2;
694
+ }
695
+ }
696
+ }
697
+ if (polled.claimIssue && polled.claimIssue.atMs > lastClaimIssueAtMs) {
698
+ lastClaimIssueAtMs = polled.claimIssue.atMs;
699
+ log('The signed-in account cannot add cameras. Switch to an account owner or screen editor.');
700
+ progress({
701
+ stage: 'account_attention',
702
+ title: 'Use an account that can add cameras',
703
+ message: polled.claimIssue.message,
704
+ displayCode: displayCodeFormatted,
705
+ ...(portalUrl ? { portalUrl } : {}),
706
+ recoverable: true,
707
+ });
708
+ }
709
+ await sleep(pollIntervalMs);
710
+ continue;
711
+ }
712
+ if (!claimedAnnounced) {
713
+ claimedAnnounced = true;
714
+ log('Connected to your Trillboards account ✓');
715
+ progress({
716
+ stage: 'account_connected',
717
+ title: 'Account connected',
718
+ message: 'This computer is securely paired with your Trillboards account.',
719
+ });
720
+ await client.heartbeat('service_verified').catch(() => { });
721
+ }
722
+ if (!discoveryReported) {
723
+ discoveryReported = true;
724
+ try {
725
+ await runDiscovery();
726
+ }
727
+ catch (err) {
728
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError) {
729
+ log('This setup session expired. Run setup again for a fresh code.');
730
+ progress({
731
+ stage: 'error',
732
+ title: 'Setup session expired',
733
+ message: 'Start setup again for a fresh code. No cameras were created.',
734
+ recoverable: true,
735
+ });
736
+ return 2;
737
+ }
738
+ }
739
+ }
740
+ if (polled.config && polled.config.cameras && polled.config.cameras.length > 0) {
741
+ // Finalized: write the merged config with locally-reassembled RTSP URLs.
742
+ progress({
743
+ stage: 'cameras_created',
744
+ title: 'Creating camera screens',
745
+ message: 'Saving the validated camera configuration on this computer.',
746
+ cameraCount: polled.config.cameras.length,
747
+ validatedFeeds: validatedChannelKeys.size || polled.config.cameras.length,
748
+ rejectedFeeds: rejectedChannelKeys.size,
749
+ });
750
+ const resolved = path.resolve(opts.configPath);
751
+ let existingRaw = null;
752
+ try {
753
+ existingRaw = fs.readFileSync(resolved, 'utf8');
754
+ }
755
+ catch {
756
+ existingRaw = null;
757
+ }
758
+ const merged = mergeConfigFile(existingRaw, polled.config, credsByHost, directUrlsByChannelKey, probedByChannelKey, macByHost);
759
+ try {
760
+ fs.writeFileSync(resolved, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
761
+ // mode: only applies when CREATING the file — an existing 0644 config
762
+ // (trillboards-edge init / hand-made) must be tightened too: it now
763
+ // holds camera credentials.
764
+ try {
765
+ fs.chmodSync(resolved, 0o600);
766
+ }
767
+ catch {
768
+ /* best-effort (Windows ACLs) — the write above succeeded */
769
+ }
770
+ }
771
+ catch (err) {
772
+ log(`Could not save the camera configuration: ${err.message}`);
773
+ log(`Fix the file permissions on ${resolved} and run setup again — your cameras are already created.`);
774
+ progress({
775
+ stage: 'error',
776
+ title: 'Could not save camera settings',
777
+ message: 'Your camera screens were created, but this computer could not save their settings. Contact Trillboards support.',
778
+ cameraCount: polled.config.cameras.length,
779
+ recoverable: true,
780
+ });
781
+ return 1;
782
+ }
783
+ await client.heartbeat('config_applied').catch(() => { });
784
+ log('');
785
+ log(`✓ ${polled.config.cameras.length} camera(s) configured — starting sensing.`);
786
+ log('You can close this window. The portal shows each camera as it comes online.');
787
+ progress({
788
+ stage: 'cameras_created',
789
+ title: `${polled.config.cameras.length} camera${polled.config.cameras.length === 1 ? '' : 's'} connected`,
790
+ message: 'The camera screens are created. Starting the private local AI installed with Trillboards.',
791
+ cameraCount: polled.config.cameras.length,
792
+ validatedFeeds: validatedChannelKeys.size || polled.config.cameras.length,
793
+ rejectedFeeds: rejectedChannelKeys.size,
794
+ });
795
+ return 0;
796
+ }
797
+ if (polled.command) {
798
+ try {
799
+ await executeCommand(polled.command);
800
+ }
801
+ catch (err) {
802
+ if (err instanceof SetupSessionClient_1.SetupSessionExpiredError) {
803
+ log('This setup session expired. Run setup again for a fresh code.');
804
+ progress({
805
+ stage: 'error',
806
+ title: 'Setup session expired',
807
+ message: 'Start setup again for a fresh code. No cameras were created.',
808
+ recoverable: true,
809
+ });
810
+ return 2;
811
+ }
812
+ log(`Command failed: ${err.message}`);
813
+ await client.ack(polled.command.commandId).catch(() => { });
814
+ }
815
+ continue; // poll immediately — more commands may be queued
816
+ }
817
+ await sleep(pollIntervalMs);
818
+ }
819
+ log('Setup timed out waiting for the portal. Run setup again when you are ready.');
820
+ progress({
821
+ stage: 'error',
822
+ title: 'Setup timed out',
823
+ message: 'Start setup again when you are ready. No cameras were created.',
824
+ recoverable: true,
825
+ });
826
+ return 2;
827
+ }
828
+ //# sourceMappingURL=setupMode.js.map