@trillboards/edge-sdk 0.2.52 → 0.2.54
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/macos/README.md +22 -7
- package/deploy/macos/apply-update.sh +225 -0
- package/deploy/macos/bench-local-vlm.sh +140 -0
- package/deploy/macos/build-macos-pkg.sh +13 -10
- package/deploy/macos/scripts/postinstall +13 -3
- package/deploy/windows/README.md +7 -5
- package/deploy/windows/bundle-windows-installer.ps1 +1 -3
- package/deploy/windows/e2e/test-local-sense.ps1 +97 -6
- package/dist/EdgeAgent.js +1 -1
- package/dist/connectorUpdate.d.ts +23 -16
- package/dist/connectorUpdate.d.ts.map +1 -1
- package/dist/connectorUpdate.js +37 -10
- package/dist/connectorUpdate.js.map +1 -1
- package/dist/connectorUpdateApply.d.ts +3 -0
- package/dist/connectorUpdateApply.d.ts.map +1 -1
- package/dist/connectorUpdateApply.js +3 -0
- package/dist/connectorUpdateApply.js.map +1 -1
- package/dist/connectorUpdateApplyMac.d.ts +50 -0
- package/dist/connectorUpdateApplyMac.d.ts.map +1 -0
- package/dist/connectorUpdateApplyMac.js +166 -0
- package/dist/connectorUpdateApplyMac.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localVlmBench.d.ts +102 -0
- package/dist/localVlmBench.d.ts.map +1 -1
- package/dist/localVlmBench.js +129 -0
- package/dist/localVlmBench.js.map +1 -1
- package/package.json +1 -1
package/deploy/macos/README.md
CHANGED
|
@@ -19,6 +19,7 @@ What lands on disk (home-domain, **no sudo / no admin password**):
|
|
|
19
19
|
edge-sdk), WITHOUT the heavy local-inference stack
|
|
20
20
|
share/com.trillboards.edge.plist LaunchAgent template
|
|
21
21
|
share/ensure-litertlm.sh background local-AI retry supervisor
|
|
22
|
+
share/apply-update.sh signed-pkg updater with rollback
|
|
22
23
|
share/models/gemma-4-E2B-it.litertlm required customer local VLM (removed after import)
|
|
23
24
|
~/Applications/Trillboards CCTV Setup.app native customer setup window (opened automatically)
|
|
24
25
|
~/Library/Application Support/Trillboards/trillboards.config.json (after pair, 0600)
|
|
@@ -31,14 +32,15 @@ bootstrapped automatically on first successful pair. The device token is
|
|
|
31
32
|
delivered **only by pairing** — it is never baked into the `.pkg`.
|
|
32
33
|
|
|
33
34
|
> The customer build includes Gemma and completes its local health check before
|
|
34
|
-
> pairing. Cloud inference requires explicit configuration.
|
|
35
|
-
>
|
|
35
|
+
> pairing. Cloud inference requires explicit configuration. The release lane
|
|
36
|
+
> also builds a signed, notarized `--skip-model` package for automatic upgrades;
|
|
37
|
+
> it is accepted only on an already-provisioned machine whose model is healthy.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
39
41
|
## Install (operator)
|
|
40
42
|
|
|
41
|
-
1. Download `
|
|
43
|
+
1. Download `SenseAnythingConnector.pkg`.
|
|
42
44
|
2. Double-click it. The installer offers **Install for me only** (home-domain) —
|
|
43
45
|
no admin password. (Equivalent CLI: `installer -pkg <pkg> -target
|
|
44
46
|
CurrentUserHomeDirectory`.)
|
|
@@ -73,7 +75,7 @@ launchctl kickstart -k "gui/$(id -u)/com.trillboards.edge"
|
|
|
73
75
|
cd trillboards-edge-sdk
|
|
74
76
|
npm install # ensures ffmpeg-static is fetched
|
|
75
77
|
cd packages/edge-sdk/deploy/macos
|
|
76
|
-
./build-macos-pkg.sh # → dist-installer/
|
|
78
|
+
./build-macos-pkg.sh # → dist-installer/SenseAnythingConnector.pkg
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
Options:
|
|
@@ -83,13 +85,26 @@ Options:
|
|
|
83
85
|
| `--arch arm64\|x64` | host arch | target architecture |
|
|
84
86
|
| `--node-version vX.Y.Z` | `v22.22.0` | pinned Node runtime to bundle |
|
|
85
87
|
| `--bundle-model` | on | include the pinned ~2.4 GB Gemma model required by customer setup |
|
|
86
|
-
| `--skip-model` | off | build
|
|
88
|
+
| `--skip-model` | off | build the slim code-only upgrade package; it is not a valid first installer |
|
|
87
89
|
| `--offline` | off | skip the official Node download; copy the host `node` |
|
|
88
90
|
| `--sign-installer "Developer ID Installer: …"` | unset | `productsign` the pkg |
|
|
89
91
|
|
|
90
92
|
The build is idempotent (it wipes its staging tree each run) and **smoke-tests
|
|
91
93
|
the staged bundle** (`trillboards-edge version`) before packaging.
|
|
92
94
|
|
|
95
|
+
### Automatic updates
|
|
96
|
+
|
|
97
|
+
Every paired connector checks the macOS version manifest at launch and every
|
|
98
|
+
six hours. Once a release has baked for six hours, the connector downloads the
|
|
99
|
+
content-addressed code-only package while sensing remains online. Before it can
|
|
100
|
+
be staged, macOS must validate the exact Aethercorp Developer ID Installer
|
|
101
|
+
identity, the notarization staple, Gatekeeper acceptance, size, and SHA-256.
|
|
102
|
+
The connector waits for a safe sensing point, exits cleanly, installs the
|
|
103
|
+
package, smoke-tests the new version, and relaunches the LaunchAgent. The old
|
|
104
|
+
tree remains as `~/Library/Trillboards/edge.previous` and is restored
|
|
105
|
+
automatically if installation, smoke testing, or relaunch fails. Existing
|
|
106
|
+
pairing and the local model are preserved; the setup window does not reopen.
|
|
107
|
+
|
|
93
108
|
### Local end-to-end test
|
|
94
109
|
|
|
95
110
|
```bash
|
|
@@ -121,9 +136,9 @@ To ship a Gatekeeper-clean installer, on a machine that has the
|
|
|
121
136
|
# (or: export MAC_INSTALLER_IDENTITY="Developer ID Installer: …" && ./build-macos-pkg.sh)
|
|
122
137
|
|
|
123
138
|
# 2. Notarize + staple
|
|
124
|
-
xcrun notarytool submit dist-installer/
|
|
139
|
+
xcrun notarytool submit dist-installer/SenseAnythingConnector.pkg \
|
|
125
140
|
--keychain-profile <notary-profile> --wait
|
|
126
|
-
xcrun stapler staple dist-installer/
|
|
141
|
+
xcrun stapler staple dist-installer/SenseAnythingConnector.pkg
|
|
127
142
|
```
|
|
128
143
|
|
|
129
144
|
The bundled `bin/node` and `bin/ffmpeg` are third-party Mach-O binaries; for full
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Stage and apply the signed, notarized macOS connector update package.
|
|
4
|
+
# The running connector chooses the safe point; this detached helper owns the
|
|
5
|
+
# download, macOS trust checks, LaunchAgent handoff, swap, smoke test, and
|
|
6
|
+
# `.previous` rollback.
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
LABEL='com.trillboards.edge'
|
|
11
|
+
EXPECTED_SIGNER='Developer ID Installer: Aethercorp Inc (78JQ7882U9)'
|
|
12
|
+
MODE=''
|
|
13
|
+
INSTALL_DIR=''
|
|
14
|
+
UPDATE_URL=''
|
|
15
|
+
EXPECTED_VERSION=''
|
|
16
|
+
CURRENT_VERSION=''
|
|
17
|
+
EXPECTED_SHA256=''
|
|
18
|
+
EXPECTED_SIZE=''
|
|
19
|
+
WAIT_PID=''
|
|
20
|
+
|
|
21
|
+
while [ "$#" -gt 0 ]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--stage|--commit) MODE="$1"; shift ;;
|
|
24
|
+
--install-dir) INSTALL_DIR="${2:-}"; shift 2 ;;
|
|
25
|
+
--update-url) UPDATE_URL="${2:-}"; shift 2 ;;
|
|
26
|
+
--expected-version) EXPECTED_VERSION="${2:-}"; shift 2 ;;
|
|
27
|
+
--current-version) CURRENT_VERSION="${2:-}"; shift 2 ;;
|
|
28
|
+
--expected-sha256) EXPECTED_SHA256="${2:-}"; shift 2 ;;
|
|
29
|
+
--expected-size) EXPECTED_SIZE="${2:-}"; shift 2 ;;
|
|
30
|
+
--wait-pid) WAIT_PID="${2:-}"; shift 2 ;;
|
|
31
|
+
*) printf '[apply-update] unknown argument: %s\n' "$1" >&2; exit 2 ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
log() {
|
|
36
|
+
/usr/bin/logger -t com.trillboards.edge "apply-update: $*" 2>/dev/null || true
|
|
37
|
+
printf '[apply-update] %s\n' "$*"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "$EXPECTED_VERSION" in
|
|
41
|
+
[0-9]*.[0-9]*.[0-9]*) ;;
|
|
42
|
+
*) log 'invalid expected version'; exit 2 ;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
# The updater is intentionally scoped to the one home-domain path the package
|
|
46
|
+
# installs. This makes every later rm/mv target resolved and bounded.
|
|
47
|
+
EXPECTED_INSTALL_DIR="${HOME}/Library/Trillboards/edge"
|
|
48
|
+
if [ "$INSTALL_DIR" != "$EXPECTED_INSTALL_DIR" ]; then
|
|
49
|
+
log "refusing unexpected install root: $INSTALL_DIR"
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
PARENT_DIR="$(dirname "$INSTALL_DIR")"
|
|
54
|
+
UPDATE_DIR="$PARENT_DIR/.edge-auto-update"
|
|
55
|
+
ARCHIVE="$UPDATE_DIR/connector-update.pkg"
|
|
56
|
+
MARKER="$UPDATE_DIR/staged.json"
|
|
57
|
+
OUTCOME="$UPDATE_DIR/outcome.json"
|
|
58
|
+
LOCK_DIR="$UPDATE_DIR/lock"
|
|
59
|
+
PREVIOUS="$PARENT_DIR/edge.previous"
|
|
60
|
+
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
61
|
+
|
|
62
|
+
mkdir -p "$UPDATE_DIR" || exit 1
|
|
63
|
+
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
64
|
+
log 'another update transaction is already running'
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
|
|
68
|
+
|
|
69
|
+
json_escape() {
|
|
70
|
+
printf '%s' "$1" | /usr/bin/sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
write_outcome() {
|
|
74
|
+
local result="$1" detail="$2" tmp="$OUTCOME.tmp.$$"
|
|
75
|
+
printf '{"attemptedAt":"%s","result":"%s","detail":"%s"}\n' \
|
|
76
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$result" "$(json_escape "$detail")" > "$tmp" \
|
|
77
|
+
&& mv -f "$tmp" "$OUTCOME"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fail_stage() {
|
|
81
|
+
log "$1"
|
|
82
|
+
rm -f "$ARCHIVE.download" "$MARKER.tmp.$$"
|
|
83
|
+
exit 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
configure_proxy() {
|
|
87
|
+
if [ -n "${HTTPS_PROXY:-}" ] || [ -n "${https_proxy:-}" ] || [ -n "${ALL_PROXY:-}" ]; then
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
local proxy_conf enabled host port
|
|
91
|
+
proxy_conf="$(/usr/sbin/scutil --proxy 2>/dev/null || true)"
|
|
92
|
+
enabled="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSEnable/ {print $3}')"
|
|
93
|
+
[ "$enabled" = '1' ] || return
|
|
94
|
+
host="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSProxy/ {print $3}')"
|
|
95
|
+
port="$(printf '%s\n' "$proxy_conf" | /usr/bin/awk '/HTTPSPort/ {print $3}')"
|
|
96
|
+
[ -n "$host" ] || return
|
|
97
|
+
HTTPS_PROXY="http://$host${port:+:$port}"
|
|
98
|
+
export HTTPS_PROXY HTTP_PROXY="$HTTPS_PROXY"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
verify_pkg() {
|
|
102
|
+
local pkg="$1" sha="$2" size="$3" actual_size actual_sha sig_out
|
|
103
|
+
[ -f "$pkg" ] || return 1
|
|
104
|
+
case "$size" in *[!0-9]*|'') return 1 ;; esac
|
|
105
|
+
printf '%s' "$sha" | /usr/bin/grep -Eq '^[0-9a-f]{64}$' || return 1
|
|
106
|
+
|
|
107
|
+
actual_size="$(/usr/bin/wc -c < "$pkg" | tr -d ' ')"
|
|
108
|
+
[ "$actual_size" = "$size" ] || { log "payload size mismatch: expected $size, got $actual_size"; return 1; }
|
|
109
|
+
actual_sha="$(/usr/bin/shasum -a 256 "$pkg" | /usr/bin/awk '{print $1}')"
|
|
110
|
+
[ "$actual_sha" = "$sha" ] || { log 'payload SHA-256 mismatch'; return 1; }
|
|
111
|
+
|
|
112
|
+
sig_out="$UPDATE_DIR/signature.txt"
|
|
113
|
+
/usr/sbin/pkgutil --check-signature "$pkg" > "$sig_out" 2>&1 || { log 'package signature is invalid'; return 1; }
|
|
114
|
+
/usr/bin/grep -Fq "$EXPECTED_SIGNER" "$sig_out" || { log 'package signer is not Aethercorp'; return 1; }
|
|
115
|
+
/usr/bin/xcrun stapler validate "$pkg" >/dev/null 2>&1 || { log 'package has no valid notarization staple'; return 1; }
|
|
116
|
+
/usr/sbin/spctl -a -vv --type install "$pkg" >/dev/null 2>&1 || { log 'Gatekeeper rejected the update package'; return 1; }
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if [ "$MODE" = '--stage' ]; then
|
|
121
|
+
case "$UPDATE_URL" in https://*) ;; *) fail_stage 'update URL must use HTTPS' ;; esac
|
|
122
|
+
configure_proxy
|
|
123
|
+
log "downloading connector $EXPECTED_VERSION while sensing stays online"
|
|
124
|
+
/usr/bin/curl --fail --location --silent --show-error --proto '=https' --tlsv1.2 \
|
|
125
|
+
--connect-timeout 30 --max-time 3600 --output "$ARCHIVE.download" "$UPDATE_URL" \
|
|
126
|
+
|| fail_stage 'update download failed'
|
|
127
|
+
verify_pkg "$ARCHIVE.download" "$EXPECTED_SHA256" "$EXPECTED_SIZE" \
|
|
128
|
+
|| fail_stage 'update package verification failed'
|
|
129
|
+
mv -f "$ARCHIVE.download" "$ARCHIVE" || fail_stage 'could not retain verified package'
|
|
130
|
+
marker_tmp="$MARKER.tmp.$$"
|
|
131
|
+
printf '{"version":"%s","currentVersion":"%s","sha256":"%s","sizeBytes":%s}\n' \
|
|
132
|
+
"$EXPECTED_VERSION" "$CURRENT_VERSION" "$EXPECTED_SHA256" "$EXPECTED_SIZE" > "$marker_tmp" \
|
|
133
|
+
|| fail_stage 'could not write staged marker'
|
|
134
|
+
mv -f "$marker_tmp" "$MARKER" || fail_stage 'could not publish staged marker'
|
|
135
|
+
log "connector $EXPECTED_VERSION is staged, signed, notarized, and verified"
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [ "$MODE" != '--commit' ]; then
|
|
140
|
+
log 'exactly one of --stage or --commit is required'
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
marker_version="$(/usr/bin/plutil -extract version raw "$MARKER" 2>/dev/null || true)"
|
|
145
|
+
marker_sha="$(/usr/bin/plutil -extract sha256 raw "$MARKER" 2>/dev/null || true)"
|
|
146
|
+
marker_size="$(/usr/bin/plutil -extract sizeBytes raw "$MARKER" 2>/dev/null || true)"
|
|
147
|
+
if [ "$marker_version" != "$EXPECTED_VERSION" ]; then
|
|
148
|
+
write_outcome 'rejected' 'staged version did not match the requested version'
|
|
149
|
+
log 'staged version mismatch; refusing update'
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
verify_pkg "$ARCHIVE" "$marker_sha" "$marker_size" || {
|
|
153
|
+
write_outcome 'rejected' 'staged package failed commit-time verification'
|
|
154
|
+
rm -f "$MARKER" "$ARCHIVE"
|
|
155
|
+
exit 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# The connector launches us and then exits cleanly. Do not touch its mapped
|
|
159
|
+
# binaries until that PID is gone; a timeout leaves the current install intact.
|
|
160
|
+
if [ -n "$WAIT_PID" ]; then
|
|
161
|
+
waited=0
|
|
162
|
+
while kill -0 "$WAIT_PID" 2>/dev/null && [ "$waited" -lt 120 ]; do
|
|
163
|
+
sleep 1
|
|
164
|
+
waited=$((waited + 1))
|
|
165
|
+
done
|
|
166
|
+
if kill -0 "$WAIT_PID" 2>/dev/null; then
|
|
167
|
+
write_outcome 'failed' 'connector did not exit before the update deadline'
|
|
168
|
+
log 'connector did not exit; leaving current install untouched'
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
restart_agent() {
|
|
174
|
+
[ -f "$PLIST" ] || return 1
|
|
175
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
176
|
+
local attempt=1
|
|
177
|
+
while [ "$attempt" -le 10 ]; do
|
|
178
|
+
/bin/launchctl bootstrap "gui/$(id -u)" "$PLIST" >/dev/null 2>&1 || true
|
|
179
|
+
if /bin/launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
|
|
180
|
+
/bin/launchctl kickstart -k "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
sleep 1
|
|
184
|
+
attempt=$((attempt + 1))
|
|
185
|
+
done
|
|
186
|
+
return 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
rollback() {
|
|
190
|
+
local detail="$1"
|
|
191
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
192
|
+
rm -rf "$INSTALL_DIR"
|
|
193
|
+
if [ -d "$PREVIOUS" ]; then
|
|
194
|
+
mv "$PREVIOUS" "$INSTALL_DIR" || true
|
|
195
|
+
fi
|
|
196
|
+
if restart_agent; then
|
|
197
|
+
write_outcome 'failed' "$detail; previous version restored"
|
|
198
|
+
else
|
|
199
|
+
write_outcome 'failed' "$detail; previous files restored but the agent did not restart"
|
|
200
|
+
fi
|
|
201
|
+
rm -f "$MARKER" "$ARCHIVE"
|
|
202
|
+
log "$detail; rolled back"
|
|
203
|
+
exit 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log "applying connector $EXPECTED_VERSION"
|
|
207
|
+
/bin/launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
|
|
208
|
+
rm -rf "$PREVIOUS"
|
|
209
|
+
mv "$INSTALL_DIR" "$PREVIOUS" || {
|
|
210
|
+
write_outcome 'failed' 'could not preserve the current install for rollback'
|
|
211
|
+
exit 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/usr/sbin/installer -pkg "$ARCHIVE" -target CurrentUserHomeDirectory >/dev/null 2>&1 \
|
|
215
|
+
|| rollback 'macOS Installer rejected the update'
|
|
216
|
+
|
|
217
|
+
installed_version="$($INSTALL_DIR/bin/trillboards-edge version 2>/dev/null || true)"
|
|
218
|
+
printf '%s' "$installed_version" | /usr/bin/grep -Fq "edge-sdk v$EXPECTED_VERSION" \
|
|
219
|
+
|| rollback 'updated connector failed its version smoke test'
|
|
220
|
+
|
|
221
|
+
restart_agent || rollback 'updated connector installed but its LaunchAgent did not restart'
|
|
222
|
+
write_outcome 'success' "updated from $CURRENT_VERSION to $EXPECTED_VERSION"
|
|
223
|
+
rm -f "$MARKER" "$ARCHIVE" "$UPDATE_DIR/signature.txt"
|
|
224
|
+
log "connector $EXPECTED_VERSION installed and relaunched"
|
|
225
|
+
exit 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# bench-local-vlm.sh — macOS twin of `deploy/windows/e2e/test-local-sense.ps1
|
|
4
|
+
# -Mode bench`: provision the PINNED litert-lm + Gemma 4 E2B, start a real
|
|
5
|
+
# serve, and MEASURE this machine's per-frame cost.
|
|
6
|
+
#
|
|
7
|
+
# WHY IT EXISTS. Local-only inference makes the box spec the product spec, and
|
|
8
|
+
# every latency number in the capacity plan came off one Apple M4 Max
|
|
9
|
+
# workstation — including the 7.30 s/frame in connectorCadence.ts that sizes the
|
|
10
|
+
# whole 493-analyses/hour budget. A 3-core / 7 GB Apple silicon runner is the
|
|
11
|
+
# closest CI has to the Mac mini we would actually recommend, and it is the only
|
|
12
|
+
# cheap way to test the "16 GB+ RAM" minimum the activation runbook publishes.
|
|
13
|
+
#
|
|
14
|
+
# It does NOT use setup-litertlm.sh's LaunchAgent path: that bootstraps a
|
|
15
|
+
# per-login agent, and a CI runner's session is not the session a customer logs
|
|
16
|
+
# into. The runtime, the pin and the model are identical; only the supervisor
|
|
17
|
+
# differs, and the supervisor is not what is being measured.
|
|
18
|
+
#
|
|
19
|
+
# NOTE ON BASH 3.2. macOS still ships bash 3.2 as /bin/bash and that is what a
|
|
20
|
+
# GitHub macOS runner executes. Its identifier scan is NOT UTF-8 aware, so an
|
|
21
|
+
# expansion written `$VAR…` swallows the ellipsis bytes into the variable NAME
|
|
22
|
+
# and `set -u` then reports an unbound variable whose name is the real one with
|
|
23
|
+
# mojibake stapled to it — which reads as "the variable is unset" when it is set.
|
|
24
|
+
# Every expansion here is braced and every log line is ASCII for that reason.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# deploy/macos/bench-local-vlm.sh [--port 9379] [--arms a,b] [--frames f]
|
|
28
|
+
# [--rounds 1] [--budget-min 40]
|
|
29
|
+
# [--frame-timeout-min 20] [--out FILE]
|
|
30
|
+
# [--model <non-default>.litertlm]
|
|
31
|
+
#
|
|
32
|
+
# Exit 0 = a matrix was written (even a partial one — a slow box is a RESULT).
|
|
33
|
+
# Exit 1 = nothing was measured at all.
|
|
34
|
+
set -uo pipefail
|
|
35
|
+
|
|
36
|
+
PORT=9379
|
|
37
|
+
ARMS='cpu-sop,cpu-dooh-sop,gpu-sop'
|
|
38
|
+
FRAMES='kitchen-5-people'
|
|
39
|
+
ROUNDS=1
|
|
40
|
+
BUDGET_MIN=40
|
|
41
|
+
FRAME_TIMEOUT_MIN=20
|
|
42
|
+
OUT="${TMPDIR:-/tmp}/bench-local-vlm.json"
|
|
43
|
+
NOTE=''
|
|
44
|
+
# A NON-default .litertlm id, for the per-silicon A/B. Empty = the generic file
|
|
45
|
+
# both provisioners import everywhere.
|
|
46
|
+
MODEL=''
|
|
47
|
+
|
|
48
|
+
while [ $# -gt 0 ]; do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--port) PORT="$2"; shift 2 ;;
|
|
51
|
+
--arms) ARMS="$2"; shift 2 ;;
|
|
52
|
+
--frames) FRAMES="$2"; shift 2 ;;
|
|
53
|
+
--rounds) ROUNDS="$2"; shift 2 ;;
|
|
54
|
+
--budget-min) BUDGET_MIN="$2"; shift 2 ;;
|
|
55
|
+
--frame-timeout-min) FRAME_TIMEOUT_MIN="$2"; shift 2 ;;
|
|
56
|
+
--out) OUT="$2"; shift 2 ;;
|
|
57
|
+
--note) NOTE="$2"; shift 2 ;;
|
|
58
|
+
--model) MODEL="$2"; shift 2 ;;
|
|
59
|
+
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
60
|
+
esac
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # <edge-sdk>/deploy/macos
|
|
64
|
+
PKG_ROOT="$(cd "$HERE/../.." && pwd)" # <edge-sdk>
|
|
65
|
+
WORKSPACE_ROOT="$(cd "$PKG_ROOT/../.." && pwd)" # <repo>/trillboards-edge-sdk
|
|
66
|
+
[ -f "$PKG_ROOT/scripts/bench-local-vlm.mjs" ] || { echo "FAIL - PKG_ROOT wrong: $PKG_ROOT" >&2; exit 1; }
|
|
67
|
+
[ -f "$WORKSPACE_ROOT/package.json" ] || { echo "FAIL - WORKSPACE_ROOT wrong: $WORKSPACE_ROOT" >&2; exit 1; }
|
|
68
|
+
log() { echo "[macos-bench] $*"; }
|
|
69
|
+
die() { echo "FAIL - $*" >&2; exit 1; }
|
|
70
|
+
|
|
71
|
+
# The PINNED version, read from the provisioner rather than restated here — the
|
|
72
|
+
# litert-lm-version-pin guard already keeps that one declaration honest, and a
|
|
73
|
+
# second copy would be a second thing to forget.
|
|
74
|
+
LITERT_LM_VERSION="$(sed -n 's/^LITERT_LM_VERSION="\${LITERT_LM_VERSION:-\([^}]*\)}"/\1/p' "$HERE/scripts/setup-litertlm.sh" | head -1)"
|
|
75
|
+
[ -n "$LITERT_LM_VERSION" ] || die "could not read the pinned litert-lm version from setup-litertlm.sh"
|
|
76
|
+
log "pinned litert-lm: $LITERT_LM_VERSION"
|
|
77
|
+
|
|
78
|
+
log "building the connector (tsc, all workspaces in dependency order) ..."
|
|
79
|
+
( cd "$WORKSPACE_ROOT" && npm run build ) || die "npm run build failed"
|
|
80
|
+
|
|
81
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
82
|
+
log "installing uv (home-domain, no sudo) ..."
|
|
83
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh || die "uv install failed"
|
|
84
|
+
fi
|
|
85
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
86
|
+
|
|
87
|
+
log "installing litert-lm==${LITERT_LM_VERSION} ..."
|
|
88
|
+
uv tool install --python 3.12 --managed-python --force "litert-lm==$LITERT_LM_VERSION" \
|
|
89
|
+
|| die "litert-lm install failed"
|
|
90
|
+
LITERTLM="$HOME/.local/bin/litert-lm"
|
|
91
|
+
[ -x "$LITERTLM" ] || die "litert-lm not installed at $LITERTLM"
|
|
92
|
+
|
|
93
|
+
MODEL_ID='gemma-4-E2B-it.litertlm'
|
|
94
|
+
HF_REPO='litert-community/gemma-4-E2B-it-litert-lm'
|
|
95
|
+
if ! "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
|
|
96
|
+
# EXACT form setup-litertlm.sh uses. The positional argument is the FILE inside
|
|
97
|
+
# the repo, not the repo: passing the repo id alone gets
|
|
98
|
+
# "Source file not found: <repo>", because the bare form imports a LOCAL path.
|
|
99
|
+
log "importing ${MODEL_ID} from ${HF_REPO} (~2.4 GB) ..."
|
|
100
|
+
"$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" || die "model import failed"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
SERVE_LOG="${TMPDIR:-/tmp}/litertlm-serve-bench.log"
|
|
104
|
+
if curl -fsS --max-time 3 "http://127.0.0.1:$PORT/v1/models" >/dev/null 2>&1; then
|
|
105
|
+
log "a serve is already healthy on :$PORT"
|
|
106
|
+
SERVE_PID=''
|
|
107
|
+
else
|
|
108
|
+
log "starting litert-lm serve on 127.0.0.1:$PORT (log: $SERVE_LOG)"
|
|
109
|
+
"$LITERTLM" serve --host 127.0.0.1 --port "$PORT" --verbose > "$SERVE_LOG" 2>&1 &
|
|
110
|
+
SERVE_PID=$!
|
|
111
|
+
fi
|
|
112
|
+
cleanup() { [ -n "${SERVE_PID:-}" ] && kill "$SERVE_PID" 2>/dev/null; }
|
|
113
|
+
trap cleanup EXIT
|
|
114
|
+
|
|
115
|
+
HEALTHY=0
|
|
116
|
+
for _ in $(seq 1 120); do
|
|
117
|
+
if curl -fsS --max-time 3 "http://127.0.0.1:$PORT/v1/models" >/dev/null 2>&1; then HEALTHY=1; break; fi
|
|
118
|
+
[ -n "${SERVE_PID:-}" ] && ! kill -0 "$SERVE_PID" 2>/dev/null && break
|
|
119
|
+
sleep 5
|
|
120
|
+
done
|
|
121
|
+
[ "$HEALTHY" = "1" ] || { tail -40 "$SERVE_LOG" 2>/dev/null; die "litert-lm serve never became healthy on :$PORT"; }
|
|
122
|
+
log "litert-lm serve healthy on :$PORT"
|
|
123
|
+
|
|
124
|
+
log "running the hardware matrix — arms=$ARMS frames=$FRAMES rounds=$ROUNDS budget=${BUDGET_MIN}m"
|
|
125
|
+
node "$PKG_ROOT/scripts/bench-local-vlm.mjs" \
|
|
126
|
+
--litert "http://127.0.0.1:$PORT" \
|
|
127
|
+
--arms "$ARMS" --frames "$FRAMES" --rounds "$ROUNDS" \
|
|
128
|
+
--budget-ms "$((BUDGET_MIN * 60000))" \
|
|
129
|
+
--timeout-ms "$((FRAME_TIMEOUT_MIN * 60000))" \
|
|
130
|
+
--machine-note "$NOTE" \
|
|
131
|
+
${MODEL:+--model "$MODEL"} \
|
|
132
|
+
--out "$OUT"
|
|
133
|
+
BENCH_EXIT=$?
|
|
134
|
+
|
|
135
|
+
# A bench REPORTS; it does not gate. Exiting non-zero because a machine is slow
|
|
136
|
+
# would turn "this box is too slow" into "the lane is broken" — the confusion
|
|
137
|
+
# that left windows-latest unmeasured for two months. Only producing NOTHING fails.
|
|
138
|
+
[ -f "$OUT" ] || die "bench produced no matrix at all (node exit $BENCH_EXIT)"
|
|
139
|
+
log "matrix written: $OUT"
|
|
140
|
+
exit 0
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
# [--sign-app "Developer ID Application: …"]
|
|
32
32
|
# [--sign-installer "Developer ID Installer: …"]
|
|
33
33
|
# [--bundle-model] (default; include the pinned ~2.4 GB VLM)
|
|
34
|
-
# [--skip-model] (
|
|
34
|
+
# [--skip-model] (code-only upgrade; not a first installer)
|
|
35
35
|
# [--offline] (skip the official Node + ffmpeg downloads,
|
|
36
36
|
# copy the host `node`/`ffmpeg` instead — for
|
|
37
37
|
# air-gapped CI)
|
|
@@ -104,6 +104,7 @@ WORKSPACE_ROOT="$(cd "$PKG_ROOT/../.." && pwd)" # …/trillboards-edge-sdk
|
|
|
104
104
|
|
|
105
105
|
BUNDLE_ID="com.trillboards.edge"
|
|
106
106
|
APP_NAME="TrillboardsEdge"
|
|
107
|
+
INSTALLER_NAME="SenseAnythingConnector"
|
|
107
108
|
INSTALL_SUBDIR="Library/Trillboards/edge" # relative to the user home
|
|
108
109
|
|
|
109
110
|
# ─── Args ────────────────────────────────────────────────────────────────────
|
|
@@ -150,8 +151,8 @@ while [ $# -gt 0 ]; do
|
|
|
150
151
|
esac
|
|
151
152
|
done
|
|
152
153
|
|
|
153
|
-
# Offline
|
|
154
|
-
#
|
|
154
|
+
# Offline first-install builds must provide the model cache explicitly. Slim
|
|
155
|
+
# builds are the release lane's code-only upgrade artifact.
|
|
155
156
|
if [ "$OFFLINE" -eq 1 ] && [ "$BUNDLE_MODEL" -eq 1 ] && [ -z "$GEMMA_MODEL_FILE" ]; then
|
|
156
157
|
echo "--offline with the customer model bundle requires GEMMA_MODEL_FILE" >&2
|
|
157
158
|
exit 2
|
|
@@ -176,7 +177,7 @@ DIST_DIR="$PKG_ROOT/deploy/macos/dist-installer"
|
|
|
176
177
|
STAGE="$DIST_DIR/stage-$ARCH_LABEL" # the payload root (== the user home view)
|
|
177
178
|
PAYLOAD="$STAGE/$INSTALL_SUBDIR" # …/Library/Trillboards/edge
|
|
178
179
|
SCRATCH="$DIST_DIR/scratch-$ARCH_LABEL"
|
|
179
|
-
PKG_OUT="$DIST_DIR/${
|
|
180
|
+
PKG_OUT="$DIST_DIR/${INSTALLER_NAME}.pkg"
|
|
180
181
|
COMPONENT_PKG="$SCRATCH/${APP_NAME}-component.pkg"
|
|
181
182
|
SETUP_APP="$STAGE/Applications/Trillboards CCTV Setup.app"
|
|
182
183
|
|
|
@@ -398,7 +399,9 @@ cp "$PKG_ROOT/deploy/macos/com.trillboards.edge.plist" "$PAYLOAD/share/com.trill
|
|
|
398
399
|
cp "$PKG_ROOT/deploy/macos/com.trillboards.litertlm.plist" "$PAYLOAD/share/com.trillboards.litertlm.plist"
|
|
399
400
|
cp "$PKG_ROOT/deploy/macos/scripts/setup-litertlm.sh" "$PAYLOAD/share/setup-litertlm.sh"
|
|
400
401
|
cp "$PKG_ROOT/deploy/macos/scripts/ensure-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
|
|
401
|
-
|
|
402
|
+
cp "$PKG_ROOT/deploy/macos/apply-update.sh" "$PAYLOAD/share/apply-update.sh"
|
|
403
|
+
chmod +x "$PAYLOAD/share/setup-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh" \
|
|
404
|
+
"$PAYLOAD/share/apply-update.sh"
|
|
402
405
|
|
|
403
406
|
# ─── 7b. Stage the pre-bundled Gemma model (.litertlm) into the payload ───────
|
|
404
407
|
# Stage the redistributable (Apache-2.0) Gemma 4 E2B LiteRT-LM model and verify
|
|
@@ -423,7 +426,7 @@ if [ "$BUNDLE_MODEL" -eq 1 ]; then
|
|
|
423
426
|
printf '%s %s\n' "$GEMMA_MODEL_SHA256" "$GEMMA_MODEL_ID" > "$PAYLOAD/share/models/$GEMMA_MODEL_ID.sha256"
|
|
424
427
|
log "Staged + SHA-verified bundled Gemma model ($(du -h "$GEMMA_OUT" | cut -f1))"
|
|
425
428
|
else
|
|
426
|
-
warn "Building
|
|
429
|
+
warn "Building the code-only upgrade package without bundled local AI; it requires an already-provisioned Mac."
|
|
427
430
|
fi
|
|
428
431
|
|
|
429
432
|
# ─── 7c. Build the native customer setup application ─────────────────────────
|
|
@@ -588,7 +591,7 @@ productbuild \
|
|
|
588
591
|
# ─── 10. Optional sign ───────────────────────────────────────────────────────
|
|
589
592
|
if [ -n "$SIGN_IDENTITY" ]; then
|
|
590
593
|
log "productsign with '$SIGN_IDENTITY'…"
|
|
591
|
-
SIGNED="$DIST_DIR/${
|
|
594
|
+
SIGNED="$DIST_DIR/${INSTALLER_NAME}-signed.pkg"
|
|
592
595
|
productsign --sign "$SIGN_IDENTITY" "$PKG_OUT" "$SIGNED"
|
|
593
596
|
mv "$SIGNED" "$PKG_OUT"
|
|
594
597
|
log "Signed: $PKG_OUT"
|
|
@@ -603,12 +606,12 @@ else
|
|
|
603
606
|
warn "cert (pass --sign-installer) then notarize (see deploy/macos/README.md)."
|
|
604
607
|
fi
|
|
605
608
|
|
|
606
|
-
# Keep
|
|
607
|
-
#
|
|
609
|
+
# Keep code-only upgrade builds bounded so an unrelated payload regression
|
|
610
|
+
# cannot turn an automatic update into another multi-gigabyte first install.
|
|
608
611
|
PKG_BYTES="$(stat -f%z "$PKG_OUT")"
|
|
609
612
|
MAX_SLIM_PKG_BYTES="${MAX_SLIM_PKG_BYTES:-262144000}"
|
|
610
613
|
if [ "$BUNDLE_MODEL" -eq 0 ] && [ "$PKG_BYTES" -gt "$MAX_SLIM_PKG_BYTES" ]; then
|
|
611
|
-
die "
|
|
614
|
+
die "code-only update package is $PKG_BYTES bytes; maximum is $MAX_SLIM_PKG_BYTES bytes"
|
|
612
615
|
fi
|
|
613
616
|
|
|
614
617
|
# ─── Done ────────────────────────────────────────────────────────────────────
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# - ensure the runtime dirs exist (Application Support config dir + Logs dir)
|
|
9
9
|
# - make the bundled binaries executable (pkg payloads can lose +x)
|
|
10
10
|
# - install and health-check the bundled local AI model
|
|
11
|
-
# - open camera pairing only after local AI is ready
|
|
11
|
+
# - open camera pairing only after local AI is ready on a first install
|
|
12
|
+
# - keep an already-paired update headless
|
|
12
13
|
# - DO NOT start the LaunchAgent: there is no paired config yet, and a
|
|
13
14
|
# config-less cloud-sense crash-loops. The agent is bootstrapped only after
|
|
14
15
|
# a successful setup (the installed pairing engine does that).
|
|
@@ -42,6 +43,9 @@ fi
|
|
|
42
43
|
EDGE_ROOT="$USER_HOME/Library/Trillboards/edge"
|
|
43
44
|
APP_SUPPORT="$USER_HOME/Library/Application Support/Trillboards"
|
|
44
45
|
LOG_DIR="$USER_HOME/Library/Logs/Trillboards"
|
|
46
|
+
PAIRED_CONFIG="$APP_SUPPORT/trillboards.config.json"
|
|
47
|
+
IS_UPGRADE=0
|
|
48
|
+
[ -s "$PAIRED_CONFIG" ] && IS_UPGRADE=1
|
|
45
49
|
|
|
46
50
|
log() { /usr/bin/logger -t com.trillboards.edge "postinstall: $*" 2>/dev/null || true; printf '[trillboards postinstall] %s\n' "$*"; }
|
|
47
51
|
|
|
@@ -118,7 +122,9 @@ APPS_DIR="$USER_HOME/Applications"
|
|
|
118
122
|
SETUP_APP="$APPS_DIR/Trillboards CCTV Setup.app"
|
|
119
123
|
LEGACY_PAIR_LAUNCHER="$APPS_DIR/Trillboards Pair.command"
|
|
120
124
|
rm -f "$LEGACY_PAIR_LAUNCHER" 2>/dev/null || true
|
|
121
|
-
if [
|
|
125
|
+
if [ "$IS_UPGRADE" -eq 1 ]; then
|
|
126
|
+
log "paired connector upgrade detected; setup remains closed"
|
|
127
|
+
elif [ -d "$SETUP_APP" ] && [ -x "$EDGE_ROOT/bin/trillboards-pair" ]; then
|
|
122
128
|
log "native setup app ready: \"$SETUP_APP\" (opening it)"
|
|
123
129
|
if [ "$(id -u)" = "0" ] && [ -n "$OWNER" ] && [ "$OWNER" != "root" ]; then
|
|
124
130
|
LAUNCH_UID="$(id -u "$OWNER" 2>/dev/null || echo '')"
|
|
@@ -149,5 +155,9 @@ else
|
|
|
149
155
|
log "WARNING: launcher not executable at $EDGE_ROOT/bin/trillboards-edge"
|
|
150
156
|
fi
|
|
151
157
|
|
|
152
|
-
|
|
158
|
+
if [ "$IS_UPGRADE" -eq 1 ]; then
|
|
159
|
+
log "done. Connector update installed; existing pairing preserved."
|
|
160
|
+
else
|
|
161
|
+
log "done. Connector and local AI installed; Trillboards CCTV Setup opened automatically."
|
|
162
|
+
fi
|
|
153
163
|
exit 0
|
package/deploy/windows/README.md
CHANGED
|
@@ -70,15 +70,17 @@ serves every Windows 10/11 — no per-arch builds.
|
|
|
70
70
|
./build-windows.ps1 -Offline # copy host node/ffmpeg (air-gapped)
|
|
71
71
|
|
|
72
72
|
# after the inner installer is signed, create the one-file customer artifact
|
|
73
|
-
./bundle-windows-installer.ps1 -InnerInstaller <signed-inner.exe>
|
|
73
|
+
./bundle-windows-installer.ps1 -InnerInstaller <signed-inner.exe> # -> dist-installer\customer\SenseAnythingConnector.exe
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Requires NSIS and 7-Zip on PATH (`choco install nsis 7zip -y`). The release workflow publishes
|
|
77
|
-
the `.exe` to `s3://trillboards-screen-static/connector/
|
|
77
|
+
the `.exe` to `s3://trillboards-screen-static/connector/SenseAnythingConnector.exe`
|
|
78
78
|
plus `trillboards-connector-windows.version.json`, containing the exact connector
|
|
79
|
-
version, production commit, SHA-256, file size, and publication time.
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
version, production commit, SHA-256, file size, and publication time. The old
|
|
80
|
+
`trillboards-connector-windows.exe` object remains a compatibility alias, with a
|
|
81
|
+
content-disposition header that still downloads as `SenseAnythingConnector.exe`.
|
|
82
|
+
It then invalidates the `screen.trillboards.com` CloudFront distribution, so the
|
|
83
|
+
portal's stable download URL never changes while field tests remain artifact-verifiable.
|
|
82
84
|
|
|
83
85
|
## Notes
|
|
84
86
|
|
|
@@ -37,9 +37,7 @@ if (-not (Test-Path -LiteralPath $InnerInstaller -PathType Leaf)) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
|
40
|
-
$
|
|
41
|
-
if ([string]::IsNullOrWhiteSpace($version)) { $version = "unknown" }
|
|
42
|
-
$OutputPath = Join-Path (Join-Path $DistDir "customer") "TrillboardsEdgeSetup-$version-x64.exe"
|
|
40
|
+
$OutputPath = Join-Path (Join-Path $DistDir "customer") "SenseAnythingConnector.exe"
|
|
43
41
|
}
|
|
44
42
|
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
|
|
45
43
|
|