@trillboards/edge-sdk 0.2.40 → 0.2.41

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 (82) hide show
  1. package/README.md +1 -1
  2. package/deploy/docker/{Dockerfile.cloud-sense → Dockerfile.connector} +52 -39
  3. package/deploy/docker/README.md +73 -1
  4. package/deploy/docker/connector-entrypoint.sh +46 -80
  5. package/deploy/macos/README.md +1 -1
  6. package/deploy/macos/test-install-pair.sh +9 -9
  7. package/deploy/systemd/install.sh +16 -1
  8. package/deploy/systemd/setup-litertlm.sh +201 -0
  9. package/deploy/systemd/trillboards-edge.service +8 -4
  10. package/deploy/systemd/trillboards-litertlm.service +46 -0
  11. package/deploy/windows/register-task.ps1 +6 -3
  12. package/deploy/windows/run-connector.ps1 +1 -1
  13. package/deploy/windows/trillboards-edge-task.vbs +5 -1
  14. package/dist/CloudSensingManager.d.ts +45 -19
  15. package/dist/CloudSensingManager.d.ts.map +1 -1
  16. package/dist/CloudSensingManager.js +89 -46
  17. package/dist/CloudSensingManager.js.map +1 -1
  18. package/dist/EdgeAgent.js +1 -1
  19. package/dist/SensingProfileClient.d.ts +9 -0
  20. package/dist/SensingProfileClient.d.ts.map +1 -1
  21. package/dist/SensingProfileClient.js +4 -0
  22. package/dist/SensingProfileClient.js.map +1 -1
  23. package/dist/SetupSessionClient.d.ts +0 -1
  24. package/dist/SetupSessionClient.d.ts.map +1 -1
  25. package/dist/autoSense.d.ts +42 -31
  26. package/dist/autoSense.d.ts.map +1 -1
  27. package/dist/autoSense.js +127 -99
  28. package/dist/autoSense.js.map +1 -1
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +25 -84
  31. package/dist/cli.js.map +1 -1
  32. package/dist/combinedSense.d.ts +1 -3
  33. package/dist/combinedSense.d.ts.map +1 -1
  34. package/dist/combinedSense.js +0 -1
  35. package/dist/combinedSense.js.map +1 -1
  36. package/dist/config.d.ts +17 -64
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +0 -10
  39. package/dist/config.js.map +1 -1
  40. package/dist/deployedCadence.d.ts +37 -75
  41. package/dist/deployedCadence.d.ts.map +1 -1
  42. package/dist/deployedCadence.js +45 -105
  43. package/dist/deployedCadence.js.map +1 -1
  44. package/dist/deployedCadenceSource.d.ts +0 -2
  45. package/dist/deployedCadenceSource.d.ts.map +1 -1
  46. package/dist/deployedCadenceSource.js +0 -1
  47. package/dist/deployedCadenceSource.js.map +1 -1
  48. package/dist/diagnose.d.ts +1 -1
  49. package/dist/diagnose.js +1 -1
  50. package/dist/index.d.ts +0 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -8
  53. package/dist/index.js.map +1 -1
  54. package/dist/localInferenceSchema.d.ts +19 -0
  55. package/dist/localInferenceSchema.d.ts.map +1 -1
  56. package/dist/localInferenceSchema.js +23 -0
  57. package/dist/localInferenceSchema.js.map +1 -1
  58. package/dist/localSensing.d.ts +48 -107
  59. package/dist/localSensing.d.ts.map +1 -1
  60. package/dist/localSensing.js +80 -171
  61. package/dist/localSensing.js.map +1 -1
  62. package/dist/sensingCatchRate.d.ts +246 -0
  63. package/dist/sensingCatchRate.d.ts.map +1 -0
  64. package/dist/sensingCatchRate.js +514 -0
  65. package/dist/sensingCatchRate.js.map +1 -0
  66. package/dist/sensingRunMode.d.ts +14 -11
  67. package/dist/sensingRunMode.d.ts.map +1 -1
  68. package/dist/sensingRunMode.js +9 -92
  69. package/dist/sensingRunMode.js.map +1 -1
  70. package/dist/setupMode.d.ts +0 -1
  71. package/dist/setupMode.d.ts.map +1 -1
  72. package/dist/setupMode.js +5 -3
  73. package/dist/setupMode.js.map +1 -1
  74. package/dist/startupDiscovery.d.ts +43 -0
  75. package/dist/startupDiscovery.d.ts.map +1 -0
  76. package/dist/startupDiscovery.js +72 -0
  77. package/dist/startupDiscovery.js.map +1 -0
  78. package/package.json +1 -1
  79. package/dist/CloudEscalationPolicy.d.ts +0 -143
  80. package/dist/CloudEscalationPolicy.d.ts.map +0 -1
  81. package/dist/CloudEscalationPolicy.js +0 -137
  82. package/dist/CloudEscalationPolicy.js.map +0 -1
