@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,383 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # test-install-pair.sh — local, no-sudo, self-cleaning end-to-end proof that the
4
+ # Trillboards "cloud-sense" CCTV connector .pkg builds, installs home-domain,
5
+ # pairs, and actually senses (real bundled-ffmpeg JPEG capture → POST) on THIS
6
+ # Mac.
7
+ #
8
+ # Acceptance (each step is a TAP assert):
9
+ # 1. Build the .pkg (build-macos-pkg.sh).
10
+ # 2. Install it home-domain (installer -target CurrentUserHomeDirectory, no
11
+ # sudo). Assert node + ffmpeg + cli.js landed and `version` runs via the
12
+ # bundled node.
13
+ # 3. Start a local STUB pair+analyze server returning a valid pair response
14
+ # for a known claim code, with one camera pointed at a LOCAL synthetic
15
+ # ffmpeg source (a generated mp4 via file:), so the real connector code
16
+ # path runs without an RTSP-server rabbit-hole.
17
+ # 4. Pair headlessly (trillboards-edge pair --code … --api-base …). Assert
18
+ # exit 0, config written 0600 with the camera + deviceToken.
19
+ # 5. Start the connector from that config (cloud-sense) and assert it produces
20
+ # a real JPEG frame via the BUNDLED ffmpeg and POSTs it to
21
+ # /v2/earner/audience-analyze carrying the paired X-Device-Token. Also
22
+ # assert the per-user LaunchAgent bootstraps + senses end-to-end.
23
+ # 6. Tear everything down (bootout agent, uninstall home tree, remove temp
24
+ # config/logs, kill the stub) — idempotent / re-runnable.
25
+ #
26
+ # Re-run: bash deploy/macos/test-install-pair.sh
27
+
28
+ set -uo pipefail
29
+
30
+ # ─── Locate ──────────────────────────────────────────────────────────────────
31
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/macos
32
+ PKG_ROOT="$(cd "$HERE/../.." && pwd)" # edge-sdk
33
+ WORKSPACE_ROOT="$(cd "$PKG_ROOT/../.." && pwd)"
34
+
35
+ EDGE_ROOT="$HOME/Library/Trillboards/edge"
36
+ APP_SUPPORT="$HOME/Library/Application Support/Trillboards"
37
+ INSTALLED_CONFIG="$APP_SUPPORT/trillboards.config.json"
38
+ LOG_DIR="$HOME/Library/Logs/Trillboards"
39
+ LA_PLIST="$HOME/Library/LaunchAgents/com.trillboards.edge.plist"
40
+ LABEL="com.trillboards.edge"
41
+ # Customer-facing native setup application installed under the user's home.
42
+ SETUP_APP="$HOME/Applications/Trillboards CCTV Setup.app"
43
+ SETUP_APP_BIN="$SETUP_APP/Contents/MacOS/Trillboards CCTV Setup"
44
+ LEGACY_PAIR_LAUNCHER="$HOME/Applications/Trillboards Pair.command"
45
+ UID_N="$(id -u)"
46
+
47
+ # Scratch (under the worktree's build dir so it's easy to clean).
48
+ SCRATCH="$HERE/dist-installer/test-scratch"
49
+ SAMPLE_MP4="$SCRATCH/cctv_sample.mp4"
50
+ STUB_JS="$SCRATCH/stub_server.js"
51
+ STUB_OUT="$SCRATCH/stub.out"
52
+ TEST_CONFIG="$SCRATCH/paired.config.json"
53
+ CS_OUT="$SCRATCH/cloud-sense.out"
54
+ STUB_PID=""
55
+ CS_PID=""
56
+
57
+ # ─── TAP plumbing ────────────────────────────────────────────────────────────
58
+ TAP_N=0
59
+ FAILS=0
60
+ ok() { TAP_N=$((TAP_N+1)); printf 'ok %d - %s\n' "$TAP_N" "$1"; }
61
+ nok() { TAP_N=$((TAP_N+1)); FAILS=$((FAILS+1)); printf 'not ok %d - %s\n' "$TAP_N" "$1"; [ -n "${2:-}" ] && printf ' ---\n %s\n ---\n' "$2"; }
62
+ diag(){ printf '# %s\n' "$1"; }
63
+
64
+ # Portable octal-mode reader. The `stat` on PATH may be GNU coreutils (uses
65
+ # `-c %a`) or BSD (uses `-f %Lp`), and on this Mac Homebrew shims GNU stat in
66
+ # front — so probe both, and finally fall back to the bundled node (always
67
+ # correct). Echoes the 3-4 digit octal mode (e.g. 600) or empty on failure.
68
+ file_mode() {
69
+ local f="$1" m=""
70
+ m="$(stat -c '%a' "$f" 2>/dev/null)" && [ -n "$m" ] && { echo "$m"; return; }
71
+ m="$(stat -f '%Lp' "$f" 2>/dev/null)" && [ -n "$m" ] && { echo "$m"; return; }
72
+ if [ -x "$EDGE_ROOT/bin/node" ]; then
73
+ "$EDGE_ROOT/bin/node" -e "process.stdout.write((require('fs').statSync(process.argv[1]).mode & 0o777).toString(8))" "$f" 2>/dev/null
74
+ fi
75
+ }
76
+
77
+ # ─── Teardown (idempotent) — runs on EXIT so partial runs still clean up ──────
78
+ teardown() {
79
+ diag "teardown…"
80
+ pkill -x 'Trillboards CCTV Setup' >/dev/null 2>&1 || true
81
+ [ -n "$CS_PID" ] && kill "$CS_PID" >/dev/null 2>&1 || true
82
+ [ -n "$STUB_PID" ] && kill "$STUB_PID" >/dev/null 2>&1 || true
83
+ pkill -f "$STUB_JS" >/dev/null 2>&1 || true
84
+ launchctl bootout "gui/$UID_N/$LABEL" >/dev/null 2>&1 || true
85
+ launchctl unload "$LA_PLIST" >/dev/null 2>&1 || true
86
+ rm -f "$LA_PLIST"
87
+ rm -rf "$HOME/Library/Trillboards" "$APP_SUPPORT" "$LOG_DIR"
88
+ rm -rf "$SETUP_APP"
89
+ rm -f "$LEGACY_PAIR_LAUNCHER"
90
+ # Forget the home-domain receipt so a re-run installs cleanly.
91
+ pkgutil --volume "$HOME" --forget "$LABEL" >/dev/null 2>&1 || true
92
+ rm -rf "$SCRATCH"
93
+ }
94
+ trap teardown EXIT
95
+
96
+ # Pre-clean anything left by a prior aborted run BEFORE we start.
97
+ pkill -x 'Trillboards CCTV Setup' >/dev/null 2>&1 || true
98
+ launchctl bootout "gui/$UID_N/$LABEL" >/dev/null 2>&1 || true
99
+ rm -f "$LA_PLIST"
100
+ rm -rf "$HOME/Library/Trillboards" "$APP_SUPPORT" "$LOG_DIR"
101
+ rm -rf "$SETUP_APP"
102
+ rm -f "$LEGACY_PAIR_LAUNCHER"
103
+ pkgutil --volume "$HOME" --forget "$LABEL" >/dev/null 2>&1 || true
104
+ mkdir -p "$SCRATCH"
105
+
106
+ echo "TAP version 13"
107
+ diag "Trillboards Edge — macOS install+pair self-test"
108
+ diag "edge-sdk root: $PKG_ROOT"
109
+
110
+ # ─── Preconditions ───────────────────────────────────────────────────────────
111
+ command -v ffmpeg >/dev/null 2>&1 || { diag "ffmpeg required on PATH to generate the synthetic source"; }
112
+ if ffmpeg -hide_banner -loglevel error -f lavfi -i "testsrc=size=320x240:rate=5" -frames:v 10 -y "$SAMPLE_MP4" >/dev/null 2>&1 && [ -s "$SAMPLE_MP4" ]; then
113
+ ok "generated synthetic camera source (testsrc → $SAMPLE_MP4)"
114
+ else
115
+ nok "could not generate synthetic camera source" "ffmpeg lavfi testsrc failed"
116
+ fi
117
+
118
+ # ─── STEP 1: build the pkg ───────────────────────────────────────────────────
119
+ diag "STEP 1 — build the .pkg"
120
+ # Offline is fine if the host already has a matching node; default tries the
121
+ # pinned official download and falls back to host node automatically.
122
+ BUILD_LOG="$SCRATCH/build.log"
123
+ if "$HERE/build-macos-pkg.sh" >"$BUILD_LOG" 2>&1; then
124
+ PKG="$(tail -1 "$BUILD_LOG")"
125
+ if [ -f "$PKG" ]; then
126
+ ok "build-macos-pkg.sh produced a pkg ($(du -h "$PKG" | cut -f1)): $PKG"
127
+ else
128
+ nok "build succeeded but pkg path not found" "last line: $PKG"
129
+ PKG=""
130
+ fi
131
+ else
132
+ nok "build-macos-pkg.sh failed" "$(tail -5 "$BUILD_LOG")"
133
+ PKG=""
134
+ fi
135
+
136
+ # ─── STEP 2: install home-domain (no sudo) + assert the tree ─────────────────
137
+ diag "STEP 2 — install home-domain (no sudo) + assert tree"
138
+ if [ -n "$PKG" ]; then
139
+ if installer -pkg "$PKG" -target CurrentUserHomeDirectory >"$SCRATCH/install.log" 2>&1; then
140
+ ok "installed home-domain with no sudo"
141
+ else
142
+ nok "home-domain install failed" "$(tail -5 "$SCRATCH/install.log")"
143
+ fi
144
+ else
145
+ nok "skipped install (no pkg)"
146
+ fi
147
+
148
+ # node + ffmpeg + launcher present and executable
149
+ for b in node ffmpeg trillboards-edge trillboards-pair; do
150
+ if [ -x "$EDGE_ROOT/bin/$b" ]; then ok "bundled bin/$b is present + executable"; else nok "bundled bin/$b missing or not executable"; fi
151
+ done
152
+ # cli.js present
153
+ if [ -f "$EDGE_ROOT/lib/edge-sdk/dist/cli.js" ]; then ok "bundled cli.js present"; else nok "bundled cli.js missing"; fi
154
+
155
+ # bundled node runs
156
+ NODE_VER="$("$EDGE_ROOT/bin/node" --version 2>/dev/null || true)"
157
+ if printf '%s' "$NODE_VER" | grep -q '^v'; then ok "bundled node runs ($NODE_VER)"; else nok "bundled node did not run" "$NODE_VER"; fi
158
+
159
+ # bundled ffmpeg runs
160
+ if "$EDGE_ROOT/bin/ffmpeg" -version >/dev/null 2>&1; then ok "bundled ffmpeg runs"; else nok "bundled ffmpeg did not run"; fi
161
+
162
+ # the connector runs through the bundled launcher (this also proves the lazy
163
+ # require keeps the cloud-sense entrypoint free of the ONNX stack — the bundle
164
+ # ships NO onnxruntime-node).
165
+ LAUNCH_VER="$("$EDGE_ROOT/bin/trillboards-edge" version 2>&1 || true)"
166
+ if printf '%s' "$LAUNCH_VER" | grep -q 'edge-sdk v'; then ok "trillboards-edge version works via bundled node ($LAUNCH_VER)"; else nok "trillboards-edge version failed" "$LAUNCH_VER"; fi
167
+
168
+ # explicitly assert the heavy stack is NOT bundled (design contract)
169
+ if [ ! -d "$EDGE_ROOT/lib/node_modules/onnxruntime-node" ]; then ok "ONNX runtime correctly EXCLUDED from the bundle"; else nok "onnxruntime-node was bundled (should be excluded)"; fi
170
+
171
+ # postinstall created the runtime dirs
172
+ if [ -d "$APP_SUPPORT" ]; then ok "postinstall created Application Support dir"; else nok "Application Support dir not created"; fi
173
+ if [ -d "$LOG_DIR" ]; then ok "postinstall created Logs dir"; else nok "Logs dir not created"; fi
174
+
175
+ # The native setup application replaces the old .command launcher, so Finder
176
+ # never opens Terminal during customer setup.
177
+ if [ -d "$SETUP_APP" ]; then ok "native setup application installed in ~/Applications"; else nok "native setup application missing" "$SETUP_APP"; fi
178
+ if [ -x "$SETUP_APP_BIN" ]; then ok "native setup application executable is present"; else nok "native setup application executable missing" "$SETUP_APP_BIN"; fi
179
+ if [ ! -e "$LEGACY_PAIR_LAUNCHER" ]; then ok "legacy Terminal .command launcher is absent"; else nok "legacy Terminal launcher still exists" "$LEGACY_PAIR_LAUNCHER"; fi
180
+
181
+ # ─── STEP 3: start the stub pair+analyze server ──────────────────────────────
182
+ diag "STEP 3 — start local stub pair+analyze server"
183
+ STATS_FILE="$SCRATCH/stub.stats.json"
184
+ # Config values are passed via env (NOT templated into JS) so the source stays
185
+ # inert and robust. The camera rtspUrl points at the synthetic mp4 via file:,
186
+ # so the connector's REAL RTSPCameraProvider → bundled ffmpeg → mjpeg path runs
187
+ # without standing up an actual RTSP server.
188
+ # NOTE: quoted heredoc ('EOF') — no shell expansion inside the JS.
189
+ cat > "$STUB_JS" <<'EOF'
190
+ const http = require('http');
191
+ const fs = require('fs');
192
+ const STATS_FILE = process.env.STATS_FILE;
193
+ const SAMPLE = process.env.SAMPLE_MP4;
194
+ const received = { pair: 0, analyze: 0, lastDeviceToken: null, lastBodyLen: 0, sawJpeg: false };
195
+ const CONFIG = {
196
+ apiBaseUrl: 'http://127.0.0.1:__PORT__',
197
+ cloudSensing: { enabled: true, periodicIntervalMs: 1500, faceTriggeredIntervalMs: 1500, jpegQuality: 70 },
198
+ cameras: [{
199
+ name: 'Front Door',
200
+ screenId: 'scr_test_1',
201
+ deviceToken: 'tok_TEST_123',
202
+ rtspUrl: SAMPLE.startsWith('/') ? 'file:' + SAMPLE : SAMPLE,
203
+ width: 320,
204
+ height: 240,
205
+ }],
206
+ };
207
+ function flush() { try { fs.writeFileSync(STATS_FILE, JSON.stringify(received)); } catch (e) {} }
208
+ const server = http.createServer((req, res) => {
209
+ let body = '';
210
+ req.on('data', (c) => (body += c));
211
+ req.on('end', () => {
212
+ if (req.url.endsWith('/v2/earner/cameras/pair')) {
213
+ received.pair++;
214
+ const cfg = JSON.parse(JSON.stringify(CONFIG).replace('__PORT__', String(server.address().port)));
215
+ res.writeHead(200, { 'Content-Type': 'application/json' });
216
+ res.end(JSON.stringify(cfg));
217
+ } else if (req.url.endsWith('/v2/earner/audience-analyze')) {
218
+ received.analyze++;
219
+ received.lastDeviceToken = req.headers['x-device-token'] || null;
220
+ received.lastBodyLen = body.length;
221
+ // Body is JSON { imageBase64, ... }; decode + verify the JPEG SOI marker.
222
+ try {
223
+ const j = JSON.parse(body);
224
+ if (j.imageBase64) {
225
+ const buf = Buffer.from(j.imageBase64, 'base64');
226
+ received.sawJpeg = buf.length > 2 && buf[0] === 0xff && buf[1] === 0xd8;
227
+ }
228
+ } catch (e) {}
229
+ flush();
230
+ res.writeHead(200, { 'Content-Type': 'application/json' });
231
+ res.end(JSON.stringify({ ok: true, profileId: 'p_test' }));
232
+ } else {
233
+ res.writeHead(404);
234
+ res.end('nope');
235
+ }
236
+ });
237
+ });
238
+ server.listen(0, '127.0.0.1', () => { process.stdout.write('PORT=' + server.address().port + '\n'); flush(); });
239
+ process.on('SIGTERM', () => { flush(); process.exit(0); });
240
+ EOF
241
+
242
+ STATS_FILE="$STATS_FILE" SAMPLE_MP4="$SAMPLE_MP4" "$EDGE_ROOT/bin/node" "$STUB_JS" >"$STUB_OUT" 2>&1 &
243
+ STUB_PID=$!
244
+ # Wait for the port line (up to ~5s).
245
+ PORT=""
246
+ for _ in $(seq 1 25); do
247
+ PORT="$(grep -oE 'PORT=[0-9]+' "$STUB_OUT" 2>/dev/null | head -1 | cut -d= -f2)"
248
+ [ -n "$PORT" ] && break
249
+ sleep 0.2
250
+ done
251
+ if [ -n "$PORT" ] && kill -0 "$STUB_PID" 2>/dev/null; then
252
+ ok "stub server listening on 127.0.0.1:$PORT"
253
+ else
254
+ nok "stub server failed to start" "$(cat "$STUB_OUT" 2>/dev/null)"
255
+ fi
256
+
257
+ # ─── STEP 4: pair headlessly + assert config ─────────────────────────────────
258
+ diag "STEP 4 — headless pair + assert config (0600, deviceToken, camera)"
259
+ if [ -n "$PORT" ]; then
260
+ if "$EDGE_ROOT/bin/trillboards-edge" pair --code TEST-CODE --api-base "http://127.0.0.1:$PORT" --config "$TEST_CONFIG" >"$SCRATCH/pair.log" 2>&1; then
261
+ ok "pair exited 0"
262
+ else
263
+ nok "pair exited non-zero" "$(cat "$SCRATCH/pair.log")"
264
+ fi
265
+ else
266
+ nok "skipped pair (no stub port)"
267
+ fi
268
+
269
+ if [ -f "$TEST_CONFIG" ]; then
270
+ ok "config file written: $TEST_CONFIG"
271
+ MODE="$(file_mode "$TEST_CONFIG")"
272
+ if [ "$MODE" = "600" ]; then ok "config is mode 0600"; else nok "config mode is '$MODE' (expected 600)"; fi
273
+ if grep -q 'tok_TEST_123' "$TEST_CONFIG"; then ok "config carries the paired deviceToken"; else nok "config missing deviceToken"; fi
274
+ if grep -q 'scr_test_1' "$TEST_CONFIG"; then ok "config carries the paired camera (screenId)"; else nok "config missing camera"; fi
275
+ else
276
+ nok "config file was not written"
277
+ fi
278
+
279
+ # ─── STEP 5a: connector starts from the config + senses (foreground, bounded) ─
280
+ diag "STEP 5a — cloud-sense from config: real bundled-ffmpeg JPEG → POST"
281
+ if [ -f "$TEST_CONFIG" ]; then
282
+ "$EDGE_ROOT/bin/trillboards-edge" cloud-sense --config "$TEST_CONFIG" >"$CS_OUT" 2>&1 &
283
+ CS_PID=$!
284
+ # Wait until we see at least one analyze POST (cadence is 1.5s) — up to ~12s.
285
+ SENSED=0
286
+ for _ in $(seq 1 40); do
287
+ if grep -q 'analyze → HTTP 200' "$CS_OUT" 2>/dev/null; then SENSED=1; break; fi
288
+ if ! kill -0 "$CS_PID" 2>/dev/null; then break; fi # process died early
289
+ sleep 0.3
290
+ done
291
+ kill "$CS_PID" >/dev/null 2>&1 || true
292
+ wait "$CS_PID" 2>/dev/null || true
293
+ CS_PID=""
294
+
295
+ if [ "$SENSED" -eq 1 ]; then
296
+ ok "cloud-sense captured + POSTed a frame (HTTP 200 in connector log)"
297
+ else
298
+ nok "cloud-sense did not produce a successful POST" "$(tail -8 "$CS_OUT" 2>/dev/null)"
299
+ fi
300
+
301
+ # Inspect what the stub actually received.
302
+ if [ -f "$STATS_FILE" ]; then
303
+ A_COUNT="$("$EDGE_ROOT/bin/node" -e "process.stdout.write(String((require('$STATS_FILE').analyze)||0))" 2>/dev/null || echo 0)"
304
+ A_TOKEN="$("$EDGE_ROOT/bin/node" -e "process.stdout.write(String((require('$STATS_FILE').lastDeviceToken)||''))" 2>/dev/null || echo '')"
305
+ A_JPEG="$("$EDGE_ROOT/bin/node" -e "process.stdout.write(String((require('$STATS_FILE').sawJpeg)||false))" 2>/dev/null || echo false)"
306
+ if [ "${A_COUNT:-0}" -ge 1 ] 2>/dev/null; then ok "stub received $A_COUNT analyze POST(s)"; else nok "stub received no analyze POSTs"; fi
307
+ if [ "$A_TOKEN" = "tok_TEST_123" ]; then ok "analyze POST carried X-Device-Token from pairing"; else nok "analyze POST missing/incorrect X-Device-Token" "got: '$A_TOKEN'"; fi
308
+ if [ "$A_JPEG" = "true" ]; then ok "analyze POST body was a real JPEG (SOI 0xFFD8) from the bundled ffmpeg"; else nok "analyze POST body was not a valid JPEG"; fi
309
+ else
310
+ nok "stub stats file missing — cannot verify POST contents"
311
+ fi
312
+ else
313
+ nok "skipped cloud-sense (no config)"
314
+ fi
315
+
316
+ # ─── STEP 5b: LaunchAgent bootstrap + autonomous sensing ─────────────────────
317
+ diag "STEP 5b — pair via trillboards-pair (GUI helper, headless mode) bootstraps the LaunchAgent"
318
+ if [ -n "$PORT" ]; then
319
+ if "$EDGE_ROOT/bin/trillboards-pair" TEST-CODE --api-base "http://127.0.0.1:$PORT" >"$SCRATCH/pairgui.log" 2>&1; then
320
+ ok "trillboards-pair (headless) exited 0"
321
+ else
322
+ nok "trillboards-pair (headless) failed" "$(cat "$SCRATCH/pairgui.log")"
323
+ fi
324
+
325
+ # The agent should now be loaded; give it a moment.
326
+ if launchctl print "gui/$UID_N/$LABEL" >/dev/null 2>&1; then
327
+ ok "LaunchAgent is bootstrapped (launchctl print sees it)"
328
+ else
329
+ nok "LaunchAgent not found after pair"
330
+ fi
331
+
332
+ # Wait for the agent's own log to show a successful POST (autonomous run).
333
+ AGENT_SENSED=0
334
+ for _ in $(seq 1 40); do
335
+ if grep -q 'analyze → HTTP 200' "$LOG_DIR/edge.out.log" 2>/dev/null; then AGENT_SENSED=1; break; fi
336
+ sleep 0.3
337
+ done
338
+ if [ "$AGENT_SENSED" -eq 1 ]; then
339
+ ok "LaunchAgent autonomously captured + POSTed a frame (edge.out.log)"
340
+ else
341
+ nok "LaunchAgent did not sense within timeout" "$(tail -6 "$LOG_DIR/edge.out.log" 2>/dev/null; echo '--- err ---'; tail -4 "$LOG_DIR/edge.err.log" 2>/dev/null)"
342
+ fi
343
+
344
+ # The installed config must be 0600 too.
345
+ if [ -f "$INSTALLED_CONFIG" ]; then
346
+ IMODE="$(file_mode "$INSTALLED_CONFIG")"
347
+ if [ "$IMODE" = "600" ]; then ok "installed config is mode 0600"; else nok "installed config mode is '$IMODE' (expected 600)"; fi
348
+ else
349
+ nok "installed config not written by pair helper"
350
+ fi
351
+ else
352
+ nok "skipped LaunchAgent test (no stub port)"
353
+ fi
354
+
355
+ # ─── STEP 5c: prove the installed UI payload has the no-Terminal contract ─────
356
+ diag "STEP 5c — native setup application payload"
357
+ if plutil -lint "$SETUP_APP/Contents/Info.plist" >/dev/null 2>&1; then
358
+ ok "native setup application Info.plist is valid"
359
+ else
360
+ nok "native setup application Info.plist is invalid or missing"
361
+ fi
362
+ if file "$SETUP_APP_BIN" 2>/dev/null | grep -q 'Mach-O'; then
363
+ ok "native setup application executable is Mach-O"
364
+ else
365
+ nok "native setup application executable is not Mach-O"
366
+ fi
367
+
368
+ # ─── STEP 6: teardown happens in the EXIT trap ───────────────────────────────
369
+ diag "STEP 6 — teardown runs automatically on exit (idempotent)"
370
+
371
+ # ─── Summary ─────────────────────────────────────────────────────────────────
372
+ echo "1..$TAP_N"
373
+ echo ""
374
+ if [ "$FAILS" -eq 0 ]; then
375
+ printf '\033[1;32mPASS\033[0m — all %d checks passed.\n' "$TAP_N"
376
+ else
377
+ printf '\033[1;31mFAIL\033[0m — %d of %d checks failed.\n' "$FAILS" "$TAP_N"
378
+ fi
379
+ echo "Re-run: bash $PKG_ROOT/deploy/macos/test-install-pair.sh"
380
+
381
+ # Explicit exit so the EXIT-trap teardown (which runs AFTER this and would
382
+ # otherwise clobber $?) does not mask a failure.
383
+ if [ "$FAILS" -eq 0 ]; then exit 0; else exit 1; fi
@@ -0,0 +1,109 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ SETUP="$SCRIPT_DIR/scripts/setup-litertlm.sh"
6
+ TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/trillboards-litertlm-reinstall.XXXXXX")"
7
+ trap 'rm -rf "$TEST_ROOT"' EXIT
8
+
9
+ export HOME="$TEST_ROOT/home"
10
+ export TRILLBOARDS_TEST_STATE_DIR="$TEST_ROOT/state"
11
+ export LITERT_LM_DIR="$TEST_ROOT/store"
12
+ export TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1
13
+ FAKE_BIN="$HOME/.local/bin"
14
+ SHARE_DIR="$HOME/Library/Trillboards/edge/share"
15
+ mkdir -p "$FAKE_BIN" "$SHARE_DIR" "$TRILLBOARDS_TEST_STATE_DIR"
16
+
17
+ cat >"$FAKE_BIN/uname" <<'EOF'
18
+ #!/bin/bash
19
+ case "${1:-}" in
20
+ -s) printf 'Darwin\n' ;;
21
+ -m) printf 'arm64\n' ;;
22
+ *) printf 'Darwin\n' ;;
23
+ esac
24
+ EOF
25
+
26
+ cat >"$FAKE_BIN/uv" <<'EOF'
27
+ #!/bin/bash
28
+ exit 0
29
+ EOF
30
+
31
+ cat >"$FAKE_BIN/litert-lm" <<'EOF'
32
+ #!/bin/bash
33
+ if [ "${1:-}" = "list" ]; then
34
+ printf 'gemma-4-E2B-it.litertlm\n'
35
+ fi
36
+ exit 0
37
+ EOF
38
+
39
+ cat >"$FAKE_BIN/curl" <<'EOF'
40
+ #!/bin/bash
41
+ printf '{"data":[{"id":"gemma-4-E2B-it.litertlm"}]}\n'
42
+ EOF
43
+
44
+ cat >"$FAKE_BIN/sleep" <<'EOF'
45
+ #!/bin/bash
46
+ exit 0
47
+ EOF
48
+
49
+ cat >"$FAKE_BIN/launchctl" <<'EOF'
50
+ #!/bin/bash
51
+ set -u
52
+ STATE_DIR="${TRILLBOARDS_TEST_STATE_DIR:?}"
53
+ STATE_FILE="$STATE_DIR/service-state"
54
+ ATTEMPTS_FILE="$STATE_DIR/bootstrap-attempts"
55
+
56
+ case "${1:-}" in
57
+ bootout)
58
+ printf '3\n' >"$STATE_FILE"
59
+ exit 0
60
+ ;;
61
+ print)
62
+ [ -f "$STATE_FILE" ] || exit 113
63
+ state="$(cat "$STATE_FILE")"
64
+ if [ "$state" = "loaded" ]; then
65
+ exit 0
66
+ fi
67
+ if [ "$state" -gt 1 ]; then
68
+ printf '%s\n' "$((state - 1))" >"$STATE_FILE"
69
+ exit 0
70
+ fi
71
+ rm -f "$STATE_FILE"
72
+ exit 113
73
+ ;;
74
+ bootstrap)
75
+ attempts=0
76
+ [ ! -f "$ATTEMPTS_FILE" ] || attempts="$(cat "$ATTEMPTS_FILE")"
77
+ attempts=$((attempts + 1))
78
+ printf '%s\n' "$attempts" >"$ATTEMPTS_FILE"
79
+ if [ "$attempts" -eq 1 ]; then
80
+ printf 'Bootstrap failed: 37: Operation already in progress\n' >&2
81
+ exit 37
82
+ fi
83
+ printf 'loaded\n' >"$STATE_FILE"
84
+ exit 0
85
+ ;;
86
+ esac
87
+
88
+ exit 0
89
+ EOF
90
+
91
+ chmod +x "$FAKE_BIN/uname" "$FAKE_BIN/uv" "$FAKE_BIN/litert-lm" \
92
+ "$FAKE_BIN/curl" "$FAKE_BIN/sleep" "$FAKE_BIN/launchctl"
93
+
94
+ cat >"$SHARE_DIR/com.trillboards.litertlm.plist" <<'EOF'
95
+ <plist><string>__LITERTLM__</string><string>__PORT__</string><string>__LOG_DIR__</string></plist>
96
+ EOF
97
+ printf 'loaded\n' >"$TRILLBOARDS_TEST_STATE_DIR/service-state"
98
+
99
+ export PATH="$FAKE_BIN:/usr/bin:/bin:/usr/sbin:/sbin"
100
+ if ! OUTPUT="$(/bin/bash "$SETUP" 2>&1)"; then
101
+ printf '%s\n' "$OUTPUT" >&2
102
+ exit 1
103
+ fi
104
+
105
+ [ "$(cat "$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts")" = "2" ]
106
+ grep -Fq 'Bootstrap failed: 37: Operation already in progress' \
107
+ "$HOME/Library/Logs/Trillboards/litertlm-setup.log"
108
+ grep -Fq 'local AI is installed and healthy' <<<"$OUTPUT"
109
+ printf 'macOS LiteRT-LM reinstall race regression passed\n'
@@ -27,28 +27,38 @@ fi
27
27
  echo "Creating trillboards user and directories..."
