@trillboards/edge-sdk 0.2.74 → 0.2.76

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 (47) hide show
  1. package/deploy/detector/README.md +133 -0
  2. package/deploy/detector/bench/verify_against_reference.py +120 -0
  3. package/deploy/detector/pyproject.toml +49 -0
  4. package/deploy/detector/tests/__init__.py +0 -0
  5. package/deploy/detector/tests/test_modelpath.py +108 -0
  6. package/deploy/detector/tests/test_server.py +577 -0
  7. package/deploy/detector/tests/test_session.py +242 -0
  8. package/deploy/detector/trillboards_detector/__init__.py +8 -0
  9. package/deploy/detector/trillboards_detector/__main__.py +4 -0
  10. package/deploy/detector/trillboards_detector/cli.py +141 -0
  11. package/deploy/detector/trillboards_detector/errors.py +16 -0
  12. package/deploy/detector/trillboards_detector/labels.py +107 -0
  13. package/deploy/detector/trillboards_detector/loader.py +257 -0
  14. package/deploy/detector/trillboards_detector/modelpath.py +74 -0
  15. package/deploy/detector/trillboards_detector/server.py +460 -0
  16. package/deploy/detector/trillboards_detector/session.py +354 -0
  17. package/deploy/macos/build-macos-pkg.sh +48 -0
  18. package/deploy/macos/com.trillboards.detector.plist +61 -0
  19. package/deploy/macos/scripts/ensure-detector.sh +85 -0
  20. package/deploy/macos/scripts/postinstall +28 -0
  21. package/deploy/macos/scripts/setup-detector.sh +185 -0
  22. package/deploy/systemd/install.sh +35 -0
  23. package/deploy/systemd/setup-detector.sh +130 -0
  24. package/deploy/systemd/trillboards-detector.service +54 -0
  25. package/deploy/windows/build-windows.ps1 +38 -0
  26. package/deploy/windows/installer.nsi +24 -1
  27. package/deploy/windows/register-detector-task.ps1 +161 -0
  28. package/deploy/windows/register-task.ps1 +27 -0
  29. package/deploy/windows/setup-detector.ps1 +176 -0
  30. package/deploy/windows/stop-existing.ps1 +1 -0
  31. package/deploy/windows/trillboards-edge.cmd +25 -0
  32. package/dist/EdgeAgent.js +1 -1
  33. package/dist/LocalDetectorClient.d.ts +92 -0
  34. package/dist/LocalDetectorClient.d.ts.map +1 -0
  35. package/dist/LocalDetectorClient.js +241 -0
  36. package/dist/LocalDetectorClient.js.map +1 -0
  37. package/dist/diagnose.d.ts +1 -1
  38. package/dist/diagnose.js +1 -1
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +17 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/zoneContainment.d.ts +148 -0
  44. package/dist/zoneContainment.d.ts.map +1 -0
  45. package/dist/zoneContainment.js +172 -0
  46. package/dist/zoneContainment.js.map +1 -0
  47. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ #!/bin/bash
