@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.
- package/deploy/detector/README.md +133 -0
- package/deploy/detector/bench/verify_against_reference.py +120 -0
- package/deploy/detector/pyproject.toml +49 -0
- package/deploy/detector/tests/__init__.py +0 -0
- package/deploy/detector/tests/test_modelpath.py +108 -0
- package/deploy/detector/tests/test_server.py +577 -0
- package/deploy/detector/tests/test_session.py +242 -0
- package/deploy/detector/trillboards_detector/__init__.py +8 -0
- package/deploy/detector/trillboards_detector/__main__.py +4 -0
- package/deploy/detector/trillboards_detector/cli.py +141 -0
- package/deploy/detector/trillboards_detector/errors.py +16 -0
- package/deploy/detector/trillboards_detector/labels.py +107 -0
- package/deploy/detector/trillboards_detector/loader.py +257 -0
- package/deploy/detector/trillboards_detector/modelpath.py +74 -0
- package/deploy/detector/trillboards_detector/server.py +460 -0
- package/deploy/detector/trillboards_detector/session.py +354 -0
- package/deploy/macos/build-macos-pkg.sh +48 -0
- package/deploy/macos/com.trillboards.detector.plist +61 -0
- package/deploy/macos/scripts/ensure-detector.sh +85 -0
- package/deploy/macos/scripts/postinstall +28 -0
- package/deploy/macos/scripts/setup-detector.sh +185 -0
- package/deploy/systemd/install.sh +35 -0
- package/deploy/systemd/setup-detector.sh +130 -0
- package/deploy/systemd/trillboards-detector.service +54 -0
- package/deploy/windows/build-windows.ps1 +38 -0
- package/deploy/windows/installer.nsi +24 -1
- package/deploy/windows/register-detector-task.ps1 +161 -0
- package/deploy/windows/register-task.ps1 +27 -0
- package/deploy/windows/setup-detector.ps1 +176 -0
- package/deploy/windows/stop-existing.ps1 +1 -0
- package/deploy/windows/trillboards-edge.cmd +25 -0
- package/dist/EdgeAgent.js +1 -1
- package/dist/LocalDetectorClient.d.ts +92 -0
- package/dist/LocalDetectorClient.d.ts.map +1 -0
- package/dist/LocalDetectorClient.js +241 -0
- package/dist/LocalDetectorClient.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/zoneContainment.d.ts +148 -0
- package/dist/zoneContainment.d.ts.map +1 -0
- package/dist/zoneContainment.js +172 -0
- package/dist/zoneContainment.js.map +1 -0
- 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.
|