28
28
  sudo useradd --system --home-dir /opt/trillboards --shell /usr/sbin/nologin trillboards 2>/dev/null || true
29
29
  sudo mkdir -p /opt/trillboards /var/lib/trillboards /etc/trillboards
30
- sudo chown -R trillboards:trillboards /opt/trillboards /var/lib/trillboards
30
+ # /etc/trillboards MUST be trillboards-owned too: pairing runs as the trillboards
31
+ # user and writes /etc/trillboards/config.json directly — without this chown the
32
+ # write fails with EACCES on a fresh install before the service ever has a config
33
+ # (Codex P1 on #7730).
34
+ sudo chown -R trillboards:trillboards /opt/trillboards /var/lib/trillboards /etc/trillboards
31
35
 
32
36
  # Install SDK
33
37
  echo "Installing @trillboards/edge-sdk..."
34
38
  cd /opt/trillboards
35
39
  sudo -u trillboards npm init -y --silent 2>/dev/null
36
- sudo -u trillboards npm install @trillboards/edge-sdk @trillboards/edge-platform-linux
40
+ sudo -u trillboards npm install @trillboards/edge-sdk @trillboards/edge-platform-linux @trillboards/edge-platform-rtsp
37
41
 
38
- # Download models
39
- echo "Downloading ONNX models..."
40
- sudo -u trillboards mkdir -p /opt/trillboards/models
41
- sudo -u trillboards npx trillboards-edge download-models --dir /opt/trillboards/models
42
+ # cloud-sense uses the cloud VLM for inference — NO local ONNX models to download.
42
43
 