2
+ #
3
+ # setup-detector.sh — provision the on-device OBJECT DETECTOR service
4
+ # (RF-DETR Medium, fp32 ONNX, 127.0.0.1:9380). Runs AS THE USER (home-domain,
5
+ # no sudo). Idempotent and safe to re-run.
6
+ #
7
+ # WHAT IT DOES, AND WHAT IT DELIBERATELY DOES NOT.
8
+ # 1. Ensure `uv` — same discovery, same enterprise-TLS and proxy handling as
9
+ # setup-litertlm.sh, because the failure it prevents is identical: behind a
10
+ # TLS-inspecting proxy, `uv tool install` dies on the private root and the
11
+ # installer blames the wrong component.
12
+ # 2. `uv tool install` the staged `trillboards-detector` package. ITS OWN TOOL
13
+ # NAME, so uv gives it its own venv — never a package added into litert-lm's,
14
+ # which `uv tool install --force` rebuilds from scratch on any drift.
15
+ # 3. Render + bootstrap the LaunchAgent. Port 9380; 9379 is not touched.
16
+ # 4. Verify the SERVICE ANSWERS. Not that it is ready — see below.
17
+ #
18
+ # It does NOT fetch the model. 124 MB of ONNX does not fit under the macOS
19
+ # slim build's MAX_SLIM_PKG_BYTES = 250 MiB cap with a ~207 MB code slice, so
20
+ # the artifact is OTA'd by edge-core's ModelManager exactly as Gemma's is.
21
+ #
22
+ # WHY THIS IS BEST-EFFORT AND setup-litertlm.sh IS FATAL.
23
+ # That script may abort the install because without Gemma the connector cannot
24
+ # sense at all. The detector is a CASCADE GATE: with it, ~84.7% of VLM calls are
25
+ # removed for ~2.1% of events; without it every frame simply goes to the VLM, as
26
+ # it does today. A cost optimisation must never be able to block a customer's
27
+ # camera pairing.
28
+ #
29
+ # WHY "ANSWERS" AND NOT "READY" IS THE INSTALL-TIME BAR.
30
+ # At install time the model has not been downloaded yet, so /health is 503 by
31
+ # design. Demanding 200 here would fail every fresh install. What IS checkable —
32
+ # and what actually distinguishes a working install from a broken one — is that
33
+ # the service is listening and speaking JSON at all. The distinction is recorded
34
+ # in the log so an operator reading it can tell "waiting for the model" from
35
+ # "did not start", which is the exact pair litert-lm's `/v1/models` check could
36
+ # not tell apart.
37
+
38
+ set -u
39
+ USER_HOME="${HOME:-$(eval echo "~$(id -un)")}"
40
+ LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
41
+ SHARE_DIR="$USER_HOME/Library/Trillboards/edge/share"
42
+ LA_DIR="$USER_HOME/Library/LaunchAgents"
43
+ LOCAL_BIN="$USER_HOME/.local/bin"
44
+ SERVE_LABEL="com.trillboards.detector"
45
+ SERVE_PORT="${TRILLBOARDS_DETECTOR_PORT:-9380}"
46
+ DETECTOR_PYTHON="${TRILLBOARDS_DETECTOR_PYTHON:-3.12}"
47
+ # The staged package source. `uv tool install <dir>` builds and installs it into
48
+ # its own venv; the directory is a few tens of KB (no model, no weights).
49
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo .)"
50
+ mkdir -p "$LOG_DIR" "$LA_DIR" 2>/dev/null || true
51
+ LOG="$LOG_DIR/detector-setup.log"
52
+ log() { printf '[trillboards setup-detector] %s %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOG" 2>&1; printf '[trillboards setup-detector] %s\n' "$*"; }
53
+
54
+ find_detector_source() {
55
+ for _c in "$SCRIPT_DIR/detector" \
56
+ "$SCRIPT_DIR/../detector" \
57
+ "$SCRIPT_DIR/../../detector"; do
58
+ [ -f "$_c/pyproject.toml" ] && { (cd "$_c" && pwd); return 0; }
59
+ done
60
+ return 1
61
+ }
62
+
63
+ # One provisioning process per user, and its OWN lock — never litert-lm's.
64
+ # Sharing a lock would make a detector repair block a Gemma repair, which is the
65
+ # component that can actually stop a customer sensing.
66
+ LOCK_DIR="$LOG_DIR/detector-setup.lock"
67
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
68
+ LOCK_PID="$(cat "$LOCK_DIR/pid" 2>/dev/null || echo '')"
69
+ if [ -n "$LOCK_PID" ] &&
70
+ kill -0 "$LOCK_PID" 2>/dev/null &&
71
+ ps -p "$LOCK_PID" -o command= 2>/dev/null | grep -q '[s]etup-detector.sh'; then
72
+ log "detector provisioning is already running (pid $LOCK_PID)"
73
+ exit 0
74
+ fi
75
+ rm -rf "$LOCK_DIR" 2>/dev/null || true
76
+ mkdir "$LOCK_DIR" 2>/dev/null || { log "detector provisioning is already starting in another process"; exit 0; }
77
+ fi
78
+ printf '%s\n' "$$" > "$LOCK_DIR/pid" 2>/dev/null || true
79
+ trap 'rm -rf "$LOCK_DIR" 2>/dev/null || true' EXIT
80
+ trap 'exit 130' INT
81
+ trap 'exit 143' TERM
82
+
83
+ log "starting detector provisioning"
84
+
85
+ # 0b. Enterprise TLS + proxy for uv — identical to setup-litertlm.sh, and for
86
+ # the identical reason. uv ships bundled Mozilla roots and does NOT consult the
87
+ # macOS keychain, nor does it read the system proxy. Both names are exported
88
+ # because an unknown env var is ignored while a CLI flag would fail outright.
89
+ export UV_SYSTEM_CERTS=1
90
+ export UV_NATIVE_TLS=1
91
+ if [ -z "${HTTPS_PROXY:-}" ] && [ -z "${https_proxy:-}" ] && [ -z "${ALL_PROXY:-}" ]; then
92
+ _proxy_conf="$(/usr/sbin/scutil --proxy 2>/dev/null || true)"
93
+ if [ "$(printf '%s\n' "$_proxy_conf" | awk '/HTTPSEnable/ {print $3}')" = "1" ]; then
94
+ _proxy_host="$(printf '%s\n' "$_proxy_conf" | awk '/HTTPSProxy/ {print $3}')"
95
+ _proxy_port="$(printf '%s\n' "$_proxy_conf" | awk '/HTTPSPort/ {print $3}')"
96
+ if [ -n "$_proxy_host" ]; then
97
+ HTTPS_PROXY="http://$_proxy_host${_proxy_port:+:$_proxy_port}"
98
+ export HTTPS_PROXY
99
+ export HTTP_PROXY="$HTTPS_PROXY"
100
+ log "using this machine's configured proxy for detector downloads: $HTTPS_PROXY"
101
+ fi
102
+ fi
103
+ fi
104
+
105
+ # 1. uv.
106
+ export PATH="$LOCAL_BIN:$USER_HOME/.cargo/bin:$PATH"
107
+ if ! command -v uv >/dev/null 2>&1; then
108
+ log "uv not found — installing via astral.sh standalone installer (no sudo)"
109
+ curl -LsSf https://astral.sh/uv/install.sh | sh >>"$LOG" 2>&1 || log "WARN: uv install failed"
110
+ export PATH="$LOCAL_BIN:$USER_HOME/.local/share/uv/bin:$PATH"
111
+ fi
112
+ UV="$(command -v uv || echo "$LOCAL_BIN/uv")"
113
+ if ! command -v "$UV" >/dev/null 2>&1 && [ ! -x "$UV" ]; then
114
+ log "ERROR: uv unavailable; the detector cascade stays off and every frame goes to the VLM"
115
+ exit 0
116
+ fi
117
+
118
+ # 2. The tool. Reinstalled unconditionally: the source is a few tens of KB, the
119
+ # dependency versions are PINNED in pyproject.toml, and uv skips work it has
120
+ # already done — so `--force` costs nothing on a healthy machine and repairs a
121
+ # drifted one. Gating it behind a health check is how a repaired-and-healthy box
122
+ # keeps a broken venv while reporting fine (setup-litertlm.sh's shim carries the
123
+ # same note, from the same mistake).
124
+ SOURCE="$(find_detector_source)" || { log "ERROR: detector package source not found next to this script"; exit 0; }
125
+ log "installing/repairing trillboards-detector from $SOURCE with uv-managed Python $DETECTOR_PYTHON"
126
+ if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force "$SOURCE" >>"$LOG" 2>&1; then
127
+ log "ERROR: trillboards-detector install failed; the cascade stays off (the VLM still runs every frame)"
128
+ exit 0
129
+ fi
130
+ DETECTOR="$LOCAL_BIN/trillboards-detector"
131
+ [ -x "$DETECTOR" ] || DETECTOR="$(command -v trillboards-detector || echo "$DETECTOR")"
132
+ if [ ! -x "$DETECTOR" ]; then
133
+ log "ERROR: trillboards-detector binary missing after install"
134
+ exit 0
135
+ fi
136
+ log "trillboards-detector installed at $DETECTOR"
137
+
138
+ # 3. Render + bootstrap the LaunchAgent.
139
+ TEMPLATE="$SHARE_DIR/$SERVE_LABEL.plist"
140
+ TARGET="$LA_DIR/$SERVE_LABEL.plist"
141
+ [ -f "$TEMPLATE" ] || TEMPLATE="$SCRIPT_DIR/../$SERVE_LABEL.plist"
142
+ if [ ! -f "$TEMPLATE" ]; then
143
+ log "ERROR: detector LaunchAgent template missing"
144
+ exit 0
145
+ fi
146
+ sed -e "s#__DETECTOR__#$DETECTOR#g" \
147
+ -e "s#__PORT__#$SERVE_PORT#g" \
148
+ -e "s#__LOG_DIR__#$LOG_DIR#g" \
149
+ "$TEMPLATE" > "$TARGET" 2>/dev/null || { log "WARN: could not render the detector plist"; exit 0; }
150
+
151
+ SERVICE_DOMAIN="gui/$(id -u)"
152
+ SERVICE_TARGET="$SERVICE_DOMAIN/$SERVE_LABEL"
153
+ if launchctl print "$SERVICE_TARGET" >/dev/null 2>&1; then
154
+ log "stopping the existing detector LaunchAgent"
155
+ launchctl bootout "$SERVICE_TARGET" >>"$LOG" 2>&1 || true
156
+ for _i in $(seq 1 40); do launchctl print "$SERVICE_TARGET" >/dev/null 2>&1 || break; sleep 0.25; done
157
+ fi
158
+ _bootstrapped=0
159
+ for _attempt in $(seq 1 10); do
160
+ if launchctl bootstrap "$SERVICE_DOMAIN" "$TARGET" >>"$LOG" 2>&1; then _bootstrapped=1; break; fi
161
+ launchctl print "$SERVICE_TARGET" >/dev/null 2>&1 && { _bootstrapped=1; break; }
162
+ sleep 1
163
+ done
164
+ [ "$_bootstrapped" -eq 1 ] || { log "WARN: launchctl bootstrap failed for the detector agent"; exit 0; }
165
+ log "detector LaunchAgent bootstrapped on :$SERVE_PORT"
166
+
167
+ # 4. Does it ANSWER? Not "is it ready" — the model is OTA'd and is legitimately
168
+ # absent right now. `curl --fail` would treat the designed 503 as a failure, so
169
+ # read the status code instead and say which of the two states we are in.
170
+ HEALTH_CODE=""
171
+ for _attempt in $(seq 1 30); do
172
+ HEALTH_CODE="$(curl -s -o "$LOG_DIR/.detector-health.json" -w '%{http_code}' --max-time 3 \
173
+ "http://127.0.0.1:$SERVE_PORT/health" 2>/dev/null)"
174
+ [ -n "$HEALTH_CODE" ] && [ "$HEALTH_CODE" != "000" ] && break
175
+ sleep 1
176
+ done
177
+ case "$HEALTH_CODE" in
178
+ 200) log "detector is READY (model already present)" ;;
179
+ 503) log "detector is running and AWAITING ITS MODEL — the OTA will land it and the service picks it up without a restart" ;;
180
+ *) log "WARN: the detector service did not answer /health (code '${HEALTH_CODE:-none}'); every frame keeps going to the VLM" ;;
181
+ esac
182
+ rm -f "$LOG_DIR/.detector-health.json" 2>/dev/null || true
183
+
184
+ log "done"
185
+ exit 0
@@ -53,6 +53,36 @@ sudo -u trillboards HOME=/opt/trillboards bash \
53
53
  *** itself as 'waiting for the on-device model' until it does — it will NOT
