deploy: add macOS-friendly deploy scripts (local / hardware / prod)
- deploy.sh dispatcher + thin shims (deploy-local.sh / -hardware.sh / -prod.sh). - scripts/deploy/local.sh: build UI bundle and serve via tmux session on :8770 for offline iteration. - scripts/deploy/hardware.sh: rsync-based push to a Pi over SSH and restart bbctrl.service. - scripts/deploy/prod.sh: bundle release tarball. - scripts/deploy/patch_font_mime.py: hot-patches Chromium 72's broken WOFF2 mime handling on the kiosk Pi.
This commit is contained in:
4
deploy-hardware.sh
Executable file
4
deploy-hardware.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shorthand for ./deploy.sh hardware
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "$SCRIPT_DIR/deploy.sh" hardware "$@"
|
||||||
4
deploy-local.sh
Executable file
4
deploy-local.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shorthand for ./deploy.sh local
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "$SCRIPT_DIR/deploy.sh" local "$@"
|
||||||
4
deploy-prod.sh
Executable file
4
deploy-prod.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shorthand for ./deploy.sh prod
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec "$SCRIPT_DIR/deploy.sh" prod "$@"
|
||||||
52
deploy.sh
Executable file
52
deploy.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Onefinity firmware deploy script.
|
||||||
|
#
|
||||||
|
# ./deploy.sh local — build & static-serve the UI on macOS
|
||||||
|
# (chrome only; no controller, shows
|
||||||
|
# DISCONNECTED overlay)
|
||||||
|
# ./deploy.sh hardware — fast iteration: rsync build/http/
|
||||||
|
# contents to the running Pi at
|
||||||
|
# onefinity.local, then restart bbctrl
|
||||||
|
# ./deploy.sh prod — full firmware update via the Pi's
|
||||||
|
# /api/firmware/update endpoint
|
||||||
|
# (equivalent to `make update`)
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# * On macOS we cannot run the Python `bbctrl` controller directly
|
||||||
|
# because it imports the ARM-only camotics gplan.so. For full UI
|
||||||
|
# testing with live data, deploy to the Pi (hardware or prod).
|
||||||
|
# * `prod` requires a clean working tree.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
CMD="${1:-}"
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
local) exec "$SCRIPT_DIR/scripts/deploy/local.sh" "$@" ;;
|
||||||
|
hardware) exec "$SCRIPT_DIR/scripts/deploy/hardware.sh" "$@" ;;
|
||||||
|
prod) exec "$SCRIPT_DIR/scripts/deploy/prod.sh" "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<USAGE
|
||||||
|
usage: $0 {local | hardware | prod}
|
||||||
|
|
||||||
|
local Build the UI and static-serve build/http/ in a tmux session
|
||||||
|
on macOS. Useful for iterating on the V09 chrome and routing.
|
||||||
|
URL: http://localhost:8770/
|
||||||
|
tmux: tmux attach -t onefin-local
|
||||||
|
|
||||||
|
hardware Fast iteration on the actual controller: rsync the freshly
|
||||||
|
built build/http/ tree onto onefinity.local, then restart
|
||||||
|
the bbctrl service. Requires SSH access as bbmc@onefinity.local.
|
||||||
|
Defaults: HOST=onefinity.local PASSWORD=onefinity
|
||||||
|
|
||||||
|
prod Build a full firmware package (.tar.bz2) and PUT it through
|
||||||
|
/api/firmware/update on the Pi. Equivalent to:
|
||||||
|
make update HOST=onefinity.local PASSWORD=onefinity
|
||||||
|
Requires a clean working tree.
|
||||||
|
USAGE
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
84
scripts/deploy/hardware.sh
Executable file
84
scripts/deploy/hardware.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# --- Hardware iteration (live Pi at onefinity.local) ---
|
||||||
|
#
|
||||||
|
# Rsyncs the freshly built static UI tree (build/http/) onto the Pi's
|
||||||
|
# bbctrl egg directory and restarts bbctrl. This is much faster than
|
||||||
|
# a full firmware update and is the fastest way to iterate on the V09
|
||||||
|
# UI changes against real machine state (W axis, jog feedback, etc).
|
||||||
|
#
|
||||||
|
# Defaults:
|
||||||
|
# HOST=onefinity.local
|
||||||
|
# REMOTE_USER=bbmc
|
||||||
|
# PASSWORD=onefinity (used for sudo on the Pi)
|
||||||
|
#
|
||||||
|
# Override:
|
||||||
|
# HOST=10.1.10.55 ./deploy.sh hardware
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
HOST="${HOST:-onefinity.local}"
|
||||||
|
REMOTE_USER="${REMOTE_USER:-bbmc}"
|
||||||
|
PASSWORD="${PASSWORD:-onefinity}"
|
||||||
|
|
||||||
|
echo "Building UI bundle (HTML + resources)..."
|
||||||
|
make build/http/index.html >/dev/null
|
||||||
|
# Copy src/resources/* into build/http/. The Makefile's "all" target
|
||||||
|
# also does this, but pulls in cross-compiled subprojects (avr/boot/
|
||||||
|
# pwr/jig) we don't have toolchains for on macOS. This rsync mirrors
|
||||||
|
# only the resource tree.
|
||||||
|
rsync -a src/resources/ build/http/
|
||||||
|
|
||||||
|
echo "Locating bbctrl http/ directory on $HOST..."
|
||||||
|
REMOTE_HTTP_DIR="$(ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
|
"ls -d /usr/local/lib/python*/dist-packages/bbctrl-*-py*.egg/bbctrl/http 2>/dev/null | head -1")"
|
||||||
|
if [[ -z "$REMOTE_HTTP_DIR" ]]; then
|
||||||
|
echo "ERROR: could not find bbctrl http/ directory on $HOST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " $REMOTE_HTTP_DIR"
|
||||||
|
|
||||||
|
echo "Rsyncing build/http/ -> $HOST:$REMOTE_HTTP_DIR/"
|
||||||
|
# Stage to a tmp dir owned by $REMOTE_USER, then sudo-rsync into
|
||||||
|
# place. This avoids needing root over rsync. We do NOT use --delete
|
||||||
|
# anywhere -- the Pi's egg ships extra runtime files (config-template
|
||||||
|
# .json, default machine JSON, buildbotics.nc, etc.) that come with
|
||||||
|
# the bbctrl package and are not in this repo's src/resources. If
|
||||||
|
# they were deleted the controller's API would 500 because Python
|
||||||
|
# imports fail.
|
||||||
|
REMOTE_TMP="/tmp/onefin_ui_$$"
|
||||||
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" "mkdir -p '${REMOTE_TMP}'"
|
||||||
|
rsync -avz \
|
||||||
|
--exclude='hostinfo.txt' \
|
||||||
|
-e "ssh -o ConnectTimeout=5" \
|
||||||
|
build/http/ "${REMOTE_USER}@${HOST}:${REMOTE_TMP}/"
|
||||||
|
|
||||||
|
echo "Installing into ${REMOTE_HTTP_DIR}/ (sudo)..."
|
||||||
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
|
"echo '${PASSWORD}' | sudo -S bash -c '
|
||||||
|
rsync -a --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \
|
||||||
|
&& rm -rf \"${REMOTE_TMP}\"
|
||||||
|
'" 2>&1 | tail -3
|
||||||
|
|
||||||
|
# Patch bbctrl Web.py so font files get the correct MIME type. The
|
||||||
|
# Pi ships Python 3.5, whose `mimetypes` module doesn't know about
|
||||||
|
# woff/woff2/ttf, so Tornado serves them as application/octet-stream
|
||||||
|
# which Chromium 72 (the Pi's onboard browser) refuses to use as a
|
||||||
|
# web font, leading to all FontAwesome icons rendering as empty
|
||||||
|
# boxes in the kiosk UI. The patch is idempotent.
|
||||||
|
echo "Patching bbctrl font MIME types (idempotent)..."
|
||||||
|
scp -o ConnectTimeout=5 "$SCRIPT_DIR/scripts/deploy/patch_font_mime.py" \
|
||||||
|
"${REMOTE_USER}@${HOST}:/tmp/patch_font_mime.py" >/dev/null
|
||||||
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
|
"echo '${PASSWORD}' | sudo -S python3 /tmp/patch_font_mime.py" 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo "Restarting bbctrl service..."
|
||||||
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
|
"echo '${PASSWORD}' | sudo -S systemctl restart bbctrl" 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deployed to http://${HOST}/"
|
||||||
|
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
|
||||||
|
echo " Open: open -a 'Google Chrome' http://${HOST}/"
|
||||||
72
scripts/deploy/local.sh
Executable file
72
scripts/deploy/local.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# --- Local development (macOS) ---
|
||||||
|
#
|
||||||
|
# Builds the UI bundle and static-serves it on http://localhost:8770/.
|
||||||
|
# Runs in a named tmux session so we can iterate (re-running this script
|
||||||
|
# rebuilds and restarts the server in-place, you keep your browser tab).
|
||||||
|
#
|
||||||
|
# What you'll see:
|
||||||
|
# * The full V09 chrome (header tabs, settings rail, jog grid, DRO
|
||||||
|
# skeleton, status strip).
|
||||||
|
# * A "DISCONNECTED" overlay because there's no controller backend.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo "🛠 Building UI bundle..."
|
||||||
|
make build/http/index.html >/dev/null
|
||||||
|
|
||||||
|
PORT="${PORT:-8770}"
|
||||||
|
SESSION="onefin-local"
|
||||||
|
|
||||||
|
ensure_tmux_window() {
|
||||||
|
local session="$1"
|
||||||
|
local window="${2:-}"
|
||||||
|
local target="${session}${window:+:$window}"
|
||||||
|
if tmux has-session -t "$session" 2>/dev/null; then
|
||||||
|
if tmux send-keys -t "$target" "" 2>/dev/null; then
|
||||||
|
echo "🔁 Reusing tmux session '$session'..."
|
||||||
|
tmux send-keys -t "$target" C-c
|
||||||
|
sleep 1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "⚠️ Dead pane in '$session', recreating..."
|
||||||
|
tmux kill-session -t "$session" 2>/dev/null
|
||||||
|
fi
|
||||||
|
echo "🆕 Creating tmux session '$session'..."
|
||||||
|
tmux new-session -d -s "$session"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_tmux_window "$SESSION"
|
||||||
|
|
||||||
|
# Free the port if a previous run is still listening.
|
||||||
|
if lsof -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||||
|
echo "⚠️ Port $PORT is busy; killing previous server..."
|
||||||
|
lsof -tiTCP:"$PORT" -sTCP:LISTEN | xargs -r kill 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmux send-keys -t "$SESSION" \
|
||||||
|
"cd '$SCRIPT_DIR' && python3 -m http.server --directory build/http $PORT" \
|
||||||
|
C-m
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Static UI server started on http://localhost:$PORT/"
|
||||||
|
echo ""
|
||||||
|
echo " Routes to try:"
|
||||||
|
echo " http://localhost:$PORT/#control"
|
||||||
|
echo " http://localhost:$PORT/#program"
|
||||||
|
echo " http://localhost:$PORT/#console"
|
||||||
|
echo " http://localhost:$PORT/#settings (Display & Units)"
|
||||||
|
echo " http://localhost:$PORT/#admin-network (WiFi / IP)"
|
||||||
|
echo " http://localhost:$PORT/#motor:0 (Motor 0 settings)"
|
||||||
|
echo ""
|
||||||
|
echo " tmux: tmux attach -t $SESSION"
|
||||||
|
echo " stop: tmux kill-session -t $SESSION"
|
||||||
|
echo ""
|
||||||
|
echo "ℹ️ No controller is running, so the page shows DISCONNECTED and"
|
||||||
|
echo " axis values stay empty. For live data + W axis, run:"
|
||||||
|
echo " ./deploy.sh hardware (fast: rsync build/http -> Pi)"
|
||||||
|
echo " ./deploy.sh prod (full firmware update)"
|
||||||
102
scripts/deploy/patch_font_mime.py
Normal file
102
scripts/deploy/patch_font_mime.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Patch bbctrl Web.py so font files get the correct MIME type.
|
||||||
|
|
||||||
|
Background
|
||||||
|
----------
|
||||||
|
The Onefinity controller (Pi 3B running Raspbian stretch) ships Python
|
||||||
|
3.5, whose ``mimetypes`` module does not recognize ``.woff``, ``.woff2``
|
||||||
|
or ``.ttf``. Tornado's ``StaticFileHandler`` therefore falls back to
|
||||||
|
``application/octet-stream`` for those, and Chromium 72 (the Pi's
|
||||||
|
onboard kiosk browser) refuses to use such payloads as web fonts. The
|
||||||
|
result is that every FontAwesome icon renders as an empty box on the
|
||||||
|
kiosk display.
|
||||||
|
|
||||||
|
This patch monkey-patches ``StaticFileHandler.get_content_type`` to
|
||||||
|
emit the right MIME types. It is idempotent: running it twice is a
|
||||||
|
no-op. Run with ``sudo`` so it can rewrite the egg's Web.py.
|
||||||
|
|
||||||
|
Used by:
|
||||||
|
scripts/deploy/hardware.sh
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def find_web_py():
|
||||||
|
"""Return the absolute path to the bbctrl Web.py shipped in the egg."""
|
||||||
|
base = "/usr/local/lib"
|
||||||
|
for entry in os.listdir(base):
|
||||||
|
if not entry.startswith("python"):
|
||||||
|
continue
|
||||||
|
candidate_dir = os.path.join(base, entry, "dist-packages")
|
||||||
|
if not os.path.isdir(candidate_dir):
|
||||||
|
continue
|
||||||
|
for sub in os.listdir(candidate_dir):
|
||||||
|
if sub.startswith("bbctrl-") and sub.endswith(".egg"):
|
||||||
|
p = os.path.join(candidate_dir, sub, "bbctrl", "Web.py")
|
||||||
|
if os.path.isfile(p):
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
OLD_BLOCK = (
|
||||||
|
"class StaticFileHandler(tornado.web.StaticFileHandler):\n"
|
||||||
|
" def set_extra_headers(self, path):\n"
|
||||||
|
" self.set_header('Cache-Control',\n"
|
||||||
|
" 'no-store, no-cache, must-revalidate, max-age=0')"
|
||||||
|
)
|
||||||
|
|
||||||
|
NEW_BLOCK = (
|
||||||
|
"class StaticFileHandler(tornado.web.StaticFileHandler):\n"
|
||||||
|
" # FONT_MIME_FIX: Python 3.5's mimetypes module does not know\n"
|
||||||
|
" # woff/woff2/ttf, so Tornado serves them as application/octet-\n"
|
||||||
|
" # stream which Chromium 72 (the Pi's onboard kiosk browser)\n"
|
||||||
|
" # refuses to use as web fonts. Set explicit types so the FA6\n"
|
||||||
|
" # icon set actually renders on the kiosk display.\n"
|
||||||
|
" def get_content_type(self):\n"
|
||||||
|
" path = self.absolute_path or ''\n"
|
||||||
|
" if path.endswith('.woff2'): return 'font/woff2'\n"
|
||||||
|
" if path.endswith('.woff'): return 'font/woff'\n"
|
||||||
|
" if path.endswith('.ttf'): return 'font/ttf'\n"
|
||||||
|
" if path.endswith('.otf'): return 'font/otf'\n"
|
||||||
|
" if path.endswith('.eot'): return 'application/vnd.ms-fontobject'\n"
|
||||||
|
" return super().get_content_type()\n"
|
||||||
|
"\n"
|
||||||
|
" def set_extra_headers(self, path):\n"
|
||||||
|
" self.set_header('Cache-Control',\n"
|
||||||
|
" 'no-store, no-cache, must-revalidate, max-age=0')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
target = find_web_py()
|
||||||
|
if target is None:
|
||||||
|
print("ERROR: could not locate bbctrl Web.py under /usr/local/lib",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
with open(target) as f:
|
||||||
|
src = f.read()
|
||||||
|
|
||||||
|
if "FONT_MIME_FIX" in src:
|
||||||
|
print("font mime: already patched ({})".format(target))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if OLD_BLOCK not in src:
|
||||||
|
print("font mime: expected block not found in {} -- skipping".format(target),
|
||||||
|
file=sys.stderr)
|
||||||
|
# Don't fail the deploy; just log and continue.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
new_src = src.replace(OLD_BLOCK, NEW_BLOCK, 1)
|
||||||
|
with open(target, "w") as f:
|
||||||
|
f.write(new_src)
|
||||||
|
print("font mime: patched {}".format(target))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
42
scripts/deploy/prod.sh
Executable file
42
scripts/deploy/prod.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# --- Production firmware update (Pi at onefinity.local) ---
|
||||||
|
#
|
||||||
|
# Builds a full firmware package (.tar.bz2) and PUTs it through the Pi's
|
||||||
|
# /api/firmware/update endpoint. This is the canonical OTA flow and goes
|
||||||
|
# through the bbctrl Tornado server's update handler.
|
||||||
|
#
|
||||||
|
# Defaults:
|
||||||
|
# HOST=onefinity.local
|
||||||
|
# PASSWORD=onefinity
|
||||||
|
#
|
||||||
|
# Override:
|
||||||
|
# HOST=10.1.10.55 PASSWORD=secret ./deploy.sh prod
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
HOST="${HOST:-onefinity.local}"
|
||||||
|
PASSWORD="${PASSWORD:-onefinity}"
|
||||||
|
|
||||||
|
# Require a clean working tree.
|
||||||
|
echo "🔍 Checking git state..."
|
||||||
|
if ! git diff --quiet || ! git diff --cached --quiet \
|
||||||
|
|| [[ -n "$(git ls-files --others --exclude-standard)" ]]; then
|
||||||
|
echo "❌ Refusing to deploy: working tree has uncommitted changes."
|
||||||
|
git status --short
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Working tree is clean."
|
||||||
|
|
||||||
|
echo "🛠 Building firmware package..."
|
||||||
|
make pkg
|
||||||
|
|
||||||
|
echo "🚚 Uploading to http://${HOST}/api/firmware/update..."
|
||||||
|
make update HOST="$HOST" PASSWORD="$PASSWORD"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Firmware update PUT to ${HOST}."
|
||||||
|
echo " The Pi will reboot itself after applying the update."
|
||||||
|
echo " Once it comes back, open: http://${HOST}/"
|
||||||
Reference in New Issue
Block a user