43
- # Install config template
44
+ # Pair with a claim code from the portal (Add a camera -> Install). Pairing writes
45
+ # /etc/trillboards/config.json (device token + feeds) — the token is delivered at
46
+ # pair, NEVER baked. Accept the code as $1 for headless installs, else prompt.
47
+ CLAIM_CODE="${1:-}"
44
48
  if [ ! -f /etc/trillboards/config.json ]; then
45
- echo "Installing config template..."
46
- sudo cp node_modules/@trillboards/edge-sdk/configs/generic-linux.json /etc/trillboards/config.json
47
- sudo chown trillboards:trillboards /etc/trillboards/config.json
48
- echo ""
49
- echo " *** IMPORTANT: Edit /etc/trillboards/config.json ***"
50
- echo " Set your deviceToken and screenId from the Trillboards dashboard."
51
- echo ""
49
+ if [ -z "$CLAIM_CODE" ]; then
50
+ read -rp "Enter your Trillboards pairing code (from the portal): " CLAIM_CODE || true
51
+ fi
52
+ if [ -n "$CLAIM_CODE" ]; then
53
+ echo "Pairing with the cloud..."
54
+ sudo -u trillboards npx trillboards-edge pair --code "$CLAIM_CODE" --config /etc/trillboards/config.json
55
+ sudo chown trillboards:trillboards /etc/trillboards/config.json
56
+ else
57
+ echo ""
58
+ echo " *** No code entered — pair before starting the service: ***"
59
+ echo " sudo -u trillboards npx trillboards-edge pair --code <CODE> --config /etc/trillboards/config.json"
60
+ echo ""
61
+ fi
52
62
  fi
