@trillboards/edge-sdk 0.2.3 → 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.
- package/README.md +4 -3
- package/deploy/docker/Dockerfile.cloud-sense +117 -0
- package/deploy/docker/Dockerfile.cpu +30 -21
- package/deploy/docker/Dockerfile.cuda +30 -21
- package/deploy/docker/Dockerfile.openvino +30 -20
- package/deploy/docker/README.md +9 -4
- package/deploy/docker/connector-entrypoint.sh +122 -0
- package/deploy/helm/README.md +1 -1
- package/deploy/k8s/daemonset.yaml +1 -1
- package/deploy/macos/README.md +161 -0
- package/deploy/macos/TrillboardsSetupApp.m +603 -0
- package/deploy/macos/build-macos-pkg.sh +588 -0
- package/deploy/macos/com.trillboards.edge.plist +62 -0
- package/deploy/macos/com.trillboards.litertlm.plist +45 -0
- package/deploy/macos/pair.command +519 -0
- package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
- package/deploy/macos/scripts/postinstall +153 -0
- package/deploy/macos/scripts/setup-litertlm.sh +272 -0
- package/deploy/macos/test-install-pair.sh +383 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
- package/deploy/systemd/install.sh +25 -15
- package/deploy/systemd/trillboards-edge.service +4 -1
- package/deploy/windows/README.md +70 -0
- package/deploy/windows/build-windows.ps1 +225 -0
- package/deploy/windows/bundle-windows-installer.ps1 +153 -0
- package/deploy/windows/check-feeds.js +26 -0
- package/deploy/windows/e2e/fixtures/person.jpg +0 -0
- package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
- package/deploy/windows/install-update.ps1 +101 -0
- package/deploy/windows/installer.nsi +138 -0
- package/deploy/windows/register-litertlm-task.ps1 +42 -0
- package/deploy/windows/register-task.ps1 +44 -0
- package/deploy/windows/run-connector.ps1 +69 -0
- package/deploy/windows/setup-litertlm.ps1 +361 -0
- package/deploy/windows/stop-existing.ps1 +72 -0
- package/deploy/windows/trillboards-edge-task.vbs +25 -0
- package/deploy/windows/trillboards-edge.cmd +33 -0
- package/deploy/windows/trillboards-pair.cmd +276 -0
- package/deploy/windows/trillboards-setup.ps1 +456 -0
- package/deploy/windows/trillboards-setup.vbs +13 -0
- package/dist/AudienceAnalyzeClient.d.ts +136 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
- package/dist/AudienceAnalyzeClient.js +246 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -0
- package/dist/ChannelProber.d.ts +150 -0
- package/dist/ChannelProber.d.ts.map +1 -0
- package/dist/ChannelProber.js +887 -0
- package/dist/ChannelProber.js.map +1 -0
- package/dist/CloudEscalationPolicy.d.ts +143 -0
- package/dist/CloudEscalationPolicy.d.ts.map +1 -0
- package/dist/CloudEscalationPolicy.js +137 -0
- package/dist/CloudEscalationPolicy.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +533 -0
- package/dist/CloudSensingManager.d.ts.map +1 -0
- package/dist/CloudSensingManager.js +1254 -0
- package/dist/CloudSensingManager.js.map +1 -0
- package/dist/DiscoveryReportClient.d.ts +66 -0
- package/dist/DiscoveryReportClient.d.ts.map +1 -0
- package/dist/DiscoveryReportClient.js +85 -0
- package/dist/DiscoveryReportClient.js.map +1 -0
- package/dist/EdgeAgent.d.ts +12 -2
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +122 -65
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +193 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -0
- package/dist/LiteRtVlmProvider.js +436 -0
- package/dist/LiteRtVlmProvider.js.map +1 -0
- package/dist/MosaicSource.d.ts +108 -0
- package/dist/MosaicSource.d.ts.map +1 -0
- package/dist/MosaicSource.js +274 -0
- package/dist/MosaicSource.js.map +1 -0
- package/dist/OnvifClient.d.ts +83 -0
- package/dist/OnvifClient.d.ts.map +1 -0
- package/dist/OnvifClient.js +209 -0
- package/dist/OnvifClient.js.map +1 -0
- package/dist/PairClient.d.ts +106 -0
- package/dist/PairClient.d.ts.map +1 -0
- package/dist/PairClient.js +186 -0
- package/dist/PairClient.js.map +1 -0
- package/dist/RecorderAddressRecovery.d.ts +53 -0
- package/dist/RecorderAddressRecovery.d.ts.map +1 -0
- package/dist/RecorderAddressRecovery.js +359 -0
- package/dist/RecorderAddressRecovery.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +291 -0
- package/dist/SceneChangeDetector.d.ts.map +1 -0
- package/dist/SceneChangeDetector.js +682 -0
- package/dist/SceneChangeDetector.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +59 -0
- package/dist/SensingProfileClient.d.ts.map +1 -0
- package/dist/SensingProfileClient.js +78 -0
- package/dist/SensingProfileClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +114 -0
- package/dist/SetupSessionClient.d.ts.map +1 -0
- package/dist/SetupSessionClient.js +95 -0
- package/dist/SetupSessionClient.js.map +1 -0
- package/dist/VlmAdmissionGate.d.ts +126 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -0
- package/dist/VlmAdmissionGate.js +166 -0
- package/dist/VlmAdmissionGate.js.map +1 -0
- package/dist/autoSense.d.ts +216 -0
- package/dist/autoSense.d.ts.map +1 -0
- package/dist/autoSense.js +521 -0
- package/dist/autoSense.js.map +1 -0
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +511 -21
- package/dist/cli.js.map +1 -1
- package/dist/combinedSense.d.ts +128 -0
- package/dist/combinedSense.d.ts.map +1 -0
- package/dist/combinedSense.js +261 -0
- package/dist/combinedSense.js.map +1 -0
- package/dist/config.d.ts +351 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +99 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorCadence.d.ts +135 -0
- package/dist/connectorCadence.d.ts.map +1 -0
- package/dist/connectorCadence.js +151 -0
- package/dist/connectorCadence.js.map +1 -0
- package/dist/dataDir.d.ts +33 -0
- package/dist/dataDir.d.ts.map +1 -0
- package/dist/dataDir.js +76 -0
- package/dist/dataDir.js.map +1 -0
- package/dist/deployedCadence.d.ts +167 -0
- package/dist/deployedCadence.d.ts.map +1 -0
- package/dist/deployedCadence.js +209 -0
- package/dist/deployedCadence.js.map +1 -0
- package/dist/deployedCadenceSource.d.ts +48 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -0
- package/dist/deployedCadenceSource.js +74 -0
- package/dist/deployedCadenceSource.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +13 -12
- package/dist/diagnose.js.map +1 -1
- package/dist/discovery.d.ts +131 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +499 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gemmaModelOta.d.ts +118 -0
- package/dist/gemmaModelOta.d.ts.map +1 -0
- package/dist/gemmaModelOta.js +291 -0
- package/dist/gemmaModelOta.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +41 -18
- package/dist/init.js.map +1 -1
- package/dist/keepAwake.d.ts +33 -0
- package/dist/keepAwake.d.ts.map +1 -0
- package/dist/keepAwake.js +54 -0
- package/dist/keepAwake.js.map +1 -0
- package/dist/legacyRecorderProfile.d.ts +20 -0
- package/dist/legacyRecorderProfile.d.ts.map +1 -0
- package/dist/legacyRecorderProfile.js +27 -0
- package/dist/legacyRecorderProfile.js.map +1 -0
- package/dist/localInferenceSchema.d.ts +132 -0
- package/dist/localInferenceSchema.d.ts.map +1 -0
- package/dist/localInferenceSchema.js +274 -0
- package/dist/localInferenceSchema.js.map +1 -0
- package/dist/localSenseE2eHarness.d.ts +101 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -0
- package/dist/localSenseE2eHarness.js +279 -0
- package/dist/localSenseE2eHarness.js.map +1 -0
- package/dist/localSensing.d.ts +268 -0
- package/dist/localSensing.d.ts.map +1 -0
- package/dist/localSensing.js +492 -0
- package/dist/localSensing.js.map +1 -0
- package/dist/modelIntegrity.d.ts +59 -0
- package/dist/modelIntegrity.d.ts.map +1 -0
- package/dist/modelIntegrity.js +165 -0
- package/dist/modelIntegrity.js.map +1 -0
- package/dist/pairVerify.d.ts +100 -0
- package/dist/pairVerify.d.ts.map +1 -0
- package/dist/pairVerify.js +296 -0
- package/dist/pairVerify.js.map +1 -0
- package/dist/qrScanner.d.ts +33 -0
- package/dist/qrScanner.d.ts.map +1 -0
- package/dist/qrScanner.js +103 -0
- package/dist/qrScanner.js.map +1 -0
- package/dist/runtimePaths.d.ts +10 -0
- package/dist/runtimePaths.d.ts.map +1 -0
- package/dist/runtimePaths.js +79 -0
- package/dist/runtimePaths.js.map +1 -0
- package/dist/sensingRunMode.d.ts +68 -0
- package/dist/sensingRunMode.d.ts.map +1 -0
- package/dist/sensingRunMode.js +346 -0
- package/dist/sensingRunMode.js.map +1 -0
- package/dist/setupMode.d.ts +98 -0
- package/dist/setupMode.d.ts.map +1 -0
- package/dist/setupMode.js +828 -0
- package/dist/setupMode.js.map +1 -0
- package/dist/setupProgress.d.ts +31 -0
- package/dist/setupProgress.d.ts.map +1 -0
- package/dist/setupProgress.js +173 -0
- package/dist/setupProgress.js.map +1 -0
- package/package.json +13 -4
package/dist/cli.js
CHANGED
|
@@ -34,35 +34,166 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.isPermissionError = isPermissionError;
|
|
38
|
+
exports.configWritePermissionMessage = configWritePermissionMessage;
|
|
39
|
+
exports.writePairedConfig = writePairedConfig;
|
|
37
40
|
const config_1 = require("./config");
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
41
|
+
const autoSense_1 = require("./autoSense");
|
|
42
|
+
const sensingRunMode_1 = require("./sensingRunMode");
|
|
43
|
+
const gemmaModelOta_1 = require("./gemmaModelOta");
|
|
44
|
+
const keepAwake_1 = require("./keepAwake");
|
|
45
|
+
const runtimePaths_1 = require("./runtimePaths");
|
|
46
|
+
const modelIntegrity_1 = require("./modelIntegrity");
|
|
47
|
+
const PairClient_1 = require("./PairClient");
|
|
48
|
+
const pairVerify_1 = require("./pairVerify");
|
|
43
49
|
const path = __importStar(require("path"));
|
|
44
50
|
const fs = __importStar(require("fs"));
|
|
45
51
|
const https = __importStar(require("https"));
|
|
46
|
-
const
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const crypto_1 = require("crypto");
|
|
54
|
+
// NOTE: the heavyweight command handlers (`start` → EdgeAgent → edge-sensing →
|
|
55
|
+
// onnxruntime-node, plus init/status/diagnose/demo) are loaded LAZILY inside
|
|
56
|
+
// their switch arms rather than as top-level imports. This keeps the
|
|
57
|
+
// cloud-sensing + pair entrypoints (the macOS/edge "cloud-sense" connector
|
|
58
|
+
// installer surface) free of the 200 MB+ ONNX runtime: a bundled-runtime build
|
|
59
|
+
// that ships ONLY edge-core + edge-platform-rtsp + edge-sdk can run
|
|
60
|
+
// `cloud-sense`, `pair`, `version`, and `help` without the local-inference
|
|
61
|
+
// stack ever being required. `mcp` was already lazy-required for the same
|
|
62
|
+
// reason.
|
|
63
|
+
// Source the connector version from package.json (the single source of truth)
|
|
64
|
+
// so `version`/`--version`/help and the pairing log can never drift from the
|
|
65
|
+
// published package. dist/cli.js lives in dist/, so ../package.json resolves to
|
|
66
|
+
// the edge-sdk package root both in-repo and in the shipped .pkg (where
|
|
67
|
+
// build-macos-pkg.sh stages package.json next to dist/) — and npm always ships
|
|
68
|
+
// package.json. Required at runtime (commonjs), like the lazy command handlers.
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
70
|
+
const VERSION = require('../package.json').version;
|
|
47
71
|
async function main() {
|
|
48
72
|
const args = process.argv.slice(2);
|
|
49
73
|
const command = args[0];
|
|
50
74
|
switch (command) {
|
|
51
|
-
case 'demo':
|
|
52
|
-
|
|
75
|
+
case 'demo': {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
77
|
+
const { runDemo } = require('./demo');
|
|
78
|
+
await runDemo();
|
|
53
79
|
break;
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
}
|
|
81
|
+
case 'init': {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
83
|
+
const { runInit } = require('./init');
|
|
84
|
+
await runInit();
|
|
56
85
|
break;
|
|
86
|
+
}
|
|
57
87
|
case 'start':
|
|
58
88
|
await startAgent(args.slice(1));
|
|
59
89
|
break;
|
|
60
|
-
case '
|
|
61
|
-
|
|
90
|
+
case 'cloud-sense':
|
|
91
|
+
(0, keepAwake_1.startKeepAwake)();
|
|
92
|
+
await startCloudSensing(args.slice(1));
|
|
93
|
+
break;
|
|
94
|
+
case 'local-sense':
|
|
95
|
+
(0, keepAwake_1.startKeepAwake)();
|
|
96
|
+
await startLocalSensing(args.slice(1));
|
|
97
|
+
break;
|
|
98
|
+
case 'auto-sense':
|
|
99
|
+
// Prefer on-device local sensing and wait for LiteRT-LM when it is still
|
|
100
|
+
// provisioning. Cloud inference is available only through explicit config;
|
|
101
|
+
// the supervisor starts local automatically with NO restart once ready.
|
|
102
|
+
// The supervisor owns the lifecycle + builds the SAME managers cloud-sense /
|
|
103
|
+
// local-sense use (see autoSense.ts + sensingRunMode.ts).
|
|
104
|
+
(0, keepAwake_1.startKeepAwake)();
|
|
105
|
+
await (0, autoSense_1.runAutoSense)(args.slice(1));
|
|
106
|
+
break;
|
|
107
|
+
case 'sense':
|
|
108
|
+
case 'combined-sense': {
|
|
109
|
+
(0, keepAwake_1.startKeepAwake)();
|
|
110
|
+
// APK parity: run the on-device ONNX ZOO (EdgeAgent) AND the VLM sensing
|
|
111
|
+
// manager (auto-detected local/cloud, like auto-sense) CONCURRENTLY in ONE
|
|
112
|
+
// process. Lazy-require like `start` so the cloud-sense/pair bundle stays
|
|
113
|
+
// free of the ONNX runtime (combinedSense → EdgeAgent → onnxruntime-node).
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
115
|
+
const { runCombinedSense } = require('./combinedSense');
|
|
116
|
+
await runCombinedSense(args.slice(1));
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'pair':
|
|
120
|
+
await runPair(args.slice(1));
|
|
121
|
+
break;
|
|
122
|
+
case 'setup': {
|
|
123
|
+
// Validate-first onboarding (direction-inverted pairing): WE display a
|
|
124
|
+
// short code, the owner claims it in the portal, the portal drives
|
|
125
|
+
// discovery/validation through our setup session, and the finalized
|
|
126
|
+
// config lands here — no screens exist until streams are PROVEN. Exit
|
|
127
|
+
// codes: 0 config written (wrapper starts the service), 2 session
|
|
128
|
+
// expired/timeout (re-run for a fresh code), 1 hard error.
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
130
|
+
const { runSetupMode } = require('./setupMode');
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
132
|
+
const { createSetupProgressReporter } = require('./setupProgress');
|
|
133
|
+
const setupArgs = args.slice(1);
|
|
134
|
+
const statusFile = getArg(setupArgs, '--status-file');
|
|
135
|
+
const rc = await runSetupMode({
|
|
136
|
+
apiBaseUrl: getArg(setupArgs, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com',
|
|
137
|
+
portalBaseUrl: getArg(setupArgs, '--portal-base') || process.env.TRILLBOARDS_PORTAL_BASE || 'https://trillboards.com',
|
|
138
|
+
configPath: getArg(setupArgs, '--config') || 'trillboards.config.json',
|
|
139
|
+
connectorVersion: VERSION,
|
|
140
|
+
ffmpegPath: (0, runtimePaths_1.resolveFfmpegBinaryPath)(getArg(setupArgs, '--ffmpeg')),
|
|
141
|
+
...(statusFile ? { progress: createSetupProgressReporter(statusFile) } : {}),
|
|
142
|
+
});
|
|
143
|
+
process.exit(rc);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'setup-status': {
|
|
147
|
+
// Internal platform-wrapper command. It advances the same credential-free
|
|
148
|
+
// progress file after setup mode hands off to model/service bootstrap.
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
150
|
+
const { SETUP_PROGRESS_STAGES, writeSetupProgressFile } = require('./setupProgress');
|
|
151
|
+
const statusArgs = args.slice(1);
|
|
152
|
+
const statusFile = getArg(statusArgs, '--status-file');
|
|
153
|
+
const stage = getArg(statusArgs, '--stage');
|
|
154
|
+
if (!statusFile || !stage || !SETUP_PROGRESS_STAGES.includes(stage)) {
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
const cameraCountRaw = getArg(statusArgs, '--camera-count');
|
|
158
|
+
writeSetupProgressFile(statusFile, {
|
|
159
|
+
stage,
|
|
160
|
+
title: getArg(statusArgs, '--title') || 'Camera setup',
|
|
161
|
+
message: getArg(statusArgs, '--message') || '',
|
|
162
|
+
...(cameraCountRaw ? { cameraCount: Number(cameraCountRaw) } : {}),
|
|
163
|
+
...(stage === 'error' || stage === 'local_ai_error' ? { recoverable: true } : {}),
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'verify-connection': {
|
|
168
|
+
// One authenticated round-trip proving the SAVED pairing works from this
|
|
169
|
+
// machine (network + token). The install wrappers (trillboards-pair.cmd /
|
|
170
|
+
// pair.command) branch on its exit codes — see pairVerify.ts for the
|
|
171
|
+
// contract. Lazy-require to keep the pair hot path lean.
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
173
|
+
const { runVerifyConnection } = require('./pairVerify');
|
|
174
|
+
await runVerifyConnection(args.slice(1));
|
|
62
175
|
break;
|
|
63
|
-
|
|
64
|
-
|
|
176
|
+
}
|
|
177
|
+
case 'discover': {
|
|
178
|
+
// Lazy-require: discovery (UDP WS-Discovery + NVR channel-range expansion)
|
|
179
|
+
// is a small, optional helper — keep it off the cloud-sense/pair hot path.
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
181
|
+
const { runDiscover } = require('./discovery');
|
|
182
|
+
await runDiscover(args.slice(1));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'status': {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
187
|
+
const { runStatus } = require('./status');
|
|
188
|
+
await runStatus(args.slice(1));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'diagnose': {
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
193
|
+
const { runDiagnose } = require('./diagnose');
|
|
194
|
+
await runDiagnose(args.slice(1));
|
|
65
195
|
break;
|
|
196
|
+
}
|
|
66
197
|
case 'mcp':
|
|
67
198
|
await startMcpServer();
|
|
68
199
|
break;
|
|
@@ -95,7 +226,13 @@ async function startAgent(args) {
|
|
|
95
226
|
console.log(`Trillboards Edge AI SDK v${VERSION}`);
|
|
96
227
|
console.log(`Loading config: ${configPath}`);
|
|
97
228
|
const config = (0, config_1.loadConfig)(configPath);
|
|
98
|
-
|
|
229
|
+
// Lazy-require so the cloud-sense/pair connector bundle (which omits the
|
|
230
|
+
// local ONNX inference stack) never pays for EdgeAgent → edge-sensing →
|
|
231
|
+
// onnxruntime-node at module-load. `start` is the full-agent (local
|
|
232
|
+
// inference) entrypoint and needs it; cloud-sense does not.
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
234
|
+
const { EdgeAgent } = require('./EdgeAgent');
|
|
235
|
+
const agent = new EdgeAgent(config);
|
|
99
236
|
agent.on('started', ({ fingerprint, tier }) => {
|
|
100
237
|
console.log(`\n Device: ${fingerprint}`);
|
|
101
238
|
console.log(` Tier: TIER_${tier}`);
|
|
@@ -111,6 +248,314 @@ async function startAgent(args) {
|
|
|
111
248
|
// Keep the process running
|
|
112
249
|
await new Promise(() => { });
|
|
113
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Fire a best-effort ONVIF LAN scan + report on connector startup so the portal's
|
|
253
|
+
* "Find my cameras" can surface the venue's real cameras. Non-blocking, never
|
|
254
|
+
* throws — returns a promise the caller can settle before a no-feeds exit. Uses
|
|
255
|
+
* any feed's device token (all share the owner). Lazy-requires the reporter so the
|
|
256
|
+
* light entrypoints don't pay for it unless cloud-sensing actually runs.
|
|
257
|
+
*/
|
|
258
|
+
function kickStartupDiscovery(config) {
|
|
259
|
+
try {
|
|
260
|
+
const deviceToken = (config.cameras || []).find((c) => c && c.deviceToken)?.deviceToken || config.deviceToken;
|
|
261
|
+
if (!config.apiBaseUrl || !deviceToken)
|
|
262
|
+
return Promise.resolve();
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
264
|
+
const { discoverAndReport } = require('./DiscoveryReportClient');
|
|
265
|
+
return discoverAndReport({
|
|
266
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
267
|
+
deviceToken,
|
|
268
|
+
log: (m) => console.log(m),
|
|
269
|
+
}).then(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return Promise.resolve();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Thin cloud-sensing run mode: RTSP frame → POST /v2/earner/audience-analyze.
|
|
277
|
+
* No local ONNX. Supports multi-channel via `cameras: [...]` (N loops, one
|
|
278
|
+
* token + one profile per channel).
|
|
279
|
+
*/
|
|
280
|
+
async function startCloudSensing(args) {
|
|
281
|
+
const configIdx = args.indexOf('--config');
|
|
282
|
+
const configPath = configIdx >= 0 && args[configIdx + 1] ? args[configIdx + 1] : 'trillboards.config.json';
|
|
283
|
+
console.log(`Trillboards Edge AI SDK v${VERSION} — cloud-sensing mode`);
|
|
284
|
+
console.log(`Loading config: ${configPath}`);
|
|
285
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
286
|
+
// Kick a best-effort ONVIF LAN scan on startup so the portal's "Find my
|
|
287
|
+
// cameras" surfaces the venue's real cameras — CRUCIALLY even when feeds were
|
|
288
|
+
// paired WITHOUT an rtspUrl (creds-local: the owner didn't know the DVR's IP),
|
|
289
|
+
// which yields zero sensable feeds below. Fire-and-forget; we only settle it
|
|
290
|
+
// before a no-feeds exit so the scan actually gets to POST.
|
|
291
|
+
const discoveryDone = kickStartupDiscovery(config);
|
|
292
|
+
// Build the manager (feed resolution + banner + event logging) via the shared
|
|
293
|
+
// helper — the SAME construction auto-sense uses. Null ⇒ no feeds configured.
|
|
294
|
+
const manager = (0, sensingRunMode_1.buildCloudSensingManager)(config, undefined, { configPath });
|
|
295
|
+
if (!manager) {
|
|
296
|
+
// No sensable feeds (e.g. all feeds are creds-local, no rtspUrl). Let the
|
|
297
|
+
// startup discovery finish + report so the owner still sees their cameras in
|
|
298
|
+
// the portal, then exit.
|
|
299
|
+
await discoveryDone;
|
|
300
|
+
process.exit(1);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const shutdown = async () => {
|
|
304
|
+
console.log('\nStopping cloud-sensing…');
|
|
305
|
+
await manager.stop();
|
|
306
|
+
process.exit(0);
|
|
307
|
+
};
|
|
308
|
+
process.on('SIGINT', shutdown);
|
|
309
|
+
process.on('SIGTERM', shutdown);
|
|
310
|
+
await manager.start();
|
|
311
|
+
console.log('Cloud-sensing started. Press Ctrl+C to stop.');
|
|
312
|
+
// Keep the process running
|
|
313
|
+
await new Promise(() => { });
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* On-device LOCAL sensing: RTSP frame → LOCAL VLM (LiteRT-LM serving Gemma 4
|
|
317
|
+
* E2B, constrained structured output via tool-calling) → POST the STRUCTURED
|
|
318
|
+
* result as `preAnalyzed` to /v2/earner/audience-analyze (SKIPS the cloud VLM →
|
|
319
|
+
* no Vertex cost). For capable hosts running `litert-lm serve`. Multi-channel
|
|
320
|
+
* via `cameras: [...]`.
|
|
321
|
+
*/
|
|
322
|
+
async function startLocalSensing(args) {
|
|
323
|
+
const configIdx = args.indexOf('--config');
|
|
324
|
+
const configPath = configIdx >= 0 && args[configIdx + 1] ? args[configIdx + 1] : 'trillboards.config.json';
|
|
325
|
+
console.log(`Trillboards Edge AI SDK v${VERSION} — LOCAL-sensing mode (on-device VLM)`);
|
|
326
|
+
console.log(`Loading config: ${configPath}`);
|
|
327
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
328
|
+
// Build the manager (feed resolution + banner + event logging) via the shared
|
|
329
|
+
// helper — the SAME construction auto-sense uses. Null ⇒ no feeds configured.
|
|
330
|
+
const manager = (0, sensingRunMode_1.buildLocalSensingManager)(config, undefined, { configPath });
|
|
331
|
+
if (!manager) {
|
|
332
|
+
process.exit(1);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Keep the on-device VLM (Gemma) up to date over the air while local-sense runs
|
|
336
|
+
// (edge-core ModelManager + /v1/federated/models, S3 presigned). Non-fatal: a
|
|
337
|
+
// null handle (disabled / startup failure) never blocks sensing.
|
|
338
|
+
const gemmaOta = (0, gemmaModelOta_1.startGemmaModelOta)(config);
|
|
339
|
+
const shutdown = async () => {
|
|
340
|
+
console.log('\nStopping local-sensing…');
|
|
341
|
+
gemmaOta?.stop();
|
|
342
|
+
await manager.stop();
|
|
343
|
+
process.exit(0);
|
|
344
|
+
};
|
|
345
|
+
process.on('SIGINT', shutdown);
|
|
346
|
+
process.on('SIGTERM', shutdown);
|
|
347
|
+
await manager.start();
|
|
348
|
+
console.log('Local-sensing started (on-device VLM). Press Ctrl+C to stop.');
|
|
349
|
+
// Keep the process running
|
|
350
|
+
await new Promise(() => { });
|
|
351
|
+
}
|
|
352
|
+
/** Read a `--flag value` argument (returns undefined if absent). */
|
|
353
|
+
function getArg(args, flag) {
|
|
354
|
+
const i = args.indexOf(flag);
|
|
355
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
|
|
356
|
+
}
|
|
357
|
+
/** Stable, non-PII connector fingerprint from this host (for pairing audit). */
|
|
358
|
+
function hostFingerprint() {
|
|
359
|
+
const raw = `${os.hostname()}|${process.platform}|${os.arch()}`;
|
|
360
|
+
return 'conn_' + (0, crypto_1.createHash)('sha256').update(raw).digest('hex').slice(0, 24);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* True if a filesystem error is a PERMISSION failure (EACCES/EPERM), or its
|
|
364
|
+
* message says so. This is the signature of the paired-config write failing
|
|
365
|
+
* because ~/Library/Application Support/Trillboards is owned by another account
|
|
366
|
+
* — which happens when the `.pkg` was installed with admin rights and its
|
|
367
|
+
* postinstall ran as root, creating that dir ROOT-OWNED. It is NOT a bad or
|
|
368
|
+
* expired claim code, so it must never be reported as one.
|
|
369
|
+
*/
|
|
370
|
+
function isPermissionError(err) {
|
|
371
|
+
const e = err;
|
|
372
|
+
if (!e)
|
|
373
|
+
return false;
|
|
374
|
+
if (e.code === 'EACCES' || e.code === 'EPERM')
|
|
375
|
+
return true;
|
|
376
|
+
return /permission denied/i.test(String(e.message ?? ''));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The operator-facing remediation when the paired config can't be written
|
|
380
|
+
* because its directory is owned by another account. Names the REAL cause and
|
|
381
|
+
* the exact one-line fix — deliberately NOT "double-check the code" (pairing
|
|
382
|
+
* already SUCCEEDED; only the local save failed). Mirrors the guidance in
|
|
383
|
+
* deploy/macos/pair.command so the GUI and headless paths say the same thing.
|
|
384
|
+
*/
|
|
385
|
+
function configWritePermissionMessage(resolvedConfigPath) {
|
|
386
|
+
const dir = path.dirname(resolvedConfigPath);
|
|
387
|
+
return (`\nPaired successfully, but Trillboards could not save its settings to:\n` +
|
|
388
|
+
` ${resolvedConfigPath}\n\n` +
|
|
389
|
+
`That folder is owned by another account: the installer was run with admin\n` +
|
|
390
|
+
`rights, so it created the folder as root and this pairing (which runs as you)\n` +
|
|
391
|
+
`can't write there. This is NOT a bad code — pairing worked.\n\n` +
|
|
392
|
+
`Fix it once by pasting this in Terminal, then run pairing again:\n` +
|
|
393
|
+
` sudo chown -R "$(whoami)" "${dir}"\n`);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Persist the paired config, translating a PERMISSION failure into the clear
|
|
397
|
+
* remediation above (and exiting non-zero) instead of letting a raw EACCES
|
|
398
|
+
* bubble up as an opaque "Fatal error" that the GUI then mislabels "check the
|
|
399
|
+
* code". Any OTHER write error is re-thrown unchanged (surfaced as "Fatal
|
|
400
|
+
* error: …"). The writer is injectable for tests (mirrors PairClient's poster).
|
|
401
|
+
*/
|
|
402
|
+
function writePairedConfig(resolvedConfigPath, serialized, writeFile = (p, data, opts) => fs.writeFileSync(p, data, opts)) {
|
|
403
|
+
try {
|
|
404
|
+
writeFile(resolvedConfigPath, serialized, { mode: 0o600 });
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
if (isPermissionError(err)) {
|
|
408
|
+
console.error(configWritePermissionMessage(resolvedConfigPath));
|
|
409
|
+
process.exit(1);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* First-run pairing: exchange a portal claim code for the connector config and
|
|
417
|
+
* write trillboards.config.json. The device token is delivered ONLY here (never
|
|
418
|
+
* baked into the installer), and RTSP credentials can be left local — feeds
|
|
419
|
+
* without an rtspUrl are flagged so the operator fills them in on this machine.
|
|
420
|
+
*
|
|
421
|
+
* trillboards-edge pair --code XXXX-XXXX [--api-base URL] [--config PATH] [--start]
|
|
422
|
+
* trillboards-edge pair --qr <image.png|.jpg> (scan the portal QR instead of typing the code)
|
|
423
|
+
*
|
|
424
|
+
* Exit codes (the install wrappers branch on these — keep stable):
|
|
425
|
+
* 0 paired, config written, AND the post-pair connection check passed
|
|
426
|
+
* 1 pairing failed (classified: bad/expired code, network, rate-limit, server)
|
|
427
|
+
* 6 paired + config written, but the connection check hit a NETWORK/SERVER
|
|
428
|
+
* failure — the code IS spent and the config IS saved; the operator fixes
|
|
429
|
+
* connectivity and re-runs the helper WITHOUT needing a new code.
|
|
430
|
+
* 7 paired + config written, but the API REJECTED the fresh pairing
|
|
431
|
+
* (verify kind 'auth', HTTP 401/403) — the saved config is unusable and
|
|
432
|
+
* the operator DOES need a fresh code. Distinct from 6 so the wrappers
|
|
433
|
+
* never promise "no new code needed" for an auth rejection.
|
|
434
|
+
*/
|
|
435
|
+
async function runPair(args) {
|
|
436
|
+
let code = getArg(args, '--code') || getArg(args, '-c');
|
|
437
|
+
const qrPath = getArg(args, '--qr');
|
|
438
|
+
const apiBase = getArg(args, '--api-base') ||
|
|
439
|
+
process.env.TRILLBOARDS_API_BASE ||
|
|
440
|
+
'https://api.trillboards.com';
|
|
441
|
+
const configPath = getArg(args, '--config') || 'trillboards.config.json';
|
|
442
|
+
const doStart = args.includes('--start');
|
|
443
|
+
// --qr: scan the claim code from an image of the portal's Install-step QR (a
|
|
444
|
+
// screenshot or phone photo) so the owner doesn't hand-type it. The decoder is
|
|
445
|
+
// lazy-required so the common `pair --code` path stays free of the image deps.
|
|
446
|
+
// An explicit --code always wins (no surprise when both are passed).
|
|
447
|
+
if (!code && qrPath) {
|
|
448
|
+
try {
|
|
449
|
+
const { decodeClaimCodeFromImageFile } = require('./qrScanner');
|
|
450
|
+
code = await decodeClaimCodeFromImageFile(qrPath);
|
|
451
|
+
console.log(` Scanned claim code from ${qrPath}`);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.error(`\nCouldn't read a claim code from the QR image: ${err instanceof Error ? err.message : err}`);
|
|
455
|
+
console.error("Use a clear PNG/JPEG of the QR shown on the portal's Install step.");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!code) {
|
|
461
|
+
console.error('Pairing requires a claim code:');
|
|
462
|
+
console.error(' trillboards-edge pair --code XXXX-XXXX');
|
|
463
|
+
console.error(' trillboards-edge pair --qr /path/to/qr.png (scan the portal QR instead)');
|
|
464
|
+
console.error('Get one from the Trillboards portal → Add a camera → Install.');
|
|
465
|
+
process.exit(1);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(`Trillboards Edge AI SDK v${VERSION} — pairing`);
|
|
469
|
+
console.log(` API: ${apiBase}`);
|
|
470
|
+
console.log(` Code: ${(0, PairClient_1.normalizeClaimCode)(code)}`);
|
|
471
|
+
const logDir = (0, pairVerify_1.defaultLogDir)();
|
|
472
|
+
const client = new PairClient_1.PairClient({ apiBaseUrl: apiBase });
|
|
473
|
+
// Classify EVERY failure mode honestly. A transport throw (no response) and a
|
|
474
|
+
// non-2xx response get different, owner-grade copy — and only a SERVER
|
|
475
|
+
// rejection of the code may say "get a fresh code" (the historical failure was
|
|
476
|
+
// blaming valid codes for local EACCES/network problems — see pairVerify.ts).
|
|
477
|
+
let res;
|
|
478
|
+
try {
|
|
479
|
+
res = await client.pair({
|
|
480
|
+
claimCode: code,
|
|
481
|
+
connectorVersion: VERSION,
|
|
482
|
+
deviceFingerprint: hostFingerprint(),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
const failure = (0, pairVerify_1.classifyPairFailure)({ thrown: err });
|
|
487
|
+
const logFile = (0, pairVerify_1.appendPairLog)(logDir, [
|
|
488
|
+
`pair: transport failure (${failure.kind})`,
|
|
489
|
+
`pair: ${err instanceof Error ? err.message : String(err)}`,
|
|
490
|
+
]);
|
|
491
|
+
console.error(`\nPairing did not complete.\n${failure.message}`);
|
|
492
|
+
if (logFile)
|
|
493
|
+
console.error(`\nDetails saved to: ${logFile}`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!res.ok || !res.config) {
|
|
498
|
+
const failure = (0, pairVerify_1.classifyPairFailure)({ statusCode: res.statusCode, error: res.error });
|
|
499
|
+
const logFile = (0, pairVerify_1.appendPairLog)(logDir, `pair: rejected (${failure.kind}, HTTP ${res.statusCode}): ${res.error ?? ''}`);
|
|
500
|
+
console.error(`\nPairing failed.\n${failure.message}`);
|
|
501
|
+
if (logFile)
|
|
502
|
+
console.error(`\nDetails saved to: ${logFile}`);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const file = (0, PairClient_1.pairedConfigToFile)(res.config);
|
|
507
|
+
const resolved = path.resolve(configPath);
|
|
508
|
+
// Pairing succeeded — persist the config. A PERMISSION failure here (the
|
|
509
|
+
// config dir is root-owned from an admin/system install) is reported with the
|
|
510
|
+
// real cause + one-line chown fix, NOT as a bad code. See writePairedConfig.
|
|
511
|
+
writePairedConfig(resolved, JSON.stringify(file, null, 2) + '\n');
|
|
512
|
+
console.log(`\n Paired ${res.config.cameras.length} camera feed(s):`);
|
|
513
|
+
for (const cam of res.config.cameras) {
|
|
514
|
+
console.log(` - ${cam.name} → ${cam.screenId}${cam.rtspUrl ? '' : ' (add rtspUrl locally)'}`);
|
|
515
|
+
}
|
|
516
|
+
console.log(` Config written: ${resolved}`);
|
|
517
|
+
const needRtsp = res.config.cameras.filter((c) => !c.rtspUrl);
|
|
518
|
+
if (needRtsp.length > 0) {
|
|
519
|
+
console.log(`\n ${needRtsp.length} feed(s) need an RTSP URL added to ${configPath} ` +
|
|
520
|
+
'(camera credentials stay on this machine).');
|
|
521
|
+
}
|
|
522
|
+
// Post-pair verification: one authenticated round-trip with a freshly-minted
|
|
523
|
+
// device token, so "paired" is PROVEN to work from this machine (network path
|
|
524
|
+
// + token accepted), not assumed. On failure the code is already spent and the
|
|
525
|
+
// config already saved — exit 6 so the wrappers say "fix the connection and
|
|
526
|
+
// re-run, NO new code needed" instead of the failure path's "get a fresh code".
|
|
527
|
+
console.log('\n Checking the connection to Trillboards…');
|
|
528
|
+
const verifiable = (0, pairVerify_1.readVerifiableConfig)(resolved);
|
|
529
|
+
const verify = verifiable
|
|
530
|
+
? await (0, pairVerify_1.verifyApiConnection)(verifiable)
|
|
531
|
+
: { ok: false, kind: 'no_config', message: 'saved config has no device token' };
|
|
532
|
+
(0, pairVerify_1.appendPairLog)(logDir, [
|
|
533
|
+
`pair: ok (${res.config.cameras.length} feed(s), ${needRtsp.length} missing rtspUrl)`,
|
|
534
|
+
`pair: connection check ${verify.kind}`,
|
|
535
|
+
]);
|
|
536
|
+
if (verify.ok) {
|
|
537
|
+
console.log(' Connected to Trillboards — this machine can reach the API and the pairing is valid.');
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Never auto-start against an unverified connection (--start included).
|
|
541
|
+
// An AUTH rejection (the fresh token itself was refused) needs a NEW code —
|
|
542
|
+
// exit 7 so the wrappers don't give the connectivity-resume copy ("you will
|
|
543
|
+
// NOT need a new code"), which would be a lie for this case. Network/server
|
|
544
|
+
// failures exit 6: fix connectivity, re-run, same code stays unnecessary.
|
|
545
|
+
console.error(`\n${verify.message}`);
|
|
546
|
+
process.exit(verify.kind === 'auth' ? 7 : 6);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (doStart) {
|
|
550
|
+
if (needRtsp.length > 0) {
|
|
551
|
+
console.error('\nCannot auto-start: some feeds are missing rtspUrl. Edit the config, then run `auto-sense`.');
|
|
552
|
+
process.exit(1);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
console.log('\nStarting local-first sensing…\n');
|
|
556
|
+
await (0, autoSense_1.runAutoSense)(['--config', configPath]);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
114
559
|
const MODEL_URLS = {
|
|
115
560
|
'blazeface.onnx': 'https://models.trillboards.com/blazeface_128x128.onnx',
|
|
116
561
|
'yamnet.onnx': 'https://models.trillboards.com/yamnet_classification.onnx',
|
|
@@ -124,6 +569,7 @@ async function downloadModels(args) {
|
|
|
124
569
|
}
|
|
125
570
|
console.log(`Downloading ONNX models to: ${resolved}`);
|
|
126
571
|
console.log('');
|
|
572
|
+
const failed = [];
|
|
127
573
|
for (const [filename, url] of Object.entries(MODEL_URLS)) {
|
|
128
574
|
const dest = path.join(resolved, filename);
|
|
129
575
|
if (fs.existsSync(dest)) {
|
|
@@ -133,17 +579,29 @@ async function downloadModels(args) {
|
|
|
133
579
|
console.log(` [GET] ${filename}...`);
|
|
134
580
|
try {
|
|
135
581
|
await downloadFile(url, dest);
|
|
582
|
+
// A 200 is not a model. models.trillboards.com answers every path with the
|
|
583
|
+
// marketing SPA's index.html (2490 bytes, HTTP 200), so without this the
|
|
584
|
+
// command wrote a web page to blazeface.onnx and printed `[OK] (0.0 MB)`.
|
|
585
|
+
(0, modelIntegrity_1.assertOnnxModel)(dest, filename);
|
|
136
586
|
const stats = fs.statSync(dest);
|
|
137
587
|
const sizeMb = (stats.size / (1024 * 1024)).toFixed(1);
|
|
138
588
|
console.log(` [OK] ${filename} (${sizeMb} MB)`);
|
|
139
589
|
}
|
|
140
590
|
catch (err) {
|
|
591
|
+
failed.push(filename);
|
|
141
592
|
console.error(` [FAIL] ${filename}: ${err instanceof Error ? err.message : err}`);
|
|
142
593
|
// Remove partial download
|
|
143
594
|
if (fs.existsSync(dest))
|
|
144
595
|
fs.unlinkSync(dest);
|
|
145
596
|
}
|
|
146
597
|
}
|
|
598
|
+
if (failed.length > 0) {
|
|
599
|
+
console.error(`\nFAILED to download ${failed.length} of ${Object.keys(MODEL_URLS).length} model(s): ${failed.join(', ')}.`);
|
|
600
|
+
console.error('No usable model was written. Sensing that needs these models will report the ' +
|
|
601
|
+
'camera/microphone subsystem as ERROR until a valid model is placed in this directory.');
|
|
602
|
+
process.exitCode = 1;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
147
605
|
console.log('\nDone. Set models.dir in your config to point to this directory.');
|
|
148
606
|
}
|
|
149
607
|
function downloadFile(url, dest, maxRedirects = 5) {
|
|
@@ -200,6 +658,14 @@ Usage:
|
|
|
200
658
|
trillboards-edge demo Try it instantly — no token, no camera, no setup
|
|
201
659
|
trillboards-edge init Interactive setup wizard
|
|
202
660
|
trillboards-edge start [--config <path>] Start the edge agent
|
|
661
|
+
trillboards-edge cloud-sense [--config <path>] Stream RTSP frame(s) to cloud VLM (no local ONNX)
|
|
662
|
+
trillboards-edge auto-sense [--config <path>] Use on-device VLM; wait locally while its model prepares
|
|
663
|
+
trillboards-edge sense [--config <path>] Run the ONNX zoo AND the VLM together (APK parity)
|
|
664
|
+
trillboards-edge setup [--config <path>] Validate-first onboarding: opens the portal and keeps a code fallback
|
|
665
|
+
trillboards-edge pair --code <XXXX-XXXX> Pair with a portal claim code (writes config)
|
|
666
|
+
trillboards-edge pair --qr <image.png> Pair by scanning a screenshot/photo of the portal QR
|
|
667
|
+
trillboards-edge verify-connection [--config p] Prove the saved pairing works (exit 0=ok 2=network 3=auth 4=no config)
|
|
668
|
+
trillboards-edge discover Find cameras on your network (ONVIF) or expand an NVR's channels
|
|
203
669
|
trillboards-edge status [--host h] [--port p] Show agent status dashboard
|
|
204
670
|
trillboards-edge diagnose [--json] Run system diagnostics
|
|
205
671
|
trillboards-edge mcp Start MCP server (for AI agent integration)
|
|
@@ -217,6 +683,10 @@ Examples:
|
|
|
217
683
|
# Quick start with default config
|
|
218
684
|
trillboards-edge start --config trillboards.config.json
|
|
219
685
|
|
|
686
|
+
# Find cameras without typing a single RTSP URL
|
|
687
|
+
trillboards-edge discover # scan the LAN for ONVIF cameras
|
|
688
|
+
trillboards-edge discover --nvr 192.168.1.10 --brand hikvision --channels 8 --user admin --pass ****
|
|
689
|
+
|
|
220
690
|
# Check agent status (or watch in real-time)
|
|
221
691
|
trillboards-edge status
|
|
222
692
|
trillboards-edge status --watch
|
|
@@ -232,8 +702,8 @@ Examples:
|
|
|
232
702
|
trillboards-edge mcp
|
|
233
703
|
|
|
234
704
|
# Docker deployment
|
|
235
|
-
docker run -e DEVICE_TOKEN=xxx trillboards/edge-sdk:latest
|
|
236
|
-
docker run --gpus all -e DEVICE_TOKEN=xxx trillboards/edge-sdk:cuda
|
|
705
|
+
docker run -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:latest
|
|
706
|
+
docker run --gpus all -e DEVICE_TOKEN=xxx ghcr.io/trillboards/edge-sdk:cuda
|
|
237
707
|
|
|
238
708
|
Config file (trillboards.config.json):
|
|
239
709
|
{
|
|
@@ -246,12 +716,25 @@ Config file (trillboards.config.json):
|
|
|
246
716
|
"kiosk": { "enabled": true }
|
|
247
717
|
}
|
|
248
718
|
|
|
719
|
+
Cloud-sensing config (point any RTSP camera / NVR at Trillboards — no ONNX):
|
|
720
|
+
{
|
|
721
|
+
"apiBaseUrl": "https://api.trillboards.com",
|
|
722
|
+
"cloudSensing": { "enabled": true },
|
|
723
|
+
"cameras": [
|
|
724
|
+
{ "name": "Parking lot", "screenId": "scr_lot", "deviceToken": "tok1", "rtspUrl": "rtsp://user:pass@nvr:554/ch01" },
|
|
725
|
+
{ "name": "Register", "screenId": "scr_reg", "deviceToken": "tok2", "rtspUrl": "rtsp://user:pass@nvr:554/ch02" }
|
|
726
|
+
]
|
|
727
|
+
}
|
|
728
|
+
# Each feed = one screen = one device token = one sensing profile.
|
|
729
|
+
# Single feed: set "camera": { "rtspUrl": "..." } + top-level "deviceToken".
|
|
730
|
+
|
|
249
731
|
Status endpoint (when agent is running):
|
|
250
732
|
curl http://localhost:9090/status Full JSON status
|
|
251
733
|
curl http://localhost:9090/health Health check (200/503)
|
|
252
734
|
curl http://localhost:9090/metrics Prometheus metrics
|
|
253
735
|
|
|
254
736
|
Supported platforms:
|
|
737
|
+
- macOS 12+ (Apple Silicon + Intel) — one-click signed installer
|
|
255
738
|
- Linux (Ubuntu 18.04+, Debian 10+, ARM64/x64)
|
|
256
739
|
- Windows 10+ (x64)
|
|
257
740
|
|
|
@@ -262,8 +745,15 @@ Execution providers:
|
|
|
262
745
|
- cuda NVIDIA GPU (requires CUDA toolkit)
|
|
263
746
|
`);
|
|
264
747
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
748
|
+
// Auto-run ONLY when this file is the process entrypoint. The bundled
|
|
749
|
+
// `trillboards-edge` launcher does `exec node …/dist/cli.js` and npm's bin shim
|
|
750
|
+
// runs it directly — both make require.main === module true, so production
|
|
751
|
+
// behavior is unchanged. Guarding here lets the unit tests import the exported
|
|
752
|
+
// helpers (writePairedConfig, …) without dispatching a real CLI command.
|
|
753
|
+
if (require.main === module) {
|
|
754
|
+
main().catch((err) => {
|
|
755
|
+
console.error(`Fatal error: ${err instanceof Error ? err.message : err}`);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
269
759
|
//# sourceMappingURL=cli.js.map
|