54
54
  *** fall back to paid cloud inference. Re-run install.sh to retry."
55
55
 
56
+ # The on-device OBJECT DETECTOR cascade gate (RF-DETR, 127.0.0.1:9380). Its own
57
+ # uv tool, its own venv, its own unit — never a package added into litert-lm's,
58
+ # which `uv tool install --force` rebuilds from scratch on any drift.
59
+ #
60
+ # BEST EFFORT, unlike the block above. Gemma is required because without it the
61
+ # connector cannot sense at all; the detector removes ~84.7% of VLM calls for
62
+ # ~2.1% of events, and without it every frame simply goes to the VLM as it does
63
+ # today. The 124 MB ONNX is NOT fetched here — it arrives over the air on the
64
+ # same channel as Gemma's weights, and the service starts, reports itself
65
+ # unhealthy, and picks the artifact up when it lands without a restart.
66
+ echo "Provisioning the on-device object detector (cascade gate; optional)..."
67
+ sudo cp node_modules/@trillboards/edge-sdk/deploy/systemd/trillboards-detector.service /etc/systemd/system/
68
+ sudo systemctl daemon-reload
69
+ sudo -u trillboards HOME=/opt/trillboards bash \
70
+ node_modules/@trillboards/edge-sdk/deploy/systemd/setup-detector.sh \
71
+ || echo " *** Detector provisioning did not finish. Sensing is unaffected — every
72
+ *** frame goes to the on-device VLM exactly as it does without the detector.
73
+ *** Re-run install.sh to retry."
74
+ # `enable --now`, AFTER the provisioner, and this ORDER is the fix.
75
+ #
76
+ # `systemctl enable` only enables — it does NOT start — and the provisioner above
77
+ # runs as the unprivileged `trillboards` service user (nologin, no sudoers
78
+ # entry), so it cannot start the unit either. Enabling before it and never
79
+ # starting left a fresh Linux box with no detector until its next reboot, with
80
+ # the installer reporting success. `--now` starts it here, where we have root,
81
+ # and after the provisioner so the binary the unit points at already exists.
82
+ sudo systemctl enable --now trillboards-detector \
83
+ || echo " *** The detector unit did not start. Sensing is unaffected. Check:
84
+ *** sudo systemctl status trillboards-detector"
85
+
56
86
  # Pair with a claim code from the portal (Add a camera -> Install). Pairing writes