package/README.md CHANGED
@@ -249,7 +249,7 @@ Or register via the [Trillboards API](https://api.trillboards.com/developer).
249
249
  | [docs/CLI.md](../../docs/CLI.md) | Full CLI reference with all commands, flags, and configuration |
250
250
  | [docs/MCP.md](../../docs/MCP.md) | MCP server reference for AI agent integration |
251
251
  | [docs/STATUS_API.md](../../docs/STATUS_API.md) | Status endpoint API reference |
252
- | [deploy/macos/README.md](deploy/macos/README.md) | macOS one-click `.pkg` installer (cloud-sense CCTV connector) |
252
+ | [deploy/macos/README.md](deploy/macos/README.md) | macOS one-click `.pkg` installer (local-inference CCTV connector) |
253
253
  | [deploy/docker/README.md](../../deploy/docker/README.md) | Docker deployment guide |
254
254
  | [deploy/helm/README.md](../../deploy/helm/README.md) | Helm chart deployment guide |
255
255
  | [create-trillboards-edge](../../../create-trillboards-edge/README.md) | Project scaffolding tool |
@@ -1,37 +1,46 @@
1
- # Trillboards Edge AI SDK — BYO-CCTV connector container
1
+ # Trillboards Edge AI SDK — BYO-CCTV connector container (LOCAL INFERENCE ONLY)
2
2
  #
3
- # No local ONNX models bundled, so this image never downloads models and is much
4
- # lighter than the cpu/cuda/openvino variants. It pairs with a short-lived claim
5
- # code (the device token is delivered at pair, NEVER baked into the image) then
3
+ # No models bundled, so this image never downloads models and is much lighter
4
+ # than the cpu/cuda/openvino variants. It pairs with a short-lived claim code
5
+ # (the device token is delivered at pair, NEVER baked into the image) then
6
6
  # senses. Same connector + same CLI as the macOS/Windows installers — ONE image,
7
7
  # not a parallel one.
8
8
  #
9
- # TWO TAGS, ONE IMAGE. `SENSE_MODE` bakes the default the entrypoint runs:
10
- # :auto-sense (SENSE_MODE=auto-sense, the default and the recommended tag)
11
- # probes localSensing.litertLmBaseUrl at startup and commits to
12
- # on-device inference if a healthy litert-lm is hosting the model.
13
- # Because this image bundles NO model, the entrypoint also asserts
14
- # localSensing.allowCloudFallback=true — auto-sense otherwise
15
- # treats cloud as opt-in and would WAIT forever for a runtime that
16
- # is never going to appear here. With that assert, a plain
17
- # `docker run` behaves exactly like :cloud-sense (frames to the
18
- # cloud VLM) and additionally AUTO-UPGRADES to on-device with no
19
- # restart the moment a litert-lm becomes reachable.
20
- # :cloud-sense (SENSE_MODE=cloud-sense) forces the cloud VLM. Kept because the
21
- # portal one-liner and existing installs reference it.
22
- # TRILLBOARDS_SENSE_MODE overrides either at run time.
9
+ # ── This image no longer has a cloud path ──────────────────────────────────
23
10
  #
24
- # WHAT CLOUD MODE COSTS. Measured 2026-08-12 over 255,812 image-bearing cloud VLM
25
- # calls: $0.007008 per analyzed frame, so one camera at the 300s default cadence
26
- # is $60.55/month and a 16-camera store is $969/month. On-device inference has no
27
- # per-frame cost. That gap is the entire reason this image defaults to probing.
11
+ # It was `Dockerfile.cloud-sense` and it published TWO tags, `:auto-sense` and
12
+ # `:cloud-sense`, and the entrypoint asserted `localSensing.allowCloudFallback:
13
+ # true` into the config on every start — so BOTH tags sent every analyzed frame
14
+ # to the cloud VLM until a litert-lm happened to appear.
28
15
  #
29
- # Build: docker build -f packages/edge-sdk/deploy/docker/Dockerfile.cloud-sense \
30
- # --build-arg SENSE_MODE=auto-sense \
31
- # -t trillboards/edge-sdk:auto-sense . # context = trillboards-edge-sdk
32
- # Run: docker run -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX \
16
+ # WHAT THAT COST. Measured 2026-08-12 over 255,812 image-bearing cloud VLM calls:
17
+ # $0.007008 per analyzed frame. At the 30s cadence that is ~$605/camera/month, so
18
+ # a 16-camera store was ~$9,688/month against a $99-500 subscription — roughly
19
+ # 10x underwater. On-device inference has no per-frame cost. The cloud path is
20
+ # gone from the connector entirely; there is no flag that brings it back.
21
+ #
22
+ # ── Therefore: this image REQUIRES a litert-lm you run ─────────────────────
23
+ #
24
+ # It bundles no model (the 2.4 GB Gemma weights have their own OTA channel; see
25
+ # cctv-connector-macos-readiness.md §7a for why baking them into a shipping
26
+ # artifact is waste). Point the connector at a serve on the host:
27
+ #
28
+ # docker run --network host ... # serve on localhost:9379
29
+ # # or set localSensing.litertLmBaseUrl in the config:
30
+ # # http://host.docker.internal:9379
31
+ #
32
+ # Without one, `auto-sense` WAITS: it analyzes nothing and uploads no FRAMES.
33
+ # It is not dark, though — it runs the same manager in health-only mode, so the
34
+ # cameras still open, video is still proven by a decode-only probe, and camera
35
+ # health still reports on the heartbeat with sensingState='unavailable'. The
36
+ # portal shows "connector alive, sensing unavailable" rather than something
37
+ # indistinguishable from a dead machine.
38
+ #
39
+ # Build: docker build -f packages/edge-sdk/deploy/docker/Dockerfile.connector \
40
+ # -t trillboards/edge-sdk:connector . # context = trillboards-edge-sdk
41
+ # Run: docker run --network host -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX \
33
42
  # -v trillboards:/app/data \
34
- # ghcr.io/trillboards/edge-sdk:auto-sense
43
+ # ghcr.io/trillboards/edge-sdk:connector
35
44
 
36
45
  FROM node:20-slim AS builder
37
46
 
@@ -58,7 +67,7 @@ FROM node:20-slim
58
67
 
59
68
  LABEL maintainer="Trillboards <engineering@trillboards.com>"
60
69
  ARG SENSE_MODE=auto-sense
61
- LABEL description="Trillboards Edge AI SDK — BYO-CCTV connector (no bundled models)"
70
+ LABEL description="Trillboards Edge AI SDK — BYO-CCTV connector (local inference only, no bundled models)"
62
71
 
63
72
  # ffmpeg = RTSP frame capture; ca-certificates/curl = TLS + healthcheck.
64
73
  RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -69,8 +78,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
69
78
 
70
79
  WORKDIR /app
71
80
 
72
- # Built packages (the cloud path lazy-requires the local ONNX stack, so it is
73
- # never loaded at runtime — but the workspace deps must resolve).
81
+ # Built packages. The workspace deps must resolve even for the paths this image
82
+ # does not take.
74
83
  COPY --from=builder /build/packages/edge-core/dist packages/edge-core/dist
75
84
  COPY --from=builder /build/packages/edge-core/package.json packages/edge-core/
76
85
  COPY --from=builder /build/packages/edge-sensing/dist packages/edge-sensing/dist
@@ -88,8 +97,8 @@ COPY --from=builder /build/packages/edge-platform-linux/package.json packages/ed
88
97
  COPY --from=builder /build/packages/edge-platform-rtsp/dist packages/edge-platform-rtsp/dist
89
98
  COPY --from=builder /build/packages/edge-platform-rtsp/package.json packages/edge-platform-rtsp/
90
99
 
91
- # Production deps only (NO model download — inference is cloud-side, or on a
92
- # litert-lm the operator runs themselves and points this at).
100
+ # Production deps only (NO model download — inference runs on a litert-lm the
101
+ # operator runs and points this at).
93
102
  COPY --from=builder /build/package.json /build/package-lock.json* ./
94
103
  RUN npm ci --legacy-peer-deps --omit=dev 2>/dev/null || npm install --legacy-peer-deps --omit=dev
95
104
 
@@ -104,14 +113,18 @@ RUN chmod +x /usr/local/bin/connector-entrypoint.sh
104
113
  ENV NODE_ENV=production
105
114
  ENV TRILLBOARDS_API_BASE="https://api.trillboards.com"
106
115
  ENV TRILLBOARDS_CONFIG="/app/data/trillboards.config.json"
107
- # Baked per tag by the publish matrix; overridable per run.
116
+ # Baked by the publish matrix; overridable per run with auto-sense|local-sense.
117
+ # There is no cloud-sense value any more — the entrypoint rejects it by name so
118
+ # an existing `-e TRILLBOARDS_SENSE_MODE=cloud-sense` fails loudly at start
119
+ # instead of silently meaning something else.
108
120
  ENV TRILLBOARDS_SENSE_MODE=${SENSE_MODE}
109
121
 
110
- # No HEALTHCHECK / status-server port: the sensing CLI paths build
111
- # CloudSensingManager directly and never start the StatusServer (that wiring is
112
- # only in EdgeAgent.start), so a /health:9090 probe would never respond and Docker
113
- # would mark a perfectly-streaming container unhealthy (Codex P2 on #7730).
114
- # Liveness for this streaming worker is observed server-side via the cloud ingest,
115
- # not a local HTTP probe.
122
+ # No HEALTHCHECK / status-server port: the sensing CLI paths build the sensing
123
+ # manager directly and never start the StatusServer (that wiring is only in
124
+ # EdgeAgent.start), so a /health:9090 probe would never respond and Docker would
125
+ # mark a perfectly-streaming container unhealthy (Codex P2 on #7730). Liveness
126
+ # for this streaming worker is observed server-side via the connector beacon —
127
+ # which now also fires while `auto-sense` is WAITING for its model, so a stalled
128
+ # container is distinguishable from a dead one without a local probe.
116
129
 
117
130
  ENTRYPOINT ["/usr/local/bin/connector-entrypoint.sh"]
@@ -4,13 +4,72 @@ This guide covers deploying the Trillboards Edge AI agent as a Docker container
4
4
 
5
5
  ## Image Variants
6
6
 
7
- Three pre-built images target different hardware profiles:
7
+ Four pre-built images. The first three are the **DOOH screen agent** (on-device ONNX,
8
+ bundled models) and target different hardware profiles. The fourth is the **BYO-CCTV
9
+ connector**, which is a different product on different hardware — see below.
8
10
 
9
11
  | Tag | Base Image | Execution Provider | Architecture | Use Case |
10
12
  |-----|-----------|-------------------|--------------|----------|
11
13
  | `latest` | `node:20-slim` | CPU (WASM) | `linux/amd64`, `linux/arm64` | Generic Linux devices, ARM SBCs, Raspberry Pi 4+ |
12
14
  | `cuda` | `nvidia/cuda:12.3.2-runtime-ubuntu22.04` | NVIDIA CUDA | `linux/amd64` | NVIDIA Jetson, GPU-equipped signage players |
13
15
  | `openvino` | `openvino/ubuntu22_runtime:2024.0.0` | Intel OpenVINO | `linux/amd64` | Backpackster, Intel NUC, mini-PCs with Intel HD 620+ |
16
+ | `auto-sense` | `node:20-slim` | **LiteRT-LM you run** (see below) | `linux/amd64`, `linux/arm64` | **BYO-CCTV connector** — pull frames from an existing NVR/DVR over RTSP |
17
+
18
+ ## The BYO-CCTV connector image (`:auto-sense`)
19
+
20
+ Built from `Dockerfile.connector`. It pairs with a short-lived claim code from the
21
+ portal (the device token is delivered at pair, **never baked into the image**), then
22
+ senses. Same connector and same CLI as the macOS/Windows installers — one image, not
23
+ a parallel product.
24
+
25
+ ### It is LOCAL-INFERENCE ONLY, and it bundles no model
26
+
27
+ The connector runs Gemma **on the machine** via LiteRT-LM. There is no cloud VLM path
28
+ in it, no `allowCloudFallback`, and no `cloud-sense` mode. This image ships no model,
29
+ so you must give it a `litert-lm serve` it can reach:
30
+
31
+ ```bash
32
+ # serve on the host's localhost:9379
33
+ docker run --network host \
34
+ -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX \
35
+ -v trillboards:/app/data \
36
+ ghcr.io/trillboards/edge-sdk:auto-sense
37
+
38
+ # …or point it at one explicitly, via localSensing.litertLmBaseUrl in the config:
39
+ # "litertLmBaseUrl": "http://host.docker.internal:9379"
40
+ ```
41
+
42
+ Without a reachable serve the connector **waits**: it analyzes nothing and uploads no
43
+ frames, but it still opens the cameras and reports their health (`sensingState:
44
+ 'unavailable'`), so the portal shows a live connector that cannot sense yet rather
45
+ than something indistinguishable from a dead machine. It starts sensing automatically
46
+ — no restart — the moment a serve appears.
47
+
48
+ Mount `/app/data` on a volume so the paired config survives restarts; re-pairing needs
49
+ a fresh code from the portal.
50
+
51
+ ### `TRILLBOARDS_SENSE_MODE`
52
+
53
+ | Value | Behaviour |
54
+ |---|---|
55
+ | `auto-sense` (default) | Probe for a local runtime; run on-device if healthy, otherwise wait and report health. |
56
+ | `local-sense` | Require the local runtime; no probe loop. |
57
+ | `cloud-sense` | **Removed.** Rejected by name at start with a message, rather than ignored — existing units and container args pin this verb, and a silent behaviour change would be worse than a hard stop. |
58
+
59
+ ### Why there is no cloud tag any more
60
+
61
+ There used to be a second tag, `:cloud-sense`, and the entrypoint asserted
62
+ `localSensing.allowCloudFallback: true` on every start — so **both** tags sent every
63
+ analyzed frame to the cloud VLM. Measured 2026-08-12 over 255,812 image-bearing cloud
64
+ VLM calls, that cost **$0.007008 per analyzed frame**: ~$605/camera/month at a 30s
65
+ cadence, so a 16-camera store ran ~$9,688/month against a $99-500 subscription. The
66
+ path was removed rather than rate-limited. On-device inference has no per-frame cost.
67
+
68
+ The tag NAME `:auto-sense` is deliberately unchanged, so existing `docker pull`
69
+ one-liners in the portal and runbooks keep resolving.
70
+
71
+ > **Note:** this says nothing about the DOOH **screen** fleet, whose cloud vision is
72
+ > untouched — different product, different hardware, and it runs server-side.
14
73
 
15
74
  ## Registry Locations
16
75
 
@@ -22,6 +81,7 @@ Images are published to two registries. Use whichever is reachable from your dep
22
81
  docker pull ghcr.io/trillboards/edge-sdk:latest
23
82
  docker pull ghcr.io/trillboards/edge-sdk:cuda
24
83
  docker pull ghcr.io/trillboards/edge-sdk:openvino
84
+ docker pull ghcr.io/trillboards/edge-sdk:auto-sense # BYO-CCTV connector
25
85
  ```
26
86
 
27
87
  **AWS ECR (private, for production fleet):**
@@ -253,6 +313,18 @@ docker build -f packages/edge-sdk/deploy/docker/Dockerfile.openvino \
253
313
  -t trillboards/edge-sdk:openvino .
254
314
  ```
255
315
 
316
+ **BYO-CCTV connector image:**
317
+
318
+ ```bash
319
+ cd trillboards-edge-sdk
320
+ docker build -f packages/edge-sdk/deploy/docker/Dockerfile.connector \
321
+ --build-arg SENSE_MODE=auto-sense \
322
+ -t trillboards/edge-sdk:auto-sense .
323
+ ```
324
+
325
+ (The file was `Dockerfile.cloud-sense` until edge-sdk 0.2.41. It was renamed because a
326
+ file named after a mode the connector no longer has is a trap for the next reader.)
327
+
256
328
  ### Build Arguments
257
329
 
258
330
  The Dockerfiles do not expose build arguments by default. To customize the build (for example, to pin a specific Node.js version or add additional system packages), edit the relevant Dockerfile directly.
@@ -5,50 +5,60 @@
5
5
  # thereafter it senses. Mount /app/data on a volume so the paired config survives
6
6
  # restarts — re-pairing needs a fresh code from the portal.
7
7
  #
8
- # ── The mode is a build arg, and the DEFAULT is auto-sense ──────────────────
8
+ # ── LOCAL INFERENCE ONLY ───────────────────────────────────────────────────
9
9
  #
10
- # This entrypoint used to `exec cloud-sense` unconditionally, so the Docker path
11
- # forced the expensive mode while §A5 of cctv-rushi-activation.md recommended
12
- # auto-sense and §0 told the reader "the connector runs auto-sense". It did not.
10
+ # This entrypoint used to assert `localSensing.allowCloudFallback: true` into the
11
+ # paired config on EVERY start, so a plain `docker run` sent every analyzed frame
12
+ # to the cloud VLM. It was one of three places that forced the cloud path (the
13
+ # systemd unit hardcoded `cloud-sense`; `TRILLBOARDS_SENSE_MODE` overrode at run
14
+ # time). All three are gone.
13
15
  #
14
- # auto-sense probes `localSensing.litertLmBaseUrl` at startup and commits to
15
- # local if a healthy litert-lm is hosting the model. An operator who runs
16
- # `litert-lm serve` on the host and points the connector at it (--network host,
17
- # or litertLmBaseUrl: http://host.docker.internal:9379 in the config) gets the
18
- # on-device path from this SAME image, at no per-frame cloud cost.
16
+ # WHY. Measured 2026-08-12 over 255,812 image-bearing cloud VLM calls: $0.007008
17
+ # per analyzed frame — ~$605/camera/month at a 30s cadence, ~$9,688/month for a
18
+ # 16-camera store against a $99-500 subscription. The product was ~10x underwater
19
+ # at scale. On-device inference has no per-frame cost.
19
20
  #
20
- # ── Why this image must also enable cloud FALLBACK ─────────────────────────
21
+ # This SUPERSEDES the escalation architecture from #11971 ("local Gemma decides
22
+ # *when* to look, the cloud decides *what* it saw") and the frequency-based cost
23
+ # control in plans/cctv-byo-sensing-plan.md:26. Cost is no longer managed by
24
+ # looking less often; the metered path is removed.
21
25
  #
22
- # `auto-sense` treats cloud inference as opt-in: with `allowCloudFallback`
23
- # unset it probes, fails, and WAITS — uploading nothing — because on a laptop or
24
- # a mini-PC that is downloading a 2.4 GB model, idling briefly is better than
25
- # silently starting a meter. That is right for the macOS/Windows installers,
26
- # which provision a local model.
26
+ # ── What this image needs from you ────────────────────────────────────────
27
27
  #
28
- # It is wrong here. This image bundles NO model and never downloads one, so a
29
- # plain `docker run` has nothing to wait FOR: without the fallback the connector
30
- # would sit idle forever and the operator would see cameras that never report.
31
- # So the entrypoint asserts `localSensing.allowCloudFallback: true` into the
32
- # paired config for auto-sense. Net behaviour for a plain run is identical to
33
- # the old `:cloud-sense` — frames go to the cloud VLM — except the connector now
34
- # AUTO-UPGRADES to on-device with no restart the moment a litert-lm becomes
35
- # reachable.
28
+ # It bundles no model, so it needs a litert-lm serve it can reach:
29
+ # docker run --network host ... # serve on localhost:9379
30
+ # # or set localSensing.litertLmBaseUrl to http://host.docker.internal:9379
36
31
  #
37
- # TRILLBOARDS_SENSE_MODE overrides at run time: cloud-sense forces the cloud VLM,
38
- # local-sense refuses to fall back.
32
+ # Without one, `auto-sense` probes and WAITS: it analyzes nothing and uploads no
33
+ # FRAMES. It is not dark, though — it runs the same manager in health-only mode,
34
+ # so the cameras open, video is proven, and camera health reports on the
35
+ # heartbeat with sensingState='unavailable'. The portal sees a live connector
36
+ # that cannot sense yet, rather than one indistinguishable from a dead machine.
37
+ #
38
+ # TRILLBOARDS_SENSE_MODE selects auto-sense (probe, then local-or-wait) or
39
+ # local-sense (require local, no probe loop). `cloud-sense` is rejected BY NAME
40
+ # rather than ignored, so an existing deployment pinning it stops with a message
41
+ # instead of silently getting different behaviour.
39
42
  set -eu
40
43
 
41
44
  CONFIG="${TRILLBOARDS_CONFIG:-/app/data/trillboards.config.json}"
42
45
  API_BASE="${TRILLBOARDS_API_BASE:-https://api.trillboards.com}"
43
46
  CLI="node packages/edge-sdk/dist/cli.js"
44
- # Baked per image variant (:auto-sense -> auto-sense, :cloud-sense -> cloud-sense),
45
- # overridable per run.
46
47
  MODE="${TRILLBOARDS_SENSE_MODE:-auto-sense}"
47
48
 
48
49
  case "$MODE" in
49
- auto-sense|cloud-sense|local-sense) ;;
50
+ auto-sense|local-sense) ;;
51
+ cloud-sense)
52
+ echo "[connector] TRILLBOARDS_SENSE_MODE=cloud-sense is no longer supported." >&2
53
+ echo "[connector] The connector is local-inference only: cloud VLM inference cost" >&2
54
+ echo "[connector] \$0.007008/frame (~\$9,688/month for a 16-camera store), so the path" >&2
55
+ echo "[connector] was removed rather than rate-limited." >&2
56
+ echo "[connector] Use auto-sense (the default) and give the container a litert-lm:" >&2
57
+ echo "[connector] docker run --network host ... # serve on localhost:9379" >&2
58
+ exit 1
59
+ ;;
50
60
  *)