53
63
 
54
64
  # Install systemd service
@@ -61,7 +71,7 @@ echo ""
61
71
  echo "=== Installation Complete ==="
62
72
  echo ""
63
73
  echo "Next steps:"
64
- echo " 1. Edit /etc/trillboards/config.json with your device token"
74
+ echo " 1. (If not paired above) sudo -u trillboards npx trillboards-edge pair --code <CODE> --config /etc/trillboards/config.json"
65
75
  echo " 2. Start: sudo systemctl start trillboards-edge"
66
76
  echo " 3. Logs: sudo journalctl -u trillboards-edge -f"
67
77
  echo " 4. Status: sudo systemctl status trillboards-edge"
@@ -10,7 +10,10 @@ User=trillboards
10
10
  Group=trillboards
11
11
  WorkingDirectory=/opt/trillboards
12
12
  ExecStartPre=/usr/bin/node -e "require('@trillboards/edge-sdk')"
13
- ExecStart=/usr/bin/npx trillboards-edge start --config /etc/trillboards/config.json
13
+ # This legacy Linux installer explicitly selects cloud-sense and does not install
14
+ # LiteRT-LM. Customer macOS/Windows installers use the local-first auto-sense
15
+ # policy. The paired config is written by install.sh; the token is never baked.
16
+ ExecStart=/usr/bin/npx trillboards-edge cloud-sense --config /etc/trillboards/config.json
14
17
  Restart=always