57
87
  # /etc/trillboards/config.json (device token + feeds) — the token is delivered at
58
88
  # pair, NEVER baked. Accept the code as $1 for headless installs, else prompt.
@@ -89,5 +119,10 @@ echo " 3. Logs: sudo journalctl -u trillboards-edge -f"
89
119
  echo " 4. Local AI: sudo systemctl status trillboards-litertlm"
90
120
  echo " (the connector runs ON-DEVICE only; if the serve is down it waits and"
91
121
  echo " reports 'waiting' in the portal rather than billing cloud inference)"
122
+ echo " 5. Detector: sudo systemctl status trillboards-detector"
123
+ echo " (a cascade gate in front of the VLM. Its model arrives over the air,"
124
+ echo " so 'awaiting its model' on a fresh install is expected, not a fault:"
125
+ echo " curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9380/health"
126
+ echo " answers 503 until the artifact lands and 200 after)"
92
127
  echo " 4. Status: sudo systemctl status trillboards-edge"
93
128
  echo ""
@@ -0,0 +1,130 @@
1
+ #!/bin/bash
2
+ #
3
+ # setup-detector.sh (Linux) — provision the on-device OBJECT DETECTOR service
4
+ # (RF-DETR Medium, fp32 ONNX, 127.0.0.1:9380).
5
+ #
6
+ # Idempotent and safe to re-run. Run as the `trillboards` service user:
7
+ # sudo -u trillboards HOME=/opt/trillboards bash setup-detector.sh
8
+ #
9
+ # It installs the tool and starts the unit. It does NOT fetch the model — the
10
+ # 124 MB ONNX is OTA'd by edge-core's ModelManager, the same channel Gemma's
11
+ # weights use — and it does NOT install the unit file (install.sh has the sudo).
12
+ #
13
+ # BEST EFFORT THROUGHOUT. Every failure path exits 0 with a logged reason,
14
+ # because a missing detector means every frame goes to the VLM exactly as it
15
+ # does today. Only the litert-lm provisioner may be fatal, and only because
16
+ # without Gemma the connector cannot sense at all.
17
+
18
+ set -u
19
+
20
+ USER_HOME="${HOME:-/opt/trillboards}"
21
+ LOG_DIR="${TRILLBOARDS_LOG_DIR:-/var/log/trillboards}"
22
+ LOCAL_BIN="$USER_HOME/.local/bin"
23
+ SERVE_PORT="${TRILLBOARDS_DETECTOR_PORT:-9380}"
24
+ SERVE_UNIT="trillboards-detector"
25
+ DETECTOR_PYTHON="${TRILLBOARDS_DETECTOR_PYTHON:-3.12}"
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo .)"
27
+
28
+ mkdir -p "$LOG_DIR" 2>/dev/null || LOG_DIR="$USER_HOME/.trillboards-logs"
29
+ mkdir -p "$LOG_DIR" 2>/dev/null || true
30
+ LOG="$LOG_DIR/detector-setup.log"
31
+ log() { printf '[trillboards setup-detector] %s %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOG" 2>&1; printf '[trillboards setup-detector] %s\n' "$*"; }
32
+
33
+ find_detector_source() {
34
+ for _c in "$SCRIPT_DIR/detector" \
35
+ "$SCRIPT_DIR/../detector" \
36
+ "$SCRIPT_DIR/../../detector"; do
37
+ [ -f "$_c/pyproject.toml" ] && { (cd "$_c" && pwd); return 0; }
38
+ done
39
+ return 1
40
+ }
41
+
42
+ log "starting detector provisioning"
43
+
44
+ # Enterprise TLS + proxy for uv. Same two names as the litert-lm provisioner,
45
+ # for the same reason: uv ships bundled Mozilla roots and reads proxy settings
46
+ # only from the environment, so behind a TLS-inspecting proxy this is the step
47
+ # that fails and it fails naming the wrong component.
48
+ export UV_SYSTEM_CERTS=1
49
+ export UV_NATIVE_TLS=1
50
+
51
+ export PATH="$LOCAL_BIN:$USER_HOME/.cargo/bin:$PATH"
52
+ if ! command -v uv >/dev/null 2>&1; then
53
+ log "uv not found - installing via astral.sh standalone installer (no sudo)"
54
+ curl -LsSf https://astral.sh/uv/install.sh | sh >>"$LOG" 2>&1 || log "WARN: uv install failed"
55
+ export PATH="$LOCAL_BIN:$USER_HOME/.local/share/uv/bin:$PATH"
56
+ fi
57
+ UV="$(command -v uv || echo "$LOCAL_BIN/uv")"
58
+ if ! command -v "$UV" >/dev/null 2>&1 && [ ! -x "$UV" ]; then
59
+ log "ERROR: uv unavailable; the detector cascade stays off and every frame goes to the VLM"
60
+ exit 0
61
+ fi
62
+
63
+ SOURCE="$(find_detector_source)" || { log "ERROR: detector package source not found near $SCRIPT_DIR"; exit 0; }
64
+
65
+ # ITS OWN TOOL NAME, so uv gives it its own venv. Never a package added into
66
+ # litert-lm's: `uv tool install --force` rebuilds that venv from scratch on any
67
+ # version drift or health failure and would take the addition with it.
68
+ log "installing/repairing trillboards-detector from $SOURCE with uv-managed Python $DETECTOR_PYTHON"
69
+ if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force "$SOURCE" >>"$LOG" 2>&1; then
70
+ log "ERROR: trillboards-detector install failed; the cascade stays off"
71
+ exit 0
72
+ fi
73
+ DETECTOR="$LOCAL_BIN/trillboards-detector"
74
+ [ -x "$DETECTOR" ] || DETECTOR="$(command -v trillboards-detector || echo "$DETECTOR")"
75
+ [ -x "$DETECTOR" ] || { log "ERROR: trillboards-detector binary missing after install"; exit 0; }
76
+ log "trillboards-detector installed at $DETECTOR"
77
+
78
+ # The unit is installed AND STARTED by install.sh, which has root. This script
79
+ # runs as the unprivileged `trillboards` service user (nologin, no sudoers
80
+ # entry), so on a fresh install BOTH attempts below fail by design.
81
+ #
82
+ # It still tries, because a re-run by an operator who DOES have privileges is a
83
+ # real recovery path. What it must not do is pretend: it returns non-zero when
84
+ # it could not act, and the caller then skips the health probe entirely rather
85
+ # than waiting 30 s to report a service nobody started as "not answering".
86
+ # `systemctl enable` only enables — it does not start — so before this, a fresh
87
+ # Linux box had no detector until its next reboot and the installer said
88
+ # nothing. Caught by codex on this PR.
89
+ restart_detector_service() {
90
+ command -v systemctl >/dev/null 2>&1 || return 1
91
+ if ! systemctl list-unit-files "$SERVE_UNIT.service" >/dev/null 2>&1; then
92
+ log "WARN: $SERVE_UNIT.service is not installed yet (install.sh installs it)"
93
+ return 1
94
+ fi
95
+ systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 && return 0
96
+ sudo -n systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 && return 0
97
+ return 1
98
+ }
99
+
100
+ # Does it ANSWER? Not "is it ready" - the model is OTA'd and is legitimately
101
+ # absent right now, so /health is 503 by design and `curl --fail` would call a
102
+ # correct install broken. Read the code and name which state we are in, so an
103
+ # operator can tell "waiting for the model" from "did not start" - the exact
104
+ # pair litert-lm's /v1/models check cannot distinguish.
105
+ #
106
+ # ONLY WHEN WE COULD ACTUALLY START IT. Probing a service this account has no
107
+ # permission to start would spend 30 s to report "did not answer" about a
108
+ # service nobody has started yet, which reads as a broken install and is not
109
+ # one. install.sh owns the start, with root, right after this script returns.
110
+ if ! restart_detector_service; then
111
+ log "tool installed; this account cannot start $SERVE_UNIT - the installer starts it with root"
112
+ log "done"
113
+ exit 0
114
+ fi
115
+
116
+ HEALTH_CODE=""
117
+ for _attempt in $(seq 1 30); do
118
+ HEALTH_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
119
+ "http://127.0.0.1:$SERVE_PORT/health" 2>/dev/null)"
120
+ [ -n "$HEALTH_CODE" ] && [ "$HEALTH_CODE" != "000" ] && break
121
+ sleep 1
122
+ done
123
+ case "$HEALTH_CODE" in
124
+ 200) log "detector is READY (model already present)" ;;
125
+ 503) log "detector is running and AWAITING ITS MODEL - the OTA will land it and the service picks it up without a restart" ;;
126
+ *) log "WARN: the detector service did not answer /health (code '${HEALTH_CODE:-none}'); every frame keeps going to the VLM" ;;
127
+ esac
128
+
129
+ log "done"
130
+ exit 0
@@ -0,0 +1,54 @@
1
+ [Unit]
2
+ Description=Trillboards on-device object detector (RF-DETR cascade gate for the BYO-CCTV connector)
3
+ Documentation=https://github.com/trillboards/packages
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+ # NOT Before=trillboards-edge.service, and not Requires= either.
7
+ #
8
+ # The litert-lm unit orders itself before the connector because without Gemma
9
+ # the connector cannot sense at all. This one gates a COST: with it, ~84.7% of
10
+ # VLM calls are removed for ~2.1% of events; without it every frame goes to the
11
+ # VLM exactly as it does today. Ordering the connector behind a pure
12
+ # optimisation would let a slow detector start delay sensing, which is a strictly
13
+ # worse trade than the one it exists to make.
14
+
15
+ [Service]
16
+ Type=simple
17
+ User=trillboards
18
+ Group=trillboards
19
+ WorkingDirectory=/opt/trillboards
20
+ # Path is written by setup-detector.sh; the tool lands in the trillboards user's
21
+ # home-domain uv bin, never system-wide (no sudo pip, no root-owned venv).
22
+ ExecStart=/opt/trillboards/.local/bin/trillboards-detector serve --host 127.0.0.1 --port 9380
23
+ Restart=always
24
+ RestartSec=10
25
+ StandardOutput=journal
26
+ StandardError=journal
27
+ SyslogIdentifier=trillboards-detector
28
+
29
+ # Security hardening. TIGHTER than the litert-lm unit: this process needs no GPU
30
+ # render nodes (`docs/runbooks/cctv-detector-tier.md` §6 — the DirectML/ROCm path
31
+ # was never exercised and the decision is CPU-only), and it needs no model store
32
+ # of its own beyond the connector's data dir.
33
+ NoNewPrivileges=true
34
+ ProtectSystem=strict
35
+ ProtectHome=false
36
+ ReadWritePaths=/opt/trillboards
37
+ PrivateTmp=true
38
+ PrivateDevices=true
39
+
40
+ # Bind ONLY to loopback (see ExecStart --host). `LocalDetectorClient` talks to
41
+ # 127.0.0.1:9380; nothing off-box has any business reaching this.
42
+ IPAddressAllow=localhost
43
+ IPAddressDeny=any
44
+
45
+ # HOME decides where the model artifact is read from: setup-detector.sh and
46
+ # `modelpath.py` both resolve ${TRILLBOARDS_DATA_DIR:-$HOME/.trillboards}/models,
47
+ # matching config.ts's `DEFAULTS.dataDir`. If this and the connector's HOME ever
48
+ # disagree, the detector is permanently unhealthy on a box whose artifact is
49
+ # sitting right there.
50
+ Environment=HOME=/opt/trillboards
51
+ EnvironmentFile=-/etc/trillboards/env
52
+
53
+ [Install]
54
+ WantedBy=multi-user.target
@@ -227,6 +227,44 @@ Copy-Item -Force "$Here\setup-litertlm.ps1" "$Payload\bin\setup-litertlm
227
227
  # provisioning time; nothing else reads it, and no upstream file is modified.