51
- echo "[connector] Unknown TRILLBOARDS_SENSE_MODE '$MODE' (want auto-sense|cloud-sense|local-sense)." >&2
61
+ echo "[connector] Unknown TRILLBOARDS_SENSE_MODE '$MODE' (want auto-sense|local-sense)." >&2
52
62
  exit 1
53
63
  ;;
54
64
  esac
@@ -57,64 +67,20 @@ if [ ! -f "$CONFIG" ]; then
57
67
  if [ -z "${TRILLBOARDS_CLAIM_CODE:-}" ]; then
58
68
  echo "[connector] No paired config and no TRILLBOARDS_CLAIM_CODE." >&2
59
69
  echo "[connector] Get a code from the Trillboards portal (Add a camera -> Install), then:" >&2
60
- echo "[connector] docker run -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX -v trillboards:/app/data ghcr.io/trillboards/edge-sdk:auto-sense" >&2
70
+ echo "[connector] docker run --network host -e TRILLBOARDS_CLAIM_CODE=XXXX-XXXX -v trillboards:/app/data ghcr.io/trillboards/edge-sdk:connector" >&2
61
71
  exit 1
62
72
  fi
63
73
  echo "[connector] Pairing with claim code (token delivered at pair, never baked)..."
64
74
  # shellcheck disable=SC2086