15
18
  RestartSec=10
16
19
  StartLimitInterval=300
@@ -0,0 +1,70 @@
1
+ # Trillboards Edge — Windows local-first CCTV connector
2
+
3
+ Self-contained, **no-admin** Windows installer for the BYO-CCTV connector. The
4
+ Windows analogue of `deploy/macos/` — same `cli pair` + `auto-sense` flow, same
5
+ claim-code pairing, same CDN.
6
+
7
+ ## What the installer lays down
8
+
9
+ `%LOCALAPPDATA%\Trillboards\edge\` (per-user, no UAC):
10
+
11
+ ```
12
+ bin\node.exe official Node x64 runtime (pinned v22.22.0)
13
+ bin\ffmpeg.exe pinned static ffmpeg (eugeneware/ffmpeg-static b6.0,
14
+ SHA-256 verified — same upstream as macOS)
15
+ bin\trillboards-edge.cmd launcher (PATH=bin first; node lib\...\cli.js)
16
+ bin\trillboards-setup.vbs no-console launcher for the customer setup app
17
+ bin\trillboards-setup.ps1 native Windows Forms customer setup window
18
+ bin\trillboards-pair.cmd hidden setup engine used by the setup window
19
+ bin\register-task.ps1 registers the logon Scheduled Task
20
+ lib\edge-sdk\dist\cli.js the connector CLI
21
+ lib\node_modules\... runtime subset (NO onnxruntime/sharp/sqlite; LiteRT-LM
22
+ provides the on-device VLM outside Node)
23
+ trillboards.config.json written by `pair` (device token delivered ONLY here)
24
+ ```
25
+
26
+ RTSP capture is via the bundled ffmpeg (cross-platform), so a single x64 build
27
+ serves every Windows 10/11 — no per-arch builds.
28
+
29
+ ## Operator flow (the "no typing" path)
30
+
31
+ 1. Portal → "Add a camera" → download the single **Windows (.exe)** containing
32
+ the connector and private local AI model.
33
+ 2. Run it. Production installers are Authenticode-signed with Azure Trusted
34
+ Signing and the release workflow verifies the signature before publishing.
35
+ 3. Installer copies the connector, installs and verifies the included local AI,
36
+ registers the logon tasks, then opens **Trillboards CCTV Setup** without a
37
+ command window.
38
+ 4. Select **Start setup**. The app opens the Trillboards camera wizard, discovers
39
+ local recorders, and validates each channel. Black, disconnected, and
40
+ unreadable inputs are ignored; only working camera feeds become screens.
41
+ 5. Sensing starts with the already-installed local AI and restarts on every
42
+ logon; cloud inference is off by default.
43
+
44
+ ## Build (CI: `release-connector-windows.yml`, runs on `windows-latest`)
45
+
46
+ ```powershell
47
+ # bundles node + ffmpeg + edge-sdk, then compiles the inner NSIS .exe
48
+ ./build-windows.ps1 # -> dist-installer\inner\TrillboardsEdgeSetup-core-<ver>-x64.exe
49
+ ./build-windows.ps1 -SkipNsis # stage the tree only (no NSIS) — for checks
50
+ ./build-windows.ps1 -Offline # copy host node/ffmpeg (air-gapped)
51
+
52
+ # after the inner installer is signed, create the one-file customer artifact
53
+ ./bundle-windows-installer.ps1 -InnerInstaller <signed-inner.exe>
54
+ ```
55
+
56
+ Requires NSIS and 7-Zip on PATH (`choco install nsis 7zip -y`). The release workflow publishes
57
+ the `.exe` to `s3://trillboards-screen-static/connector/trillboards-connector-windows.exe`
58
+ and invalidates the `screen.trillboards.com` CloudFront distribution, so the
59
+ portal's stable download URL never changes.
60
+
61
+ ## Notes
62
+
63
+ - Production `.exe` artifacts are signed with Azure Trusted Signing and checked
64
+ with `Get-AuthenticodeSignature` before CDN publication.
65
+ - The one-file wrapper uses the official 7-Zip SFX installer module from the
66
+ LZMA SDK. 7-Zip is licensed under the GNU LGPL; source and license information
67
+ are available at https://www.7-zip.org/.
68
+ - The legacy `install-service.ps1` (local-inference: requires system Node + npm
69
+ install + ONNX models + a hand-edited deviceToken) is a different, older path.
70
+ This local-first installer supersedes it for BYO-CCTV onboarding.