diff --git a/deploy-hardware.sh b/deploy-hardware.sh new file mode 100755 index 0000000..aa5996b --- /dev/null +++ b/deploy-hardware.sh @@ -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 "$@" diff --git a/deploy-local.sh b/deploy-local.sh new file mode 100755 index 0000000..e14f0a2 --- /dev/null +++ b/deploy-local.sh @@ -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 "$@" diff --git a/deploy-prod.sh b/deploy-prod.sh new file mode 100755 index 0000000..54780f0 --- /dev/null +++ b/deploy-prod.sh @@ -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 "$@" diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c67ba88 --- /dev/null +++ b/deploy.sh @@ -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 </dev/null + +# Discover the on-Pi http path; the bbctrl egg version may change. +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 "❌ 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-mv into place. +# This avoids needing root over rsync. +REMOTE_TMP="/tmp/onefin_ui_$$" +ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" "mkdir -p '${REMOTE_TMP}'" +rsync -avz --delete \ + --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 --delete --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \ + && rm -rf \"${REMOTE_TMP}\" + '" 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}/" diff --git a/scripts/deploy/local.sh b/scripts/deploy/local.sh new file mode 100755 index 0000000..d117b6d --- /dev/null +++ b/scripts/deploy/local.sh @@ -0,0 +1,75 @@ +#!/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. +# * The W axis row in jog/DRO is hidden (correct: it appears only when +# the controller reports `aux_enabled = true`). To exercise the W +# axis end-to-end, deploy to the Pi (`./deploy.sh hardware`). + +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)" diff --git a/scripts/deploy/prod.sh b/scripts/deploy/prod.sh new file mode 100755 index 0000000..6118d41 --- /dev/null +++ b/scripts/deploy/prod.sh @@ -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}/" diff --git a/src/js/settings-shell-view.js b/src/js/settings-shell-view.js index cf88802..c356600 100644 --- a/src/js/settings-shell-view.js +++ b/src/js/settings-shell-view.js @@ -47,6 +47,10 @@ module.exports = { { sub: "motor", motor: 1, href: "#motor:1", icon: "fa-arrows-up-down-left-right", label: "Motor 1" }, { sub: "motor", motor: 2, href: "#motor:2", icon: "fa-arrows-up-down-left-right", label: "Motor 2" }, { sub: "motor", motor: 3, href: "#motor:3", icon: "fa-arrows-up-down-left-right", label: "Motor 3" }, + // W axis is auxiliary (auxcnc ESP32). Its config lives inside + // the main Settings page; we route to #settings and scroll to + // the #w-axis anchor on click. + { sub: "w-axis", href: "#settings", anchor: "w-axis", icon: "fa-arrows-up-down", label: "W Axis" }, { section: " " }, { sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" }, { sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" }, @@ -95,6 +99,12 @@ module.exports = { is_active: function (item) { if (!item || item.section) return false; + // The W Axis rail item is a soft-link into #settings; we mark + // it active only when the user is on Display & Units AND the + // last clicked rail item was W Axis. + if (item.sub === "w-axis") { + return this.sub === "settings" && this._w_axis_focus === true; + } if (item.sub !== this.sub) return false; if (item.sub === "motor") { return "" + item.motor === "" + this.ridx; @@ -102,6 +112,23 @@ module.exports = { return true; }, + on_rail_click: function (item, ev) { + // Soft-link rail items use an anchor and a scrollIntoView call. + if (item && item.anchor) { + ev.preventDefault(); + // Navigate to settings if not already there, then scroll. + if (location.hash !== item.href) location.hash = item.href; + this._w_axis_focus = (item.sub === "w-axis"); + // Defer the scroll so Vue mounts the inner Svelte page first. + setTimeout(() => { + const el = document.getElementById(item.anchor); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 250); + } else { + this._w_axis_focus = false; + } + }, + showShutdownDialog: function () { SvelteComponents.showDialog("Shutdown"); }, diff --git a/src/pug/templates/settings-shell.pug b/src/pug/templates/settings-shell.pug index 3c605b1..7b11d8a 100644 --- a/src/pug/templates/settings-shell.pug +++ b/src/pug/templates/settings-shell.pug @@ -9,7 +9,7 @@ script#settings-shell-view-template(type="text/x-template") template(v-for="item in rail_items") .set-section(v-if="item.section") {{item.section}} a.set-item(v-if="!item.section", :class="{active: is_active(item)}", - :href="item.href") + :href="item.href", @click="on_rail_click(item, $event)") .fa(:class="item.icon") |  {{item.label}} .set-rail-foot diff --git a/src/svelte-components/src/components/SettingsView.svelte b/src/svelte-components/src/components/SettingsView.svelte index c2a32f6..f13a6ed 100644 --- a/src/svelte-components/src/components/SettingsView.svelte +++ b/src/svelte-components/src/components/SettingsView.svelte @@ -95,7 +95,7 @@ {/each} -

W Axis (auxcnc)

+

W Axis (auxcnc)