65
- $CLI pair --code "$TRILLBOARDS_CLAIM_CODE" --config "$CONFIG" --api-base "$API_BASE"
66
- fi
67
-
68
- if [ "$MODE" = "auto-sense" ]; then
69
- # This image ships no local model, so auto-sense MUST be allowed to fall back
70
- # or it waits forever for something that will never arrive. Idempotent, and it
71
- # never touches a value the operator set themselves.
72
- #
73
- # The assert WRITES, and a config file is very often mounted read-only
74
- # (-v ./trillboards.config.json:/app/data/trillboards.config.json:ro)
75
- # which is a perfectly reasonable way to run this image and skips pairing
76
- # entirely. Under `set -e` an unguarded writeFileSync there killed the
77
- # container on every single start, with a raw stack trace, because a DEFAULT
78
- # could not be persisted. So a failed write degrades to exactly the behaviour
79
- # the write was there to guarantee — cloud inference this run — and says so.
80
- set +e
81
- node -e '
82
- const fs = require("fs");
83
- const p = process.argv[1];
84
- let c;
85
- try { c = JSON.parse(fs.readFileSync(p, "utf-8")); } catch (e) {
86
- console.error("[connector] cannot read config for fallback assert: " + e.message);
87
- process.exit(0);
88
- }
89
- c.localSensing = c.localSensing || {};
90
- if (c.localSensing.allowCloudFallback === true) process.exit(0);
91
- if (c.localSensing.allowCloudFallback === false) {
92
- console.error("[connector] localSensing.allowCloudFallback is explicitly false — honoring it; this connector will WAIT for a local runtime and upload nothing until one is reachable.");
93
- process.exit(0);
94
- }
95
- c.localSensing.allowCloudFallback = true;
96
- try {
97
- fs.writeFileSync(p, JSON.stringify(c, null, 2));
98
- } catch (e) {
99
- console.error("[connector] cannot persist localSensing.allowCloudFallback to " + p + ": " + e.message);
100
- process.exit(3);
101
- }
102
- console.error("[connector] enabled localSensing.allowCloudFallback (this image bundles no local model).");
103
- ' "$CONFIG"
104
- FALLBACK_ASSERT_RC=$?
105
- set -e
106
- if [ "$FALLBACK_ASSERT_RC" = "3" ]; then
107
- echo "[connector] config is not writable, so auto-sense cannot record its cloud-fallback policy." >&2
108
- echo "[connector] Running cloud-sense for this process instead — cameras still report. To get the" >&2
109
- echo "[connector] on-device auto-upgrade, mount the config writable or set localSensing.allowCloudFallback." >&2
110
- MODE=cloud-sense
111
- fi
75
+ $CLI pair --code "$TRILLBOARDS_CLAIM_CODE" --config "$CONFIG" --api-base "$API_BASE"
112
76
  fi