228
228
  Copy-Item -Force "$Here\..\litertlm\trillboards_litertlm_shim.py" "$Payload\bin\trillboards_litertlm_shim.py"
229
229
  Copy-Item -Force "$Here\register-litertlm-task.ps1" "$Payload\bin\register-litertlm-task.ps1"
230
+
231
+ # On-device OBJECT DETECTOR (RF-DETR cascade gate, :9380). Its own uv tool, its
232
+ # own venv, its own Scheduled Task -- never a package added into litert-lm's,
233
+ # which `uv tool install --force` rebuilds from scratch on any drift.
234
+ #
235
+ # THE EXACT STAGING SHAPE BELOW IS LOAD-BEARING, character for character.
236
+ # ci\list-shipped-scripts.ps1 REGEX-SCRAPES this file to derive the Authenticode
237
+ # signing set: a literal Copy-Item -Force, a "$Here\..." source with no inner
238
+ # backslash, and a "$Payload\bin\..." destination. A Join-Path, a loop, or a
239
+ # `..\` in the source is invisible to it and the script ships UNSIGNED -- which
240
+ # then fails apply-update.ps1's Assert-PayloadSignatures in the FIELD, not just
241
+ # at install. (And do not write that pattern out in a comment: the scrape is a
242
+ # regex over the whole file, so a comment naming it enrols a file that does not
243
+ # exist. This paragraph is deliberately phrased so it cannot match.)
244
+ Copy-Item -Force "$Here\setup-detector.ps1" "$Payload\bin\setup-detector.ps1"
245
+ Copy-Item -Force "$Here\register-detector-task.ps1" "$Payload\bin\register-detector-task.ps1"
246
+ # The Python package SOURCE that `uv tool install` builds into its own venv.
247
+ # Tens of KB; the 124 MB fp32 ONNX it runs is OTA'd, never bundled. Not matched
248
+ # by the signing derivation above (the `..\` forbids it, and .py is not a
249
+ # signable kind) -- the same treatment trillboards_litertlm_shim.py gets.
250
+ New-Item -ItemType Directory -Force -Path "$Payload\bin\detector" | Out-Null
251
+ # `build`/`dist`/`*.egg-info` are excluded for a REASON: setuptools copies from
252
+ # `build\lib\` on a later build, so a stale one staged here would install the
253
+ # PREVIOUS revision of the service behind a green installer. The package uses
254
+ # hatchling so it never creates one; this covers a leftover from an older
255
+ # backend or a hand-run build.
256
+ #
257
+ # ALL FOUR ARE DIRECTORIES, SO ALL FOUR GO IN /XD. `*.egg-info` sat in /XF for
258
+ # one commit, where it matched nothing at all -- /XF excludes FILES -- so the
259
+ # comment above described an exclusion that was not happening. An exclude that
260
+ # silently matches nothing is worse than no exclude, because the comment stops
261
+ # anyone checking.
262
+ robocopy "$Here\..\detector" "$Payload\bin\detector" /E /NFL /NDL /NJH /NJS /NP `
263
+ /XD "__pycache__" "bench" "build" "dist" "*.egg-info" /XF "*.pyc" | Out-Null
264
+ if ($LASTEXITCODE -ge 8) { Die "failed to stage the detector package source (robocopy $LASTEXITCODE)" }
265
+ $global:LASTEXITCODE = 0
266
+ if (-not (Test-Path "$Payload\bin\detector\pyproject.toml")) { Die "detector package source did not stage" }
267
+
230
268
  Copy-Item -Force "$Here\check-feeds.js" "$Payload\bin\check-feeds.js"
231
269
 
232
270
  # ─── 7. Smoke-test the staged bundle BEFORE packaging ──────────────────────────
@@ -27,6 +27,7 @@
27
27
  !define APP_NAME "Sense Anything Connector"
28
28
  !define TASK_NAME "TrillboardsEdge"
29
29
  !define LITERT_TASK_NAME "TrillboardsLiteRtLm"
30
+ !define DETECTOR_TASK_NAME "TrillboardsDetector"
30
31
  !define MUI_ICON "${APP_ICON}"
31
32
  !define MUI_UNICON "${APP_ICON}"
32
33
 
@@ -203,6 +204,26 @@ Section "Install"
203
204
  ${EndIf}
204
205
  DetailPrint "Private local AI is installed and healthy."
205
206
 
207
+ ; -- On-device object detector (cascade gate, :9380) -----------------------
208
+ ; BEST EFFORT, and deliberately NOT an Abort like the block above.
209
+ ;
210
+ ; Gemma is required: without it the connector cannot sense at all. The
211
+ ; detector is a gate in FRONT of it -- it removes ~84.7% of VLM calls for
212
+ ; ~2.1% of events -- so a machine without one senses exactly as it does today,
213
+ ; only more expensively. A cost optimisation must never block camera pairing.
214
+ ;
215
+ ; It also cannot be verified here the way Gemma is: its 124 MB ONNX is OTA'd
216
+ ; (bundling it would break the code-only size budget), so right now the
217
+ ; artifact is legitimately absent and the service correctly reports itself
218
+ ; unhealthy. setup-detector.ps1 checks that it ANSWERS and logs which of the
219
+ ; two states it is in.
220
+ DetailPrint "Installing the on-device object detector..."
221
+ nsExec::ExecToLog '"$WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bin\setup-detector.ps1"'
222
+ Pop $0
223
+ ${If} $0 != 0
224
+ DetailPrint "The object detector could not be installed; sensing is unaffected (every frame goes to the on-device VLM)."
225
+ ${EndIf}
226
+
206
227
  ; -- Unattended provisioning ----------------------------------------------
207
228
  ; With /ENROLLMENT_TOKEN=, the machine provisions ITSELF here - no code typed
208
229
  ; at the machine, no browser, no human. enroll-connector.ps1 writes the
@@ -252,12 +273,14 @@ Section "Install"
252
273
  SectionEnd
253
274
 
254
275
  Section "Uninstall"
255
- ; Stop + remove the background tasks (connector + litert-lm serve).
276
+ ; Stop + remove the background tasks (connector + litert-lm serve + detector).
256
277
  nsExec::ExecToLog 'powershell -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bin\stop-existing.ps1" -InstallDir "$INSTDIR"'
257
278
  nsExec::ExecToLog 'schtasks /End /TN "${TASK_NAME}"'
258
279
  nsExec::ExecToLog 'schtasks /Delete /F /TN "${TASK_NAME}"'
259
280
  nsExec::ExecToLog 'schtasks /End /TN "${LITERT_TASK_NAME}"'
260
281
  nsExec::ExecToLog 'schtasks /Delete /F /TN "${LITERT_TASK_NAME}"'
282
+ nsExec::ExecToLog 'schtasks /End /TN "${DETECTOR_TASK_NAME}"'
283
+ nsExec::ExecToLog 'schtasks /Delete /F /TN "${DETECTOR_TASK_NAME}"'
261
284
  ; Explicit, ahead of RMDir /r: a fleet enrollment credential must never
262
285
  ; survive an uninstall, and relying on the recursive delete makes that a
263
286
  ; property of statement ORDER rather than of intent.