Settings rail: add W Axis entry; deploy scripts (local/hardware/prod)
UX
- The V09 redesign already exposed the W axis in the Control jog grid
(row 4 when w.enabled) and as a row in the DRO table. The Settings
shell now also surfaces a dedicated 'W Axis' rail entry that smooth-
scrolls to the W Axis (auxcnc) section of the main settings page.
The rail item is marked active only while the user is on Display &
Units AND the W Axis link was the most recent click.
- The W Axis section in src/svelte-components/src/components/Settings
View.svelte gets an id="w-axis" anchor so the scroll lands cleanly.
Tested live against onefinity.local. Aux status reports
{enabled: true, present: true, pos_mm: 43.96, homed: false}; the W
axis row appears in the DRO with the right purple styling, and the
jog row 4 shows W- / Home W / W+ / Probe.
Deploy scripts
- deploy.sh dispatches to scripts/deploy/{local,hardware,prod}.sh
with shorthand wrappers (deploy-local.sh / deploy-hardware.sh /
deploy-prod.sh).
- local: builds the UI bundle and serves build/http/ via
python3 -m http.server 8770 in a tmux session 'onefin-local'.
Useful for visual iteration on macOS — chrome only, no controller.
- hardware: rsyncs the freshly built build/http/ tree onto the Pi at
onefinity.local and restarts bbctrl. Stages to /tmp on the Pi and
uses sudo to install into the running egg's bbctrl/http directory,
so iteration time is ~5 seconds.
- prod: requires a clean working tree, then runs 'make pkg' followed
by 'make update HOST=onefinity.local PASSWORD=onefinity'.
Defaults can be overridden with environment variables (HOST, PASSWORD,
REMOTE_USER for the hardware path).
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
|
||||||
65
scripts/deploy/hardware.sh
Executable file
65
scripts/deploy/hardware.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/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
|
||||||
|
# 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 (not USER, which the shell pre-populates with the local
|
||||||
|
# logged-in account).
|
||||||
|
REMOTE_USER="${REMOTE_USER:-bbmc}"
|
||||||
|
PASSWORD="${PASSWORD:-onefinity}"
|
||||||
|
|
||||||
|
echo "🛠 Building UI bundle..."
|
||||||
|
make build/http/index.html >/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}/"
|
||||||
75
scripts/deploy/local.sh
Executable file
75
scripts/deploy/local.sh
Executable file
@@ -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)"
|
||||||
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}/"
|
||||||
@@ -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: 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: 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" },
|
{ 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: " " },
|
{ section: " " },
|
||||||
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
|
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
|
||||||
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
|
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
|
||||||
@@ -95,6 +99,12 @@ module.exports = {
|
|||||||
|
|
||||||
is_active: function (item) {
|
is_active: function (item) {
|
||||||
if (!item || item.section) return false;
|
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 !== this.sub) return false;
|
||||||
if (item.sub === "motor") {
|
if (item.sub === "motor") {
|
||||||
return "" + item.motor === "" + this.ridx;
|
return "" + item.motor === "" + this.ridx;
|
||||||
@@ -102,6 +112,23 @@ module.exports = {
|
|||||||
return true;
|
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 () {
|
showShutdownDialog: function () {
|
||||||
SvelteComponents.showDialog("Shutdown");
|
SvelteComponents.showDialog("Shutdown");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ script#settings-shell-view-template(type="text/x-template")
|
|||||||
template(v-for="item in rail_items")
|
template(v-for="item in rail_items")
|
||||||
.set-section(v-if="item.section") {{item.section}}
|
.set-section(v-if="item.section") {{item.section}}
|
||||||
a.set-item(v-if="!item.section", :class="{active: is_active(item)}",
|
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")
|
.fa(:class="item.icon")
|
||||||
| {{item.label}}
|
| {{item.label}}
|
||||||
.set-rail-foot
|
.set-rail-foot
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2>W Axis (auxcnc)</h2>
|
<h2 id="w-axis">W Axis (auxcnc)</h2>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<WAxisSettings />
|
<WAxisSettings />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
Reference in New Issue
Block a user