113
77
 
114
78
  if [ "$MODE" = "auto-sense" ]; then
115
- echo "[connector] Starting $MODE (API $API_BASE) — probing for on-device inference," >&2
116
- echo "[connector] falling back to the cloud VLM if none is reachable, and" >&2
117
- echo "[connector] auto-upgrading to on-device (no restart) if one appears." >&2
79
+ echo "[connector] Starting $MODE (API $API_BASE) — probing for on-device inference." >&2
80
+ echo "[connector] This image bundles no model. If no litert-lm is reachable the" >&2
81
+ echo "[connector] connector analyzes nothing and uploads no frames — but it still" >&2
82
+ echo "[connector] opens the cameras and reports their health, so the portal shows it" >&2
83
+ echo "[connector] alive. It starts sensing automatically once a litert-lm appears." >&2
118
84
  else
119
85
  echo "[connector] Starting explicitly selected $MODE (API $API_BASE)..." >&2
120
86
  fi
@@ -97,7 +97,7 @@ bash packages/edge-sdk/deploy/macos/test-install-pair.sh
97
97
  ```
98
98
 
99
99
  No sudo. It builds the pkg, installs it home-domain, starts a local stub
100
- pair+analyze server, pairs headlessly, runs `cloud-sense` against a **synthetic
100
+ pair+analyze server, pairs headlessly, runs `auto-sense` against a **synthetic
101
101
  ffmpeg source** (a generated clip via `file:` — no RTSP server needed), asserts a
102
102
  **real JPEG frame** is produced by the bundled ffmpeg and POSTed with the paired
103
103
  `X-Device-Token`, verifies the LaunchAgent bootstraps and senses autonomously,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
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,
4
+ # Trillboards CCTV connector .pkg builds, installs home-domain,
5
5
  # pairs, and actually senses (real bundled-ffmpeg JPEG capture → POST) on THIS
6
6
  # Mac.
7
7
  #
@@ -16,7 +16,7 @@
16
16
  # path runs without an RTSP-server rabbit-hole.
17
17
  # 4. Pair headlessly (trillboards-edge pair --code … --api-base …). Assert
18
18
  # exit 0, config written 0600 with the camera + deviceToken.
19
- # 5. Start the connector from that config (cloud-sense) and assert it produces
19
+ # 5. Start the connector from that config (auto-sense) and assert it produces
20
20
  # a real JPEG frame via the BUNDLED ffmpeg and POSTs it to
21
21
  # /v2/earner/audience-analyze carrying the paired X-Device-Token. Also
22
22
  # assert the per-user LaunchAgent bootstraps + senses end-to-end.
@@ -50,7 +50,7 @@ SAMPLE_MP4="$SCRATCH/cctv_sample.mp4"
50
50
  STUB_JS="$SCRATCH/stub_server.js"
51
51
  STUB_OUT="$SCRATCH/stub.out"
52
52
  TEST_CONFIG="$SCRATCH/paired.config.json"
53
- CS_OUT="$SCRATCH/cloud-sense.out"
53
+ CS_OUT="$SCRATCH/auto-sense.out"
54
54
  STUB_PID=""
55
55
  CS_PID=""
56
56
 
@@ -160,7 +160,7 @@ if printf '%s' "$NODE_VER" | grep -q '^v'; then ok "bundled node runs ($NODE_VER
160
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
161
 
162
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
163
+ # require keeps the pair/sense entrypoint free of the ONNX stack — the bundle
164
164
  # ships NO onnxruntime-node).
165
165
  LAUNCH_VER="$("$EDGE_ROOT/bin/trillboards-edge" version 2>&1 || true)"
166
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
@@ -277,9 +277,9 @@ else
277
277
  fi
278
278
 
279
279
  # ─── STEP 5a: connector starts from the config + senses (foreground, bounded) ─
280
- diag "STEP 5a — cloud-sense from config: real bundled-ffmpeg JPEG → POST"
280
+ diag "STEP 5a — auto-sense from config: real bundled-ffmpeg JPEG → POST"
281
281
  if [ -f "$TEST_CONFIG" ]; then
282
- "$EDGE_ROOT/bin/trillboards-edge" cloud-sense --config "$TEST_CONFIG" >"$CS_OUT" 2>&1 &
282
+ "$EDGE_ROOT/bin/trillboards-edge" auto-sense --config "$TEST_CONFIG" >"$CS_OUT" 2>&1 &
283
283
  CS_PID=$!
284
284
  # Wait until we see at least one analyze POST (cadence is 1.5s) — up to ~12s.
285
285
  SENSED=0
@@ -293,9 +293,9 @@ if [ -f "$TEST_CONFIG" ]; then
293
293
  CS_PID=""
294
294
 
295
295
  if [ "$SENSED" -eq 1 ]; then
296
- ok "cloud-sense captured + POSTed a frame (HTTP 200 in connector log)"
296
+ ok "auto-sense captured + POSTed a frame (HTTP 200 in connector log)"
297
297
  else
298
- nok "cloud-sense did not produce a successful POST" "$(tail -8 "$CS_OUT" 2>/dev/null)"
298
+ nok "auto-sense did not produce a successful POST" "$(tail -8 "$CS_OUT" 2>/dev/null)"
299
299
  fi
300
300
 
301
301
  # Inspect what the stub actually received.
@@ -310,7 +310,7 @@ if [ -f "$TEST_CONFIG" ]; then
310
310
  nok "stub stats file missing — cannot verify POST contents"
311
311
  fi
312
312
  else
313
- nok "skipped cloud-sense (no config)"
313
+ nok "skipped auto-sense (no config)"
314
314
  fi
315
315
 
316
316
  # ─── STEP 5b: LaunchAgent bootstrap + autonomous sensing ─────────────────────
@@ -39,7 +39,19 @@ cd /opt/trillboards
39
39
  sudo -u trillboards npm init -y --silent 2>/dev/null
40
40
  sudo -u trillboards npm install @trillboards/edge-sdk @trillboards/edge-platform-linux @trillboards/edge-platform-rtsp
41
41
 
42
- # cloud-sense uses the cloud VLM for inference — NO local ONNX models to download.
42
+ # LOCAL INFERENCE ONLY. The unit runs `auto-sense`, which is local-or-wait — it
43
+ # never uploads a frame to a cloud VLM. So the on-device runtime is not optional
44
+ # on this platform any more: provision LiteRT-LM + Gemma before the service can
45
+ # do anything. ~2.4 GB, public HF repo, home-domain (no sudo pip).
46
+ echo "Provisioning the on-device AI runtime (LiteRT-LM + Gemma, ~2.4 GB)..."
47
+ sudo cp node_modules/@trillboards/edge-sdk/deploy/systemd/trillboards-litertlm.service /etc/systemd/system/
48
+ sudo systemctl daemon-reload
49
+ sudo systemctl enable trillboards-litertlm
50
+ sudo -u trillboards HOME=/opt/trillboards bash \
51
+ node_modules/@trillboards/edge-sdk/deploy/systemd/setup-litertlm.sh \
52
+ || echo " *** Local AI provisioning did not finish. The connector will run and report
53
+ *** itself as 'waiting for the on-device model' until it does — it will NOT
54
+ *** fall back to paid cloud inference. Re-run install.sh to retry."
43
55
 
44
56
  # Pair with a claim code from the portal (Add a camera -> Install). Pairing writes
45
57
  # /etc/trillboards/config.json (device token + feeds) — the token is delivered at
@@ -74,5 +86,8 @@ echo "Next steps:"
74
86
  echo " 1. (If not paired above) sudo -u trillboards npx trillboards-edge pair --code <CODE> --config /etc/trillboards/config.json"
75
87
  echo " 2. Start: sudo systemctl start trillboards-edge"
76
88
  echo " 3. Logs: sudo journalctl -u trillboards-edge -f"
89
+ echo " 4. Local AI: sudo systemctl status trillboards-litertlm"
90
+ echo " (the connector runs ON-DEVICE only; if the serve is down it waits and"
91
+ echo " reports 'waiting' in the portal rather than billing cloud inference)"
77
92
  echo " 4. Status: sudo systemctl status trillboards-edge"
78
93
  echo ""