Compare commits
30 Commits
master
...
3d73e6c59d
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d73e6c59d | |||
| 860ca30aba | |||
| 8e3b7a29e5 | |||
| 420caf52be | |||
| 561d2fd7ea | |||
| 73c6a4f160 | |||
| 5926316a25 | |||
| ea23f94b87 | |||
| b8c4f53bb1 | |||
| 32f3aca368 | |||
| 081209decf | |||
| ef4658aaf6 | |||
| ef78f20eaa | |||
| 36829020a5 | |||
| 2413fc49ab | |||
|
|
7d5949f5fc | ||
|
|
23f22105a8 | ||
|
|
4f74e75d44 | ||
|
|
c7cf9483b3 | ||
| 54a15f9d12 | |||
| 704bc8d35c | |||
| 4d2d5fd88c | |||
| eab204b7be | |||
| e3c059eb9b | |||
| 7306464440 | |||
| 1625b768d8 | |||
| 5be7515a92 | |||
| b10a6d537e | |||
| 7d0755c55b | |||
| 7f8fd23615 |
77
AGENTS.md
77
AGENTS.md
@@ -1,77 +0,0 @@
|
|||||||
# Onefinity firmware — agent guidelines
|
|
||||||
|
|
||||||
## Branch model
|
|
||||||
|
|
||||||
This fork lives on **two long-lived branches**:
|
|
||||||
|
|
||||||
- **`master`** — public-facing fork. General-use upgrades on top of
|
|
||||||
upstream OneFinity firmware: V09 UX redesign, Font Awesome 6, faster
|
|
||||||
cold boot, macOS dev/deploy tooling, build & flash docs, SD-card
|
|
||||||
backup, `/api/diag/timing`, kiosk/tablet polish, and assorted
|
|
||||||
bug-fixes. **No A-axis, ATC, hooks, or auxcnc/ESP content.** Aim for
|
|
||||||
changes that benefit any Onefinity owner.
|
|
||||||
|
|
||||||
- **`private-mods`** — bespoke shop branch. Stacks on top of `master`
|
|
||||||
and adds everything specific to the auxcnc-ESP-driven A axis and
|
|
||||||
the ATC: `Hooks` (ATC IPC), `AuxAxis` (ESP serial driver),
|
|
||||||
`ExternalAxis` (virtual A through gplan), `AuxPreprocessor` (M100-M103),
|
|
||||||
Z-A coupling interlock, the A-axis UI surface, and the
|
|
||||||
`/api/aux/*` endpoints.
|
|
||||||
|
|
||||||
Upstream:
|
|
||||||
- `upstream` → `https://github.com/OneFinityCNC/onefinity-firmware.git`
|
|
||||||
- `origin` → Gitea (`https://gitea.home.muehe.org/muehe/onefinity-firmware.git`)
|
|
||||||
|
|
||||||
`origin/pre-split-backup` is a tag preserving the pre-split master
|
|
||||||
tip. Keep it indefinitely until further notice.
|
|
||||||
|
|
||||||
## Where does a change go?
|
|
||||||
|
|
||||||
| Change | Branch |
|
|
||||||
|---|---|
|
|
||||||
| UI polish, theme, layout that any user benefits from | `master` |
|
|
||||||
| Build / install / boot performance | `master` |
|
|
||||||
| Diagnostics, logging, generic Python / Tornado fixes | `master` |
|
|
||||||
| Anything that touches `AuxAxis`, `ExternalAxis`, `Hooks`, `AuxPreprocessor` | `private-mods` |
|
|
||||||
| Anything mentioning the auxcnc ESP, `/dev/ttyUSB0`, the M100-M103 ATC pneumatics, or motor index 4 | `private-mods` |
|
|
||||||
| Z-A coupling interlock, ATC tool change sequencing | `private-mods` |
|
|
||||||
| A-axis UI (DRO row, jog tile, settings page, A-axis routes) | `private-mods` |
|
|
||||||
| W → A renames or aux.json migrations | `private-mods` |
|
|
||||||
|
|
||||||
When in doubt: ask "would this be useful on a stock Onefinity with no
|
|
||||||
ESP attached?" If yes → `master`. If no → `private-mods`.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Day-to-day shop / hardware work (default)
|
|
||||||
git checkout private-mods
|
|
||||||
# … do work, commit …
|
|
||||||
git push origin private-mods
|
|
||||||
|
|
||||||
# Generic improvement to master
|
|
||||||
git checkout master
|
|
||||||
# … do work, commit …
|
|
||||||
git push origin master
|
|
||||||
|
|
||||||
# After landing on master, replay private-mods on top
|
|
||||||
git checkout private-mods
|
|
||||||
git rebase master
|
|
||||||
git push --force-with-lease origin private-mods
|
|
||||||
```
|
|
||||||
|
|
||||||
If a change accidentally lands on `master` but is bespoke (touches
|
|
||||||
the file table above), move it: `git reset --hard <prev>` on master,
|
|
||||||
cherry-pick onto `private-mods`, force-push master.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
- `./deploy.sh local` — UI bundle on `localhost:8770` (tmux session
|
|
||||||
`onefin-local`). No controller backend; A-axis row stays hidden.
|
|
||||||
- `./deploy.sh hardware` — rsync to the Pi over SSH, restart
|
|
||||||
`bbctrl.service`. Use the `private-mods` branch on the shop Pi.
|
|
||||||
- `./deploy.sh prod` — bundle a release tarball.
|
|
||||||
|
|
||||||
See `.pi/BUILD.md` for the full build / flash / cross-compile flow.
|
|
||||||
|
|
||||||
## Commit before ending a turn; push after significant changes.
|
|
||||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,67 +1,6 @@
|
|||||||
OneFinity CNC Controller Firmware Changelog
|
OneFinity CNC Controller Firmware Changelog
|
||||||
===========================================
|
===========================================
|
||||||
|
|
||||||
## Unreleased (community fork)
|
|
||||||
|
|
||||||
General-use additions on top of upstream OneFinity firmware.
|
|
||||||
|
|
||||||
### UI
|
|
||||||
- V09 redesign: 4-tab top header (Control / Program / Console /
|
|
||||||
Settings) replaces the legacy side menu.
|
|
||||||
- Control: redesigned DRO with per-axis offset + zero + home
|
|
||||||
actions, jog grid with consistent button sizing across kiosk
|
|
||||||
and tablet, status strip with live state / velocity / spindle.
|
|
||||||
- Program: dedicated tab for run / pause / stop, file browser,
|
|
||||||
toolpath preview.
|
|
||||||
- Console: MDI shell, message log, indicators.
|
|
||||||
- Settings: rail-driven inner pages so each section is its own
|
|
||||||
focused panel rather than one long scroll.
|
|
||||||
- Tablet mode (`?tablet=1`) pins the UI to 1920x1080 and scales
|
|
||||||
it to fit the actual viewport.
|
|
||||||
- Kiosk mode (`?kiosk=1`, auto on localhost): tighter layout for
|
|
||||||
the controller's onboard 1366x768 screen.
|
|
||||||
- Font Awesome 6 throughout (replaces FA4).
|
|
||||||
- Fix: stop clobbering motor settings while the user is editing
|
|
||||||
them.
|
|
||||||
- Fix: keep jog grid visible during jog/home/probe/MDI activity.
|
|
||||||
- Fix: opaque dark canvas for path-viewer (no flash through page
|
|
||||||
background).
|
|
||||||
- Fix: OrbitControls now uses non-passive wheel/touch listeners so
|
|
||||||
it can suppress page panning while interacting with the 3D
|
|
||||||
viewer.
|
|
||||||
- Fix: macros tab no longer renders placeholder color stripes for
|
|
||||||
`#dedede`/`#fff`-only macros.
|
|
||||||
- Fix: hide the X cursor in kiosk mode (touchscreen).
|
|
||||||
- Fix: chromium 72 mime + flex-gap fallbacks (some kiosk Pis ship
|
|
||||||
with that older browser build).
|
|
||||||
- Fix: Vue 1 async batching disabled so reactive writes from
|
|
||||||
`hashchange` listeners propagate synchronously.
|
|
||||||
|
|
||||||
### Boot / install
|
|
||||||
- Cold-boot optimisations cutting bbctrl listen latency by ~8s on
|
|
||||||
the Pi (mask sysstat, replace dphys-swapfile with an fstab swap
|
|
||||||
entry, lazy-load `camotics.gplan`, `bbserial-rebind.service`
|
|
||||||
with explicit `Before=bbctrl.service`).
|
|
||||||
- `install.sh` now ships these with firmware updates.
|
|
||||||
- `bbctrl.Trace` + `/api/diag/timing` for measuring startup, with
|
|
||||||
a UI-side `restart-timing.js` client that POSTs browser marks.
|
|
||||||
- `Camera.py` switched from deprecated `@web.asynchronous` to
|
|
||||||
`async def` so the streaming endpoint works on newer Tornado.
|
|
||||||
- `Log.py` tolerates missing rotated log files on startup
|
|
||||||
(concurrent logrotate runs from `/etc/cron.reboot` no longer
|
|
||||||
crash bbctrl).
|
|
||||||
|
|
||||||
### Build / tooling
|
|
||||||
- `.pi/BUILD.md`: end-to-end macOS dev workflow, deploy paths,
|
|
||||||
troubleshooting.
|
|
||||||
- `.pi/Dockerfile.gplan` + `build-gplan.sh`: rebuild `gplan.so`
|
|
||||||
from source on Raspbian Stretch (Bullseye is too new).
|
|
||||||
- `deploy.sh` dispatcher with `local`, `hardware`, `prod` modes.
|
|
||||||
- `backup/onefinity-backup.sh`: dd-based whole-card backup/restore
|
|
||||||
with shrink/expand support.
|
|
||||||
- `Makefile`: ensure trailing newlines between concatenated pug
|
|
||||||
templates so Pug doesn't glue file boundaries together.
|
|
||||||
|
|
||||||
## v1.0.8
|
## v1.0.8
|
||||||
- Fixed chatter and lost steps issues (most commonly seen by Fusion users), re-enabled support for G61, G61.1, G64.
|
- Fixed chatter and lost steps issues (most commonly seen by Fusion users), re-enabled support for G61, G61.1, G64.
|
||||||
- Fixed 3d preview on Safari-based web browsers (MacOS & iOS)
|
- Fixed 3d preview on Safari-based web browsers (MacOS & iOS)
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -68,11 +68,7 @@ update: pkg
|
|||||||
|
|
||||||
build/templates.pug: $(TEMPLS)
|
build/templates.pug: $(TEMPLS)
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
# Use awk to ensure each template is followed by a newline so the
|
cat $(TEMPLS) >$@
|
||||||
# next file's first line never gets glued onto the previous file's
|
|
||||||
# last line (some templates ship without a trailing newline, which
|
|
||||||
# would produce subtle Pug parse failures).
|
|
||||||
awk 'FNR==1 && NR>1 {print ""} {print} END{print ""}' $(TEMPLS) >$@
|
|
||||||
|
|
||||||
node_modules: package.json
|
node_modules: package.json
|
||||||
npm install && touch node_modules
|
npm install && touch node_modules
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -1,8 +1,8 @@
|
|||||||
# OneFinity CNC Controller Firmware (community fork)
|
# OneFinity CNC Controller Firmware (W-axis fork)
|
||||||
|
|
||||||
This is the OneFinity / Buildbotics bbctrl firmware with a redesigned
|
This is the OneFinity / Buildbotics bbctrl firmware with a virtual W
|
||||||
UI (V09), Font Awesome 6, faster cold boot, and a streamlined macOS
|
axis driven by an auxcnc ESP32 over USB serial. See
|
||||||
dev / deploy workflow.
|
[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for the design and config.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ src/svelte-components/ Newer Svelte UI for dialogs and settings
|
|||||||
src/pug/ Pug templates compiled into build/http/index.html
|
src/pug/ Pug templates compiled into build/http/index.html
|
||||||
src/resources/ Static assets and config templates
|
src/resources/ Static assets and config templates
|
||||||
scripts/ Install / update / RPi build helpers
|
scripts/ Install / update / RPi build helpers
|
||||||
docs/ Architecture, dev setup
|
docs/ Architecture, dev setup, W-axis docs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build & flash (quick path, macOS or Linux)
|
## Build & flash (quick path, macOS or Linux)
|
||||||
@@ -100,7 +100,7 @@ bbctrl restarts, then the new UI).
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s http://onefinity.local/ | grep -c "OneFinity"
|
curl -s http://onefinity.local/ | grep -c "OneFinity"
|
||||||
curl -s http://onefinity.local/api/diag/timing | head
|
curl -s http://onefinity.local/api/aux/status # if W axis is enabled
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build & flash (full path, Debian/Linux)
|
## Build & flash (full path, Debian/Linux)
|
||||||
@@ -108,3 +108,15 @@ curl -s http://onefinity.local/api/diag/timing | head
|
|||||||
For AVR + GPlan rebuilds, see [docs/development.md](docs/development.md).
|
For AVR + GPlan rebuilds, see [docs/development.md](docs/development.md).
|
||||||
That path uses qemu + chroot to cross-compile gplan for ARM and needs
|
That path uses qemu + chroot to cross-compile gplan for ARM and needs
|
||||||
the `gcc-avr` / `avr-libc` toolchain.
|
the `gcc-avr` / `avr-libc` toolchain.
|
||||||
|
|
||||||
|
## W axis (auxcnc)
|
||||||
|
|
||||||
|
This fork adds a virtual W axis. See
|
||||||
|
[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for:
|
||||||
|
|
||||||
|
- G-code surface (`G28 W0`, `G1 W25`, etc.)
|
||||||
|
- The G-code preprocessor and hook architecture
|
||||||
|
- aux.json keys
|
||||||
|
- REST API (`/api/aux/*`)
|
||||||
|
- UI surface (jog row in Control, settings panel in Settings)
|
||||||
|
- Edge cases (ESP reboot mid-job, limit closed at home start, …)
|
||||||
|
|||||||
172
docs/AUX_W_AXIS.md
Normal file
172
docs/AUX_W_AXIS.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# W axis (auxcnc) integration
|
||||||
|
|
||||||
|
This adds a virtual `W` axis to the bbctrl controller, driven by the
|
||||||
|
auxcnc ESP32 over USB serial (`/dev/ttyUSB0`). The ESP owns step-pulse
|
||||||
|
generation, real-time limit-switch monitoring, and the homing dance.
|
||||||
|
The Pi owns units (mm), soft limits, sequencing inside G-code jobs, and
|
||||||
|
a small REST API for jogging / homing from the UI.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The bbctrl planner (gplan) only understands `xyzabc`, so adding a true
|
||||||
|
7th axis would require rebuilding gplan + the AVR firmware. We avoid
|
||||||
|
that by treating W as a synchronous out-of-band axis: W moves run
|
||||||
|
*between* G-code blocks, not blended with XYZ.
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
|
||||||
|
1. User uploads a G-code file containing `W` words.
|
||||||
|
2. `FileHandler` runs `AuxPreprocessor` on the upload, rewriting W
|
||||||
|
tokens in place into `(MSG,HOOK:aux:<mm>)` etc. The original line
|
||||||
|
minus the W word continues to drive XYZ.
|
||||||
|
3. The planner sees only XYZ + message comments. When it reaches a
|
||||||
|
message line, the message goes through `state.add_message` which
|
||||||
|
`Hooks._on_state_change` watches for the `HOOK:` prefix.
|
||||||
|
4. `Hooks._fire('custom', ...)` finds the registered internal handler
|
||||||
|
for the event name (`aux`, `aux_rel`, `aux_home`, `aux_setzero`).
|
||||||
|
5. The handler runs in a hook thread, gating `Mach.unpause` until done.
|
||||||
|
While the handler is busy the machine is in HOLDING - no XYZ motion
|
||||||
|
can resume until W finishes.
|
||||||
|
6. The handler talks to the ESP over `/dev/ttyUSB0` via `AuxAxis`,
|
||||||
|
blocking on a deterministic reply token (`[step] done`, `[home]
|
||||||
|
done`, etc).
|
||||||
|
|
||||||
|
MDI commands containing `W` words are rewritten the same way at the
|
||||||
|
`Mach.mdi()` boundary so manual jog and macros work too.
|
||||||
|
|
||||||
|
## G-code surface
|
||||||
|
|
||||||
|
```gcode
|
||||||
|
G21 G90
|
||||||
|
G28 W0 ; home W axis
|
||||||
|
G1 W25 F300 ; move W to 25 mm absolute
|
||||||
|
G1 X100 W12.5 ; mixed: W moves first, then XYZ (configurable)
|
||||||
|
G91
|
||||||
|
G1 W-2.5 ; relative W move
|
||||||
|
G90
|
||||||
|
G92 W0 ; set current W as zero (G92-style)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `G28` / `G28.2` with W only -> homing hook; the bare `G28` is NOT
|
||||||
|
emitted to gplan (that would mean home-all).
|
||||||
|
- `G28.2 X0 Y0 W0` -> emit hook, then keep `G28.2 X0 Y0` for XY homing.
|
||||||
|
- A line with both W and XYZ axis words is split into two sequential
|
||||||
|
blocks. Default order: W first, then XYZ. Toggle via the
|
||||||
|
`w_first` constructor arg.
|
||||||
|
- Lines inside parens or after `;` are passed through verbatim.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Per-controller config lives at `<ctrl_path>/aux.json` (created on first
|
||||||
|
save via the API). Keys:
|
||||||
|
|
||||||
|
| Key | Default | Notes |
|
||||||
|
|------------------------|----------------|------------------------------------|
|
||||||
|
| `enabled` | `false` | Master switch |
|
||||||
|
| `port` | `/dev/ttyUSB0` | Serial device |
|
||||||
|
| `baud` | `115200` | |
|
||||||
|
| `steps_per_mm` | `80.0` | Logical steps per mm |
|
||||||
|
| `dir_sign` | `1` | +1 or -1: maps logical+ to motor+ |
|
||||||
|
| `min_w`, `max_w` | `0`, `100` | Soft limits in mm |
|
||||||
|
| `home_dir` | `'-'` | Direction toward limit switch |
|
||||||
|
| `home_position_mm` | `0.0` | mm value assigned at home |
|
||||||
|
| `home_fast_sps` | `4000` | Fast seek rate |
|
||||||
|
| `home_slow_sps` | `400` | Slow re-seek rate |
|
||||||
|
| `home_backoff_steps` | `200` | Backoff after touching limit |
|
||||||
|
| `home_maxtravel_steps` | `200000` | Hard cap on phase 1 seek |
|
||||||
|
| `step_max_sps` | `4000` | Cruise rate for STEPS |
|
||||||
|
| `step_accel_sps2` | `16000` | Trapezoidal ramp accel |
|
||||||
|
| `step_start_sps` | `200` | Ramp floor |
|
||||||
|
| `limit_low` | `true` | Switch active low (closed = LOW) |
|
||||||
|
|
||||||
|
Most of these are pushed to the ESP via `HOMECFG` on connect and
|
||||||
|
persisted there in NVS.
|
||||||
|
|
||||||
|
## REST API
|
||||||
|
|
||||||
|
| Verb | Path | Body | Effect |
|
||||||
|
|------|----------------------------|-----------------------|------------------------|
|
||||||
|
| GET | `/api/aux/config` | - | Current config |
|
||||||
|
| PUT | `/api/aux/config/save` | `{key: val, ...}` | Save and re-push |
|
||||||
|
| GET | `/api/aux/status` | - | `{enabled, present, homed, pos_mm}` |
|
||||||
|
| PUT | `/api/aux/home` | - | Run home cycle (blocks)|
|
||||||
|
| PUT | `/api/aux/abort` | - | Cancel running motion |
|
||||||
|
| PUT | `/api/aux/jog` | `{mm: 1.5}` or `{steps: 200}` | Relative move |
|
||||||
|
| PUT | `/api/aux/move` | `{mm: 12.5}` | Absolute move (mm) |
|
||||||
|
| PUT | `/api/aux/set-zero` | `{mm: 0}` | Set current pos to mm |
|
||||||
|
|
||||||
|
Steps-mode jog ignores soft limits (use it to inch the axis to the
|
||||||
|
limit switch when the axis isn't homed yet).
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
**Control view**
|
||||||
|
|
||||||
|
- A jog row appears under the XYZ jog grid when `aux_enabled` is true,
|
||||||
|
with three buttons: `W-`, `W+`, and a wide `Home W`. There is
|
||||||
|
intentionally no separate "set zero" or "W origin" button - homing
|
||||||
|
lands the axis at `home_position_mm` (0 by default), so home and
|
||||||
|
zero are the same point.
|
||||||
|
- The DRO table shows a W axis row with position, status (OFFLINE /
|
||||||
|
UNHOMED / HOMED), and a single Home button in the actions column
|
||||||
|
(the cog and map-marker columns are placeholders for layout).
|
||||||
|
|
||||||
|
**Settings view**
|
||||||
|
|
||||||
|
A "W Axis (auxcnc)" section exposes every aux.json field except
|
||||||
|
`enabled` (which stays read-only - flipping the W axis on/off requires
|
||||||
|
editingaux.json on the controller, so a fresh install can't surprise
|
||||||
|
the user with hardware that isn't there). Saving PUTs the merged
|
||||||
|
config to `/api/aux/config/save`, which writes aux.json and pushes
|
||||||
|
`HOMECFG` to the ESP. A status line shows whether the axis is
|
||||||
|
disabled / offline / connected-unhomed / homed at `<pos> mm`.
|
||||||
|
|
||||||
|
## State surface
|
||||||
|
|
||||||
|
These are pushed via `state.set` and visible in the websocket stream:
|
||||||
|
|
||||||
|
- `aux_enabled` - bool, axis is configured + enabled
|
||||||
|
- `aux_present` - bool, ESP responding on serial
|
||||||
|
- `aux_homed` - bool, has been homed since last ESP reset
|
||||||
|
- `aux_pos` - float, current W in mm (4 decimals)
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
- **ESP reboots mid-job**: `[boot] auxcnc v=N` banner -> `aux_homed`
|
||||||
|
cleared, message added: "W axis controller restarted - re-home
|
||||||
|
before use". Subsequent W moves still run; if you want a hard fail
|
||||||
|
instead, that's a one-line change in `_require_present`.
|
||||||
|
- **Limit switch closed at boot of HOME**: `[home] failed
|
||||||
|
reason=already_at_limit` -> hook raises -> Mach surfaces error.
|
||||||
|
- **Pause mid-W-move**: the hook is blocking, so feed-hold takes
|
||||||
|
effect *after* the W move completes. For an immediate stop hit
|
||||||
|
estop; the Hooks listener will call `aux.abort()` which sends
|
||||||
|
`ABORT\n` to the ESP and the step-pulse loop exits.
|
||||||
|
- **Connection loss**: if `/dev/ttyUSB0` can't be opened at startup,
|
||||||
|
`aux_present=False` and any G-code with W will fail-fast at the
|
||||||
|
hook handler with "Aux axis not connected".
|
||||||
|
- **No home enforcement**: per design, manual jogs and W moves are
|
||||||
|
allowed even without a successful home. Soft limits still apply
|
||||||
|
unless you use the raw step jog endpoint.
|
||||||
|
|
||||||
|
## Files added/changed
|
||||||
|
|
||||||
|
- `src/py/bbctrl/AuxAxis.py` (new): serial worker + RPC layer
|
||||||
|
- `src/py/bbctrl/AuxPreprocessor.py` (new): G-code rewriter
|
||||||
|
- `src/py/bbctrl/Hooks.py`: register_internal(), fix the messages
|
||||||
|
listener so `(MSG,HOOK:...)` actually fires
|
||||||
|
- `src/py/bbctrl/Ctrl.py`: instantiate AuxAxis, register hooks
|
||||||
|
- `src/py/bbctrl/Mach.py`: rewrite MDI commands containing W
|
||||||
|
- `src/py/bbctrl/FileHandler.py`: rewrite uploads in place
|
||||||
|
- `src/py/bbctrl/Web.py`: REST endpoints
|
||||||
|
- `src/py/bbctrl/__init__.py`: export AuxAxis
|
||||||
|
- `src/pug/templates/control-view.pug`: W jog row + DRO row
|
||||||
|
- `src/js/control-view.js`: aux_home / aux_jog / aux_jog_incr handlers
|
||||||
|
- `src/js/axis-vars.js`: `_compute_aux_axis` for W state
|
||||||
|
- `src/svelte-components/src/components/WAxisSettings.svelte`: settings panel
|
||||||
|
- `src/svelte-components/src/components/SettingsView.svelte`: hosts WAxisSettings
|
||||||
|
- `auxcnc/src/main.cpp`: new commands HOME, HOMECFG, WPOS, HOMED?,
|
||||||
|
LIMIT?, ABORT-able STEPS with limit-aware abort, trapezoidal ramps,
|
||||||
|
NVS-persisted config, `[boot]` banner, deterministic reply tokens
|
||||||
900
docs/mocks/v09_full_ux.html
Normal file
900
docs/mocks/v09_full_ux.html
Normal file
@@ -0,0 +1,900 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Onefinity · V09 · Full UX</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{margin:0;font-family:'Inter',system-ui,sans-serif;background:#0f172a;color:#e5e7eb}
|
||||||
|
.mono{font-family:'JetBrains Mono',monospace}
|
||||||
|
|
||||||
|
/* ---------- HOST CHROME ---------- */
|
||||||
|
.host{min-height:100vh;display:flex;flex-direction:column;background:radial-gradient(circle at 30% 0%,#374151,#0f172a 60%);}
|
||||||
|
.topbar{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;padding:.7rem 1rem;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.08);position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);}
|
||||||
|
.topbar .brand{display:flex;align-items:center;gap:.5rem;font-weight:800;color:#fff}
|
||||||
|
.stripe-logo-sm{background:repeating-linear-gradient(135deg,#a7c7a3 0 6px,transparent 6px 14px);width:26px;height:26px;border-radius:6px}
|
||||||
|
.pill{padding:.3rem .65rem;border-radius:9999px;font-size:.75rem;font-weight:700;background:rgba(255,255,255,.08);color:#cbd5e1}
|
||||||
|
.seg-host{display:inline-flex;background:rgba(255,255,255,.05);border-radius:9999px;padding:3px;gap:3px}
|
||||||
|
.seg-host button{padding:.4rem .85rem;border-radius:9999px;font-size:.78rem;font-weight:700;color:#cbd5e1}
|
||||||
|
.seg-host button.on{background:#fde047;color:#0f172a}
|
||||||
|
.toggle{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .7rem;border-radius:8px;background:rgba(255,255,255,.08);font-size:.75rem;font-weight:600;color:#e5e7eb;cursor:pointer}
|
||||||
|
.toggle.on{background:#22c55e;color:#0b1220}
|
||||||
|
|
||||||
|
.stage{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:1rem;overflow:auto}
|
||||||
|
.scaler-viewport{position:relative;flex:0 0 auto}
|
||||||
|
.scaler{position:absolute;top:0;left:0;width:1920px;height:auto;transform-origin:top left;transition:transform .2s}
|
||||||
|
|
||||||
|
/* ---------- KIOSK (1920x1080) ---------- */
|
||||||
|
.kiosk{
|
||||||
|
width:1920px;height:1080px;overflow:hidden;border-radius:14px;position:relative;
|
||||||
|
box-shadow:0 30px 60px rgba(0,0,0,.5);
|
||||||
|
display:flex;flex-direction:column;
|
||||||
|
background:#ffffff;color:#0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.head{
|
||||||
|
flex:0 0 96px;height:96px;
|
||||||
|
display:flex;align-items:center;gap:18px;
|
||||||
|
padding:0 24px;background:#ffffff;border-bottom:1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.brand-blk{display:flex;align-items:center;gap:14px}
|
||||||
|
.menu-btn{width:54px;height:54px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;color:#0f172a;display:inline-flex;align-items:center;justify-content:center;font-size:1.1rem}
|
||||||
|
.menu-btn:hover{background:#e2e8f0}
|
||||||
|
.brand-logo{width:42px;height:42px;border-radius:8px;background:repeating-linear-gradient(135deg,#a7c7a3 0 6px,transparent 6px 14px)}
|
||||||
|
.brand-name{font-weight:900;font-size:22px;letter-spacing:-.01em}
|
||||||
|
|
||||||
|
/* Underline-ribbon tab style (V02) */
|
||||||
|
.kiosk-tabs{display:inline-flex;gap:0;margin-right:auto;padding-left:18px;align-items:stretch;height:96px}
|
||||||
|
.ktab{
|
||||||
|
position:relative;
|
||||||
|
height:96px;padding:0 26px;
|
||||||
|
background:transparent;border:none;border-radius:0;
|
||||||
|
color:#475569;font-size:1.05rem;font-weight:700;
|
||||||
|
display:inline-flex;align-items:center;gap:.55rem;cursor:pointer;
|
||||||
|
transition:color .15s;
|
||||||
|
}
|
||||||
|
.ktab i{font-size:1.1rem;color:#94a3b8;transition:color .15s}
|
||||||
|
.ktab:hover{color:#0f172a}
|
||||||
|
.ktab:hover i{color:#475569}
|
||||||
|
.ktab.active{color:#0f172a}
|
||||||
|
.ktab.active i{color:#0f172a}
|
||||||
|
.ktab.active::after{
|
||||||
|
content:"";position:absolute;left:14px;right:14px;bottom:0;
|
||||||
|
height:5px;background:#fde047;border-radius:5px 5px 0 0;
|
||||||
|
}
|
||||||
|
.ktab .ktab-badge{background:#fee2e2;color:#991b1b;font-size:.7rem;padding:3px 8px;border-radius:9999px;font-weight:800;line-height:1}
|
||||||
|
.ktab.active .ktab-badge{background:#fde047;color:#0f172a}
|
||||||
|
|
||||||
|
.sys-btn{display:inline-flex;align-items:center;gap:.55rem;height:54px;padding:0 1.1rem;border-radius:14px;background:#f1f5f9;border:1px solid #e2e8f0;color:#0f172a;font-size:.9rem;font-weight:600}
|
||||||
|
.sys-btn .pip{width:9px;height:9px;border-radius:9999px;background:#22c55e}
|
||||||
|
.state-badge{display:inline-flex;align-items:center;gap:.6rem;height:54px;padding:0 1.1rem;border-radius:14px;background:#dcfce7;color:#166534;font-weight:800;font-size:1rem;letter-spacing:.04em}
|
||||||
|
.state-badge .dot{width:10px;height:10px;border-radius:9999px;background:currentColor;position:relative}
|
||||||
|
.state-badge .dot::after{content:"";position:absolute;inset:-3px;border-radius:9999px;border:2px solid currentColor;opacity:.5;animation:pls 1.6s ease-out infinite}
|
||||||
|
@keyframes pls{0%{transform:scale(.7);opacity:.6}100%{transform:scale(2.2);opacity:0}}
|
||||||
|
|
||||||
|
.estop{
|
||||||
|
width:88px;height:88px;background:#dc2626;color:#fff;font-weight:900;
|
||||||
|
clip-path:polygon(30% 0,70% 0,100% 30%,100% 70%,70% 100%,30% 100%,0 70%,0 30%);
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
border:3px solid #fff;box-shadow:0 0 0 3px #b91c1c, 0 8px 20px rgba(220,38,38,.35);font-size:1rem;letter-spacing:.05em
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body */
|
||||||
|
.body{flex:1;display:flex;flex-direction:column;background:#f1f5f9;min-height:0}
|
||||||
|
.panel{display:none;flex:1;min-height:0;flex-direction:column;padding:18px;gap:14px}
|
||||||
|
.panel.active{display:flex}
|
||||||
|
|
||||||
|
/* ----------------------- V09 jog/macro palette ----------------------- */
|
||||||
|
/* Flat soft slate, no shadow */
|
||||||
|
:root{
|
||||||
|
--jog-bg:#3f4b63;
|
||||||
|
--jog-hover:#4a5777;
|
||||||
|
--jog-dir-bg:#5b6885;
|
||||||
|
--jog-dir-hover:#6a779a;
|
||||||
|
--jog-ghost-bg:#8c97ad;
|
||||||
|
--jog-ghost-hover:#9ba6bb;
|
||||||
|
--jog-ink:#fff;
|
||||||
|
--jog-ghost-ink:#0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JOG */
|
||||||
|
.jog-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;display:flex;flex-direction:column;padding:18px;min-height:0}
|
||||||
|
.jog-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
|
||||||
|
.jog-title{font-size:18px;font-weight:700;color:#0f172a}
|
||||||
|
.jog-title .step{color:#0ea5e9;font-family:'JetBrains Mono',monospace}
|
||||||
|
.step-seg{display:inline-flex;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:14px;padding:4px}
|
||||||
|
.step-seg button{height:48px;min-width:64px;padding:0 1rem;border-radius:11px;font-size:1rem;font-weight:800;color:#475569;cursor:pointer}
|
||||||
|
.step-seg button.active{background:#0f172a;color:#fde047}
|
||||||
|
.jog-grid{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(4,1fr);gap:10px;flex:1;min-height:0}
|
||||||
|
.jbtn{
|
||||||
|
border-radius:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
|
||||||
|
user-select:none;-webkit-tap-highlight-color:transparent;cursor:pointer;
|
||||||
|
font-weight:700;font-size:1.05rem;border:none;
|
||||||
|
transition:transform .06s, background .15s;
|
||||||
|
background:var(--jog-bg);color:var(--jog-ink);
|
||||||
|
}
|
||||||
|
.jbtn:hover{background:var(--jog-hover)}
|
||||||
|
.jbtn:active{transform:scale(.97)}
|
||||||
|
.jbtn .ico{font-size:1.6rem}
|
||||||
|
.jbtn .lbl{font-size:.8rem;color:inherit;opacity:.85;font-weight:600}
|
||||||
|
.jbtn.dir{background:var(--jog-dir-bg)} .jbtn.dir:hover{background:var(--jog-dir-hover)}
|
||||||
|
.jbtn.ghost{background:var(--jog-ghost-bg);color:var(--jog-ghost-ink)} .jbtn.ghost:hover{background:var(--jog-ghost-hover)}
|
||||||
|
|
||||||
|
/* DRO + STATUS */
|
||||||
|
.control-grid{display:grid;grid-template-columns:720px 1fr;gap:18px;flex:1;min-height:0}
|
||||||
|
.right-col{display:grid;grid-template-rows:1fr 158px;gap:18px;min-height:0}
|
||||||
|
.dro-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.dro-head{display:grid;grid-template-columns:84px 1.4fr 1fr 1fr 170px 170px 280px;column-gap:.75rem;align-items:center;padding:14px 22px;background:#f8fafc;border-bottom:1px solid #e5e7eb;font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#94a3b8}
|
||||||
|
.dro-row{display:grid;grid-template-columns:84px 1.4fr 1fr 1fr 170px 170px 280px;column-gap:.75rem;align-items:center;padding:14px 22px;border-bottom:1px solid #f1f5f9;flex:1;min-height:0}
|
||||||
|
.dro-row:last-child{border-bottom:none}
|
||||||
|
.dro-axis{font-weight:900;font-size:46px;line-height:1}
|
||||||
|
.dro-pos{font-family:'JetBrains Mono',monospace;font-size:36px;font-weight:800}
|
||||||
|
.dro-pos .u{font-size:14px;color:#94a3b8;font-weight:500;margin-left:6px}
|
||||||
|
.dro-sec{font-family:'JetBrains Mono',monospace;font-size:18px;color:#64748b;font-weight:600}
|
||||||
|
.axis-x{color:#dc2626} .axis-y{color:#16a34a} .axis-z{color:#2563eb} .axis-w{color:#7c3aed}
|
||||||
|
|
||||||
|
.chip{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .7rem;border-radius:9999px;font-size:.78rem;font-weight:700}
|
||||||
|
.chip-green{background:#dcfce7;color:#166534}
|
||||||
|
.chip-amber{background:#fef3c7;color:#92400e}
|
||||||
|
.chip-red{background:#fee2e2;color:#991b1b}
|
||||||
|
.chip-slate{background:#e2e8f0;color:#334155}
|
||||||
|
.chip-blue{background:#dbeafe;color:#1e40af}
|
||||||
|
|
||||||
|
.icon-btn{
|
||||||
|
width:72px;height:72px;border-radius:14px;cursor:pointer;
|
||||||
|
display:inline-flex;align-items:center;justify-content:center;
|
||||||
|
color:#334155;background:#f1f5f9;border:1px solid #e2e8f0;
|
||||||
|
font-size:1.45rem
|
||||||
|
}
|
||||||
|
.icon-btn:hover{background:#e2e8f0}
|
||||||
|
.actions-cell{display:flex;justify-content:flex-end;gap:10px}
|
||||||
|
.z-highlight{background:rgba(254,243,199,.4)}
|
||||||
|
|
||||||
|
.status-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;min-height:0}
|
||||||
|
.stat-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;padding:18px 22px;display:flex;flex-direction:column;justify-content:center}
|
||||||
|
.stat-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.14em;color:#94a3b8}
|
||||||
|
.stat-val{font-family:'JetBrains Mono',monospace;font-size:30px;font-weight:800;margin-top:6px}
|
||||||
|
.stat-val.ok{color:#166534}
|
||||||
|
.stat-sub{font-size:13px;color:#64748b;margin-top:2px}
|
||||||
|
|
||||||
|
/* MACROS */
|
||||||
|
.macro-row{display:grid;grid-template-columns:repeat(8,1fr);gap:12px;flex:0 0 auto}
|
||||||
|
.macro-btn{
|
||||||
|
height:84px;border-radius:14px;border:none;cursor:pointer;
|
||||||
|
color:#fff;background:#3f4b63;
|
||||||
|
font-weight:800;font-size:1rem;
|
||||||
|
display:flex;align-items:center;justify-content:center;gap:.6rem;
|
||||||
|
transition:transform .06s, background .15s
|
||||||
|
}
|
||||||
|
.macro-btn:hover{background:#4a5777}
|
||||||
|
.macro-btn:active{transform:translateY(2px)}
|
||||||
|
.macro-btn .mnum{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px;background:#fde047;color:#0f172a;font-size:.85rem;font-weight:900}
|
||||||
|
.macro-btn .micon{font-size:1.1rem;opacity:.75}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
PROGRAM PANEL
|
||||||
|
============================================================= */
|
||||||
|
.program-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
|
||||||
|
.ptab-bar{display:flex;align-items:center;gap:6px;border-bottom:1px solid #e5e7eb;flex:0 0 auto;background:#fff;padding:0 18px}
|
||||||
|
.ptab{height:60px;padding:0 22px;font-weight:700;color:#64748b;border-bottom:3px solid transparent;font-size:1rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
|
||||||
|
.ptab:hover{color:#0f172a}
|
||||||
|
.ptab.active{color:#0f172a;border-bottom-color:#0f172a}
|
||||||
|
.ptab .ptab-badge{background:#fde047;color:#0f172a;font-size:.7rem;padding:2px 7px;border-radius:9999px;font-weight:900}
|
||||||
|
|
||||||
|
.action-bar{display:flex;align-items:center;gap:12px;padding:18px;flex-wrap:wrap;border-bottom:1px solid #f1f5f9}
|
||||||
|
.action-btn{height:84px;padding:0 24px;border-radius:14px;background:#3f4b63;color:#fff;border:none;cursor:pointer;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-weight:800;font-size:.9rem;letter-spacing:.04em;transition:background .15s}
|
||||||
|
.action-btn:hover{background:#4a5777}
|
||||||
|
.action-btn .ico{font-size:1.4rem}
|
||||||
|
.action-btn.run{background:#16a34a}
|
||||||
|
.action-btn.run:hover{background:#15803d}
|
||||||
|
.action-btn.stop{background:#0f172a}
|
||||||
|
.action-btn.stop:hover{background:#1e293b}
|
||||||
|
.action-btn.danger{background:#fee2e2;color:#7f1d1d}
|
||||||
|
.action-btn.danger:hover{background:#fecaca}
|
||||||
|
.action-btn.danger .ico{color:#dc2626}
|
||||||
|
|
||||||
|
.file-bar{display:flex;align-items:center;gap:10px;padding:14px 18px;flex-wrap:wrap;border-bottom:1px solid #f1f5f9}
|
||||||
|
.file-btn{height:54px;padding:0 18px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;font-weight:700;color:#0f172a;font-size:.9rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
|
||||||
|
.file-btn:hover{background:#e2e8f0}
|
||||||
|
.file-select{height:54px;padding:0 16px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-weight:600;color:#0f172a;font-size:.9rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
|
||||||
|
.file-select .caret{color:#94a3b8;margin-left:.5rem}
|
||||||
|
.file-select.primary{background:#fff;border:2px solid #0ea5e9;flex:1;min-width:300px}
|
||||||
|
|
||||||
|
.program-body{flex:1;display:grid;grid-template-columns:1fr 600px;min-height:0}
|
||||||
|
.gcode{font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.6;background:#fafafa;border-right:1px solid #f1f5f9;padding:14px 0;overflow:auto;color:#1e293b}
|
||||||
|
.gline{display:grid;grid-template-columns:60px 1fr;gap:14px;padding:1px 18px 1px 0}
|
||||||
|
.gline:nth-child(odd){background:#f4f4f5}
|
||||||
|
.gline .gn{color:#f59e0b;text-align:right;font-weight:700}
|
||||||
|
.gline.cur{background:#dbeafe !important}
|
||||||
|
.gline.cur .gn{color:#1e40af}
|
||||||
|
.gcomment{color:#64748b}
|
||||||
|
.gword{color:#0f172a}
|
||||||
|
.gnum{color:#16a34a}
|
||||||
|
|
||||||
|
.viewer{display:flex;flex-direction:column;min-height:0}
|
||||||
|
.viewer-3d{flex:1;background:#0b1220;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center}
|
||||||
|
.viewer-tools{display:flex;gap:8px;padding:14px;border-top:1px solid #f1f5f9;background:#fff;flex-wrap:wrap}
|
||||||
|
.vtool{height:60px;width:60px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;color:#475569;display:inline-flex;align-items:center;justify-content:center;font-size:1.2rem;cursor:pointer}
|
||||||
|
.vtool:hover{background:#e2e8f0}
|
||||||
|
.vtool.on{background:#0f172a;color:#fff;border-color:#0f172a}
|
||||||
|
.vinfo{padding:14px 18px;background:#fff;font-size:13px;color:#64748b;border-top:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.vinfo .ext{color:#0f172a;font-weight:600}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
MESSAGES PANEL
|
||||||
|
============================================================= */
|
||||||
|
.messages{display:none;flex-direction:column;flex:1;min-height:0;padding:18px;gap:12px;overflow:auto}
|
||||||
|
.messages.active{display:flex}
|
||||||
|
.msg{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:18px 22px;display:grid;grid-template-columns:54px 1fr auto;gap:18px;align-items:flex-start}
|
||||||
|
.msg .mi{width:54px;height:54px;border-radius:12px;display:inline-flex;align-items:center;justify-content:center;font-size:1.4rem}
|
||||||
|
.msg.error{border-left:6px solid #dc2626}
|
||||||
|
.msg.error .mi{background:#fee2e2;color:#991b1b}
|
||||||
|
.msg.warn{border-left:6px solid #f59e0b}
|
||||||
|
.msg.warn .mi{background:#fef3c7;color:#92400e}
|
||||||
|
.msg.info{border-left:6px solid #0ea5e9}
|
||||||
|
.msg.info .mi{background:#dbeafe;color:#1e40af}
|
||||||
|
.msg.ok{border-left:6px solid #16a34a}
|
||||||
|
.msg.ok .mi{background:#dcfce7;color:#166534}
|
||||||
|
.msg .mtitle{font-weight:800;font-size:1.05rem;color:#0f172a}
|
||||||
|
.msg .mtime{font-size:.8rem;color:#94a3b8;margin-top:2px}
|
||||||
|
.msg .mbody{margin-top:6px;color:#475569;font-size:.95rem;line-height:1.5}
|
||||||
|
.msg .mbody .mono{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:.85rem}
|
||||||
|
.msg .mactions{display:flex;gap:8px}
|
||||||
|
.mbtn{height:48px;padding:0 16px;border-radius:10px;background:#f1f5f9;border:1px solid #e2e8f0;font-weight:700;color:#0f172a;font-size:.85rem;cursor:pointer}
|
||||||
|
.mbtn:hover{background:#e2e8f0}
|
||||||
|
.mbtn.primary{background:#0f172a;color:#fff;border-color:#0f172a}
|
||||||
|
.mbtn.primary:hover{background:#1e293b}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
INDICATORS PANEL
|
||||||
|
============================================================= */
|
||||||
|
.indicators{display:none;flex:1;min-height:0;padding:18px;gap:14px;overflow:auto;grid-template-columns:repeat(4,1fr);grid-auto-rows:min-content}
|
||||||
|
.indicators.active{display:grid}
|
||||||
|
.ind{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.ind-label{font-size:.8rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#94a3b8}
|
||||||
|
.ind-val{font-family:'JetBrains Mono',monospace;font-size:1.6rem;font-weight:800;color:#0f172a}
|
||||||
|
.ind-state{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:700;color:#475569}
|
||||||
|
.ind-state .dot{width:10px;height:10px;border-radius:9999px}
|
||||||
|
.ind .progress{height:8px;background:#f1f5f9;border-radius:9999px;overflow:hidden;margin-top:4px}
|
||||||
|
.ind .progress > div{height:100%;background:#0ea5e9}
|
||||||
|
.ind.full{grid-column:span 2}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
MDI PANEL
|
||||||
|
============================================================= */
|
||||||
|
.mdi{display:none;flex-direction:column;flex:1;min-height:0;padding:18px;gap:14px}
|
||||||
|
.mdi.active{display:flex}
|
||||||
|
.mdi-input{
|
||||||
|
background:#0b1220;color:#86efac;border:1px solid #1e293b;border-radius:14px;
|
||||||
|
padding:22px 24px;font-family:'JetBrains Mono',monospace;font-size:1.4rem;font-weight:600;
|
||||||
|
display:flex;align-items:center;gap:.6rem;
|
||||||
|
}
|
||||||
|
.mdi-input .prompt{color:#475569}
|
||||||
|
.mdi-input .cursor{display:inline-block;width:14px;height:1.4rem;background:#86efac;animation:blink 1s steps(2,end) infinite;vertical-align:middle}
|
||||||
|
@keyframes blink{50%{opacity:0}}
|
||||||
|
.mdi-keys{display:grid;grid-template-columns:repeat(8,1fr);gap:8px;flex:0 0 auto}
|
||||||
|
.mkey{height:64px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-weight:800;font-size:1.05rem;color:#0f172a;cursor:pointer;font-family:'JetBrains Mono',monospace}
|
||||||
|
.mkey:hover{background:#f1f5f9}
|
||||||
|
.mkey.send{background:#16a34a;color:#fff;border-color:#15803d;grid-column:span 2;font-family:'Inter',sans-serif;font-size:.95rem;letter-spacing:.04em}
|
||||||
|
.mkey.send:hover{background:#15803d}
|
||||||
|
.mkey.clear{background:#fee2e2;color:#7f1d1d;border-color:#fca5a5;font-family:'Inter',sans-serif;font-size:.95rem;letter-spacing:.04em}
|
||||||
|
.mdi-history{flex:1;background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:14px 18px;overflow:auto;font-family:'JetBrains Mono',monospace;font-size:.95rem}
|
||||||
|
.mdi-history .h-row{display:grid;grid-template-columns:80px 1fr auto;gap:14px;padding:6px 0;border-bottom:1px solid #f1f5f9;align-items:center}
|
||||||
|
.mdi-history .h-time{color:#94a3b8;font-size:.8rem}
|
||||||
|
.mdi-history .h-cmd{color:#0f172a;font-weight:700}
|
||||||
|
.mdi-history .h-status{color:#16a34a;font-weight:700;font-size:.8rem}
|
||||||
|
.mdi-history .h-status.err{color:#dc2626}
|
||||||
|
|
||||||
|
/* =============================================================
|
||||||
|
SETTINGS PANEL
|
||||||
|
============================================================= */
|
||||||
|
.settings{display:none;flex:1;min-height:0;padding:18px;gap:14px;overflow:auto;grid-template-columns:280px 1fr}
|
||||||
|
.settings.active{display:grid}
|
||||||
|
.set-side{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:10px;display:flex;flex-direction:column;gap:4px;height:fit-content}
|
||||||
|
.set-item{height:56px;padding:0 16px;border-radius:10px;display:flex;align-items:center;gap:.6rem;color:#475569;font-weight:700;cursor:pointer}
|
||||||
|
.set-item:hover{background:#f1f5f9}
|
||||||
|
.set-item.active{background:#0f172a;color:#fff}
|
||||||
|
.set-content{display:flex;flex-direction:column;gap:14px}
|
||||||
|
.set-card{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:22px}
|
||||||
|
.set-title{font-weight:800;font-size:1.1rem;color:#0f172a;margin-bottom:14px}
|
||||||
|
.set-row{display:grid;grid-template-columns:280px 1fr auto;gap:14px;align-items:center;padding:14px 0;border-bottom:1px solid #f1f5f9}
|
||||||
|
.set-row:last-child{border-bottom:none}
|
||||||
|
.set-row .label{font-weight:700;color:#0f172a;font-size:.95rem}
|
||||||
|
.set-row .desc{color:#64748b;font-size:.85rem;margin-top:2px}
|
||||||
|
.set-row .val{font-family:'JetBrains Mono',monospace;color:#475569}
|
||||||
|
.set-input{height:48px;padding:0 14px;border-radius:10px;border:1px solid #e2e8f0;background:#fff;font-family:'JetBrains Mono',monospace;font-size:.95rem;color:#0f172a;min-width:200px}
|
||||||
|
.set-toggle{width:54px;height:30px;border-radius:9999px;background:#cbd5e1;position:relative;cursor:pointer;transition:background .15s}
|
||||||
|
.set-toggle::after{content:"";position:absolute;left:3px;top:3px;width:24px;height:24px;border-radius:9999px;background:#fff;transition:transform .15s}
|
||||||
|
.set-toggle.on{background:#16a34a}
|
||||||
|
.set-toggle.on::after{transform:translateX(24px)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="host">
|
||||||
|
|
||||||
|
<div class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="stripe-logo-sm"></div>
|
||||||
|
ONEFINITY · V09 · Full UX preview
|
||||||
|
</div>
|
||||||
|
<span class="pill">Click the inner tabs to navigate</span>
|
||||||
|
<div style="margin-left:auto"></div>
|
||||||
|
<button id="oneToOne" class="toggle">1:1</button>
|
||||||
|
<button id="fitBtn" class="toggle on">Fit</button>
|
||||||
|
<span id="scaleInfo" class="pill mono">100%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage" id="stage">
|
||||||
|
<div class="scaler-viewport" id="viewport">
|
||||||
|
<div class="scaler" id="scaler">
|
||||||
|
|
||||||
|
<!-- ============= KIOSK ============= -->
|
||||||
|
<div class="kiosk">
|
||||||
|
<header class="head">
|
||||||
|
<div class="brand-blk">
|
||||||
|
<div class="brand-logo"></div>
|
||||||
|
<div class="brand-name">ONEFINITY</div>
|
||||||
|
</div>
|
||||||
|
<div class="kiosk-tabs">
|
||||||
|
<button class="ktab active" data-target="control"><i class="fa-solid fa-gamepad"></i> Control</button>
|
||||||
|
<button class="ktab" data-target="program"><i class="fa-solid fa-list-ol"></i> Program</button>
|
||||||
|
<button class="ktab" data-target="console"><i class="fa-solid fa-terminal"></i> Console <span class="ktab-badge">2</span></button>
|
||||||
|
<button class="ktab" data-target="settings"><i class="fa-solid fa-sliders"></i> Settings</button>
|
||||||
|
</div>
|
||||||
|
<button class="sys-btn"><span class="pip"></span> All systems · view <i class="fa-solid fa-chevron-down" style="font-size:10px;opacity:.6"></i></button>
|
||||||
|
<span class="state-badge"><span class="dot"></span> READY</span>
|
||||||
|
<button class="estop">STOP</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
|
||||||
|
<!-- ============= CONTROL ============= -->
|
||||||
|
<div class="panel active" data-panel="control">
|
||||||
|
<div class="control-grid">
|
||||||
|
<!-- jog -->
|
||||||
|
<div class="jog-card">
|
||||||
|
<div class="jog-head">
|
||||||
|
<div class="jog-title">Jog · step <span class="step">10mm</span></div>
|
||||||
|
<div class="step-seg">
|
||||||
|
<button>0.1</button><button>1</button><button class="active">10</button><button>100</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jog-grid">
|
||||||
|
<button class="jbtn dir"><i class="fa-solid fa-arrow-up ico" style="transform:rotate(-45deg)"></i></button>
|
||||||
|
<button class="jbtn">Y+</button>
|
||||||
|
<button class="jbtn dir"><i class="fa-solid fa-arrow-up ico" style="transform:rotate(45deg)"></i></button>
|
||||||
|
<button class="jbtn">Z+</button>
|
||||||
|
<button class="jbtn">X−</button>
|
||||||
|
<button class="jbtn ghost"><span class="lbl">XY</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
|
||||||
|
<button class="jbtn">X+</button>
|
||||||
|
<button class="jbtn ghost"><span class="lbl">Z</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
|
||||||
|
<button class="jbtn dir"><i class="fa-solid fa-arrow-down ico" style="transform:rotate(45deg)"></i></button>
|
||||||
|
<button class="jbtn">Y−</button>
|
||||||
|
<button class="jbtn dir"><i class="fa-solid fa-arrow-down ico" style="transform:rotate(-45deg)"></i></button>
|
||||||
|
<button class="jbtn">Z−</button>
|
||||||
|
<button class="jbtn"><i class="fa-solid fa-arrow-down ico"></i><span class="lbl">W−</span></button>
|
||||||
|
<button class="jbtn ghost"><span class="lbl">W</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
|
||||||
|
<button class="jbtn"><i class="fa-solid fa-arrow-up ico"></i><span class="lbl">W+</span></button>
|
||||||
|
<button class="jbtn"><i class="fa-solid fa-house ico"></i><span class="lbl">Home</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DRO + status -->
|
||||||
|
<div class="right-col">
|
||||||
|
<div class="dro-card">
|
||||||
|
<div class="dro-head">
|
||||||
|
<div>Axis</div><div>Position</div><div>Absolute</div><div>Offset</div><div>State</div><div>Toolpath</div><div style="text-align:right">Actions</div>
|
||||||
|
</div>
|
||||||
|
<div class="dro-row">
|
||||||
|
<div class="dro-axis axis-x">X</div>
|
||||||
|
<div class="dro-pos">0.000<span class="u">mm</span></div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
|
||||||
|
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
|
||||||
|
<div class="actions-cell">
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dro-row">
|
||||||
|
<div class="dro-axis axis-y">Y</div>
|
||||||
|
<div class="dro-pos">0.000<span class="u">mm</span></div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
|
||||||
|
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
|
||||||
|
<div class="actions-cell">
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dro-row z-highlight">
|
||||||
|
<div class="dro-axis axis-z">Z</div>
|
||||||
|
<div class="dro-pos">0.000<span class="u">mm</span></div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
|
||||||
|
<div><span class="chip chip-amber"><i class="fa-solid fa-triangle-exclamation"></i> Over</span></div>
|
||||||
|
<div class="actions-cell">
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dro-row">
|
||||||
|
<div class="dro-axis axis-w">W</div>
|
||||||
|
<div class="dro-pos">0.000<span class="u">mm</span></div>
|
||||||
|
<div class="dro-sec">0.000</div>
|
||||||
|
<div class="dro-sec" style="opacity:.4">—</div>
|
||||||
|
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
|
||||||
|
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
|
||||||
|
<div class="actions-cell">
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
|
||||||
|
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-strip">
|
||||||
|
<div class="stat-card"><div class="stat-label">State</div><div class="stat-val ok">READY</div><div class="stat-sub">No alerts</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-label">Velocity / Feed</div><div class="stat-val">0 · 0</div><div class="stat-sub">m/min · mm/min</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-label">Spindle</div><div class="stat-val">0 (0)</div><div class="stat-sub">RPM (commanded / actual)</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-label">Job</div><div class="stat-val">0 / 1,785</div><div class="stat-sub">Line · 19:07 remaining</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- macros -->
|
||||||
|
<div class="macro-row">
|
||||||
|
<button class="macro-btn"><span class="mnum">1</span><i class="fa-solid fa-circle-play micon"></i> Macro 1</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">2</span><i class="fa-solid fa-circle-play micon"></i> Macro 2</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">3</span><i class="fa-solid fa-circle-play micon"></i> Macro 3</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">4</span><i class="fa-solid fa-circle-play micon"></i> Macro 4</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">5</span><i class="fa-solid fa-circle-play micon"></i> Macro 5</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">6</span><i class="fa-solid fa-circle-play micon"></i> Macro 6</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">7</span><i class="fa-solid fa-circle-play micon"></i> Macro 7</button>
|
||||||
|
<button class="macro-btn"><span class="mnum">8</span><i class="fa-solid fa-circle-play micon"></i> Macro 8</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============= PROGRAM ============= -->
|
||||||
|
<div class="panel" data-panel="program" style="padding:0;gap:0">
|
||||||
|
<div class="program-card" style="margin:18px;border-radius:18px">
|
||||||
|
<!-- Auto sub-panel -->
|
||||||
|
<div class="auto-sub" data-sub="auto" style="display:flex;flex-direction:column;flex:1;min-height:0">
|
||||||
|
<div class="action-bar">
|
||||||
|
<button class="action-btn run"><i class="fa-solid fa-play ico"></i><span>RUN</span></button>
|
||||||
|
<button class="action-btn stop"><i class="fa-solid fa-stop ico"></i><span>STOP</span></button>
|
||||||
|
<button class="action-btn"><i class="fa-solid fa-folder-arrow-up ico"></i><span>UPLOAD FOLDER</span></button>
|
||||||
|
<button class="action-btn"><i class="fa-solid fa-file-arrow-up ico"></i><span>UPLOAD FILE</span></button>
|
||||||
|
<button class="action-btn"><i class="fa-solid fa-file-arrow-down ico"></i><span>DOWNLOAD FILE</span></button>
|
||||||
|
<button class="action-btn danger"><i class="fa-solid fa-trash ico"></i><span>DELETE</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-bar">
|
||||||
|
<button class="file-btn"><i class="fa-solid fa-folder-plus"></i> Create Folder</button>
|
||||||
|
<button class="file-btn"><i class="fa-solid fa-folder-minus"></i> Delete Folder</button>
|
||||||
|
<span class="file-select"><i class="fa-solid fa-folder-open" style="color:#64748b"></i> Default folder <i class="fa-solid fa-chevron-down caret"></i></span>
|
||||||
|
<span class="file-select primary"><i class="fa-solid fa-file-code" style="color:#0ea5e9"></i> thin-rough.nc <i class="fa-solid fa-chevron-down caret" style="margin-left:auto"></i></span>
|
||||||
|
<span class="file-select"><i class="fa-solid fa-arrow-down-wide-short" style="color:#64748b"></i> By Upload Date <i class="fa-solid fa-chevron-down caret"></i></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="program-body">
|
||||||
|
<div class="gcode" id="gcode-list"></div>
|
||||||
|
<div class="viewer">
|
||||||
|
<div class="viewer-3d">
|
||||||
|
<svg viewBox="0 0 400 220" style="width:100%;height:100%">
|
||||||
|
<defs>
|
||||||
|
<pattern id="gridv" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#1e293b" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="400" height="220" fill="url(#gridv)"/>
|
||||||
|
<rect x="40" y="80" width="320" height="60" stroke="#475569" stroke-width="1" fill="none" stroke-dasharray="3 3"/>
|
||||||
|
<text x="40" y="74" fill="#64748b" font-size="9" font-family="monospace">Stock: 250 × 25 × 16 mm</text>
|
||||||
|
<!-- toolpath -->
|
||||||
|
<path d="M40,110 L360,110 M40,100 L360,100 M40,120 L360,120 M40,90 L360,90 M40,130 L360,130" stroke="#22c55e" stroke-width="1.4" fill="none" opacity=".8"/>
|
||||||
|
<path d="M40,110 L40,80 L60,80 L60,110 M80,110 L80,80 L100,80 L100,110 M120,110 L120,80 L140,80 L140,110" stroke="#ef4444" stroke-width="1.4" fill="none" opacity=".8"/>
|
||||||
|
<circle cx="40" cy="110" r="3" fill="#22c55e"/>
|
||||||
|
<circle cx="360" cy="110" r="3" fill="#ef4444"/>
|
||||||
|
<text x="46" y="108" fill="#22c55e" font-size="8" font-family="monospace">START</text>
|
||||||
|
<text x="332" y="108" fill="#ef4444" font-size="8" font-family="monospace">END</text>
|
||||||
|
<!-- axes gizmo -->
|
||||||
|
<g transform="translate(28,196)">
|
||||||
|
<line x1="0" y1="0" x2="22" y2="0" stroke="#ef4444" stroke-width="2"/>
|
||||||
|
<line x1="0" y1="0" x2="0" y2="-22" stroke="#3b82f6" stroke-width="2"/>
|
||||||
|
<line x1="0" y1="0" x2="-12" y2="12" stroke="#22c55e" stroke-width="2"/>
|
||||||
|
<text x="24" y="4" fill="#ef4444" font-size="9" font-family="monospace">X</text>
|
||||||
|
<text x="-4" y="-26" fill="#3b82f6" font-size="9" font-family="monospace">Z</text>
|
||||||
|
<text x="-22" y="22" fill="#22c55e" font-size="9" font-family="monospace">Y</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="viewer-tools">
|
||||||
|
<button class="vtool" title="Fit"><i class="fa-solid fa-expand"></i></button>
|
||||||
|
<button class="vtool on" title="Tool"><i class="fa-solid fa-screwdriver-wrench"></i></button>
|
||||||
|
<button class="vtool" title="Stock"><i class="fa-solid fa-cube"></i></button>
|
||||||
|
<button class="vtool" title="Origin"><i class="fa-solid fa-up-right-and-down-left-from-center"></i></button>
|
||||||
|
<button class="vtool" title="Top"><i class="fa-solid fa-square"></i></button>
|
||||||
|
<button class="vtool" title="Front"><i class="fa-solid fa-square-full"></i></button>
|
||||||
|
<button class="vtool" title="Iso"><i class="fa-solid fa-cubes"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="vinfo">
|
||||||
|
<span><span class="ext">thin-rough.nc</span> · 1,785 lines · 12.4 KB</span>
|
||||||
|
<span class="mono">est. 19:07</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============= CONSOLE ============= -->
|
||||||
|
<div class="panel" data-panel="console" style="padding:0;gap:0">
|
||||||
|
<div class="program-card" style="margin:18px;border-radius:18px">
|
||||||
|
|
||||||
|
<div class="ptab-bar">
|
||||||
|
<button class="ptab active" data-ptab="mdi"><i class="fa-solid fa-keyboard"></i> MDI</button>
|
||||||
|
<button class="ptab" data-ptab="messages"><i class="fa-solid fa-comment-dots"></i> Messages <span class="ptab-badge">2</span></button>
|
||||||
|
<button class="ptab" data-ptab="indicators"><i class="fa-solid fa-bell"></i> Indicators</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MDI sub-panel -->
|
||||||
|
<div class="mdi active" data-sub="mdi">
|
||||||
|
<div class="mdi-input">
|
||||||
|
<span class="prompt">G></span>
|
||||||
|
<span class="mono">G0 X100 Y50 F2000</span>
|
||||||
|
<span class="cursor"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mdi-keys">
|
||||||
|
<button class="mkey">G0</button>
|
||||||
|
<button class="mkey">G1</button>
|
||||||
|
<button class="mkey">G2</button>
|
||||||
|
<button class="mkey">G3</button>
|
||||||
|
<button class="mkey">G28</button>
|
||||||
|
<button class="mkey">G92</button>
|
||||||
|
<button class="mkey">M3</button>
|
||||||
|
<button class="mkey">M5</button>
|
||||||
|
<button class="mkey">X</button>
|
||||||
|
<button class="mkey">Y</button>
|
||||||
|
<button class="mkey">Z</button>
|
||||||
|
<button class="mkey">W</button>
|
||||||
|
<button class="mkey">F</button>
|
||||||
|
<button class="mkey">S</button>
|
||||||
|
<button class="mkey clear">CLEAR</button>
|
||||||
|
<button class="mkey send">SEND ↵</button>
|
||||||
|
</div>
|
||||||
|
<div class="mdi-history">
|
||||||
|
<div class="h-row"><span class="h-time">19:42:11</span><span class="h-cmd">G21</span><span class="h-status">✓ ok</span></div>
|
||||||
|
<div class="h-row"><span class="h-time">19:42:14</span><span class="h-cmd">G90</span><span class="h-status">✓ ok</span></div>
|
||||||
|
<div class="h-row"><span class="h-time">19:43:02</span><span class="h-cmd">G0 Y12.800</span><span class="h-status">✓ ok</span></div>
|
||||||
|
<div class="h-row"><span class="h-time">19:43:08</span><span class="h-cmd">G0 Z19.040</span><span class="h-status">✓ ok</span></div>
|
||||||
|
<div class="h-row"><span class="h-time">19:43:30</span><span class="h-cmd">G1 Z-20 F800</span><span class="h-status err">✗ blocked: Z over travel</span></div>
|
||||||
|
<div class="h-row"><span class="h-time">19:44:01</span><span class="h-cmd">G0 Z5</span><span class="h-status">✓ ok</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages sub-panel -->
|
||||||
|
<div class="messages" data-sub="messages">
|
||||||
|
<div class="msg warn">
|
||||||
|
<div class="mi"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:baseline;gap:.6rem">
|
||||||
|
<div class="mtitle">Z toolpath exceeds soft-limit</div>
|
||||||
|
<div class="mtime">2 min ago · sticky</div>
|
||||||
|
</div>
|
||||||
|
<div class="mbody">Loaded program reaches <span class="mono">Z = -16.500</span>. Configured soft-limit is <span class="mono">Z = -15.000</span>. Adjust the Z origin or set a deeper soft-limit before running.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mactions">
|
||||||
|
<button class="mbtn">Open settings</button>
|
||||||
|
<button class="mbtn primary">Acknowledge</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg info">
|
||||||
|
<div class="mi"><i class="fa-solid fa-circle-info"></i></div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:baseline;gap:.6rem">
|
||||||
|
<div class="mtitle">Camera offline</div>
|
||||||
|
<div class="mtime">12 min ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="mbody">Camera at <span class="mono">10.1.10.55:8554</span> did not respond on last poll. Live preview disabled.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mactions">
|
||||||
|
<button class="mbtn">Retry</button>
|
||||||
|
<button class="mbtn">Dismiss</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg ok">
|
||||||
|
<div class="mi"><i class="fa-solid fa-check"></i></div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:baseline;gap:.6rem">
|
||||||
|
<div class="mtitle">File uploaded · thin-rough.nc</div>
|
||||||
|
<div class="mtime">21 min ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="mbody">1,785 lines · 12.4 KB · checksum verified.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mactions">
|
||||||
|
<button class="mbtn">Open</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg error">
|
||||||
|
<div class="mi"><i class="fa-solid fa-circle-xmark"></i></div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:baseline;gap:.6rem">
|
||||||
|
<div class="mtitle">WiFi: not connected</div>
|
||||||
|
<div class="mtime">1 h ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="mbody">Falling back to wired ethernet. SSID <span class="mono">workshop-2g</span> last seen 53 min ago.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mactions">
|
||||||
|
<button class="mbtn">Network…</button>
|
||||||
|
<button class="mbtn">Mute</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicators sub-panel -->
|
||||||
|
<div class="indicators" data-sub="indicators">
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Spindle Load</div>
|
||||||
|
<div class="ind-val">0 %</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> idle</div>
|
||||||
|
<div class="progress"><div style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Spindle Temp</div>
|
||||||
|
<div class="ind-val">24 °C</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> nominal</div>
|
||||||
|
<div class="progress"><div style="width:24%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Driver Voltage</div>
|
||||||
|
<div class="ind-val">48.1 V</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Coolant</div>
|
||||||
|
<div class="ind-val">OFF</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#94a3b8"></span> standby</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Limit X</div>
|
||||||
|
<div class="ind-val" style="color:#16a34a">CLEAR</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Limit Y</div>
|
||||||
|
<div class="ind-val" style="color:#16a34a">CLEAR</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Limit Z</div>
|
||||||
|
<div class="ind-val" style="color:#dc2626">BLOCKED</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#dc2626"></span> over-travel</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Probe</div>
|
||||||
|
<div class="ind-val">OPEN</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#94a3b8"></span> not contacted</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">E-Stop</div>
|
||||||
|
<div class="ind-val" style="color:#16a34a">RELEASED</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> safe</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Door</div>
|
||||||
|
<div class="ind-val">CLOSED</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Air Pressure</div>
|
||||||
|
<div class="ind-val">6.2 bar</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
|
||||||
|
<div class="progress"><div style="width:62%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ind">
|
||||||
|
<div class="ind-label">Vacuum</div>
|
||||||
|
<div class="ind-val">−0.81 bar</div>
|
||||||
|
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> hold</div>
|
||||||
|
<div class="progress"><div style="width:81%"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============= SETTINGS ============= -->
|
||||||
|
<div class="panel" data-panel="settings" style="padding:0;gap:0">
|
||||||
|
<div class="settings active" style="padding:18px">
|
||||||
|
<div class="set-side">
|
||||||
|
<div class="set-item active"><i class="fa-solid fa-display"></i> Display & Units</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-arrows-up-down-left-right"></i> Motion</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-bolt"></i> Spindle</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-shield-halved"></i> Safety / Soft-limits</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-network-wired"></i> Network</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-video"></i> Camera</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-keyboard"></i> Macros</div>
|
||||||
|
<div class="set-item"><i class="fa-solid fa-circle-info"></i> About</div>
|
||||||
|
</div>
|
||||||
|
<div class="set-content">
|
||||||
|
<div class="set-card">
|
||||||
|
<div class="set-title">Display & Units</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">Display Units</div>
|
||||||
|
<div class="desc">Position, feed and dimensions throughout the UI.</div>
|
||||||
|
</div>
|
||||||
|
<div><div class="step-seg" style="display:inline-flex"><button class="active">METRIC</button><button>IMPERIAL</button></div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">Decimal places</div>
|
||||||
|
<div class="desc">Position readout precision.</div>
|
||||||
|
</div>
|
||||||
|
<div><input class="set-input" value="3" /></div>
|
||||||
|
<div class="val">0–4</div>
|
||||||
|
</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">Pulse-dot animation</div>
|
||||||
|
<div class="desc">Animate status badges (ready, idle, alarm).</div>
|
||||||
|
</div>
|
||||||
|
<div><div class="set-toggle on"></div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">Theme</div>
|
||||||
|
<div class="desc">Pick a tile finish.</div>
|
||||||
|
</div>
|
||||||
|
<div><span class="file-select"><i class="fa-solid fa-palette" style="color:#64748b"></i> V09 · Flat soft slate <i class="fa-solid fa-chevron-down caret"></i></span></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-card">
|
||||||
|
<div class="set-title">Network</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">IP Address</div>
|
||||||
|
<div class="desc">Wired ethernet, DHCP.</div>
|
||||||
|
</div>
|
||||||
|
<div><span class="mono" style="font-size:1.05rem;font-weight:700">10.1.10.55</span></div>
|
||||||
|
<div><button class="mbtn">Edit</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">WiFi</div>
|
||||||
|
<div class="desc">Wireless network connection.</div>
|
||||||
|
</div>
|
||||||
|
<div><span class="chip chip-red"><i class="fa-solid fa-wifi"></i> Not connected</span></div>
|
||||||
|
<div><button class="mbtn primary">Configure</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="set-row">
|
||||||
|
<div>
|
||||||
|
<div class="label">Hostname</div>
|
||||||
|
<div class="desc">Used in mDNS / Bonjour discovery.</div>
|
||||||
|
</div>
|
||||||
|
<div><input class="set-input" value="onefinity-shop.local" style="width:300px" /></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ----- Build G-code list -----
|
||||||
|
const gcodeLines = [
|
||||||
|
[1,'G21','word'],[2,'; X = along blank, Z = tool entry from top, Y fixed','c'],
|
||||||
|
[3,'; Y fixed to blank center: 12.800','c'],[4,'; nominal rapid: 3200.0 mm/min','c'],
|
||||||
|
[5,'; stock top Z: -0.960','c'],[6,'; deepest allowed cut Z: -16.500','c'],
|
||||||
|
[7,'G21','word'],[8,'G90','word'],[9,'G0 Y12.800','word'],
|
||||||
|
[10,'G0 Z19.040','word'],[11,'; rough pass 1 radius=18.540','c'],
|
||||||
|
[12,'G0 X0.000','word'],[13,'G1 Z-0.710 F800.000','word cur'],
|
||||||
|
[14,'G1 Z-0.960 F200.000','word'],[15,'G4 P0.250','word'],
|
||||||
|
[16,'G1 X249.500 F200.000','word'],[17,'G4 P0.250','word'],
|
||||||
|
[18,'G0 Z19.040','word'],[19,'; rough pass 2 radius=17.540','c'],
|
||||||
|
[20,'G0 X0.000','word'],[21,'G1 Z-1.710 F800.000','word'],
|
||||||
|
[22,'G1 Z-1.960 F200.000','word'],[23,'G4 P0.250','word'],
|
||||||
|
[24,'G1 X249.500 F200.000','word'],[25,'G4 P0.250','word'],
|
||||||
|
[26,'G0 Z19.040','word'],[27,'; rough pass 3 radius=16.540','c'],
|
||||||
|
[28,'G0 X0.000','word'],[29,'G1 Z-2.710 F800.000','word'],
|
||||||
|
[30,'G1 Z-2.960 F200.000','word'],[31,'G4 P0.250','word'],
|
||||||
|
[32,'G1 X249.500 F200.000','word'],[33,'G4 P0.250','word'],
|
||||||
|
[34,'G0 Z19.040','word'],[35,'; rough pass 4 radius=15.540','c'],
|
||||||
|
];
|
||||||
|
document.getElementById('gcode-list').innerHTML = gcodeLines.map(([n,t,cls])=>{
|
||||||
|
const isComment = cls.includes('c');
|
||||||
|
const isCur = cls.includes('cur');
|
||||||
|
const cls2 = 'gline' + (isCur?' cur':'');
|
||||||
|
const inner = isComment ? `<span class="gcomment">${t}</span>` : `<span class="gword">${t}</span>`;
|
||||||
|
return `<div class="${cls2}"><span class="gn">${n}</span><span>${inner}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// ----- Top tab switching (Control / Program / Settings) -----
|
||||||
|
document.querySelectorAll('.ktab').forEach(b=>{
|
||||||
|
b.addEventListener('click', ()=>{
|
||||||
|
const target = b.dataset.target;
|
||||||
|
document.querySelectorAll('.ktab').forEach(x=>x.classList.remove('active'));
|
||||||
|
b.classList.add('active');
|
||||||
|
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
document.querySelector(`.panel[data-panel="${target}"]`).classList.add('active');
|
||||||
|
applyScale();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----- Console sub-tab switching (MDI / Messages / Indicators) -----
|
||||||
|
function showSub(name){
|
||||||
|
document.querySelectorAll('.ptab').forEach(x=>x.classList.toggle('active', x.dataset.ptab===name));
|
||||||
|
document.querySelectorAll('[data-sub]').forEach(s=>{
|
||||||
|
const on = s.dataset.sub===name;
|
||||||
|
if(s.classList.contains('messages') || s.classList.contains('indicators') || s.classList.contains('mdi')){
|
||||||
|
s.classList.toggle('active', on);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.ptab').forEach(b=>{
|
||||||
|
b.addEventListener('click', ()=>{ showSub(b.dataset.ptab); });
|
||||||
|
});
|
||||||
|
// Default Console sub: MDI active
|
||||||
|
document.querySelectorAll('.messages[data-sub], .indicators[data-sub]').forEach(s=>s.classList.remove('active'));
|
||||||
|
|
||||||
|
// ----- Scaling -----
|
||||||
|
const stage = document.getElementById('stage');
|
||||||
|
const scaler = document.getElementById('scaler');
|
||||||
|
const viewport = document.getElementById('viewport');
|
||||||
|
const fitBtn = document.getElementById('fitBtn');
|
||||||
|
const oneToOne = document.getElementById('oneToOne');
|
||||||
|
const scaleInfo = document.getElementById('scaleInfo');
|
||||||
|
let mode = 'fit';
|
||||||
|
function activeKioskHeight(){
|
||||||
|
const m = document.querySelector('.kiosk');
|
||||||
|
return m ? Math.max(1080, m.offsetHeight) : 1080;
|
||||||
|
}
|
||||||
|
function applyScale(){
|
||||||
|
let s;
|
||||||
|
if(mode==='1:1'){
|
||||||
|
s = 1; scaleInfo.textContent = '100% · 1920px wide';
|
||||||
|
} else {
|
||||||
|
const sw = stage.clientWidth - 32;
|
||||||
|
s = Math.min(sw/1920, 1);
|
||||||
|
scaleInfo.textContent = Math.round(s*100) + '% · 1920px wide';
|
||||||
|
}
|
||||||
|
const h = activeKioskHeight();
|
||||||
|
scaler.style.transform = `scale(${s})`;
|
||||||
|
viewport.style.width = (1920 * s) + 'px';
|
||||||
|
viewport.style.height = (h * s) + 'px';
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', applyScale);
|
||||||
|
fitBtn.addEventListener('click', ()=>{ mode='fit'; fitBtn.classList.add('on'); oneToOne.classList.remove('on'); applyScale(); });
|
||||||
|
oneToOne.addEventListener('click', ()=>{ mode='1:1'; oneToOne.classList.add('on'); fitBtn.classList.remove('on'); applyScale(); });
|
||||||
|
applyScale();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
169
plans/2026-04-30_ux_redesign.md
Normal file
169
plans/2026-04-30_ux_redesign.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# UX Redesign — Implementation Plan
|
||||||
|
|
||||||
|
Reference mock: `docs/mocks/v09_full_ux.html`
|
||||||
|
Target hardware: 10.8" portable monitor, 1920×1080, capacitive touch, Chrome fullscreen.
|
||||||
|
|
||||||
|
## 1. Goals
|
||||||
|
|
||||||
|
The redesign keeps every existing feature but reorganizes the page into a single-screen control surface for finger-touch use:
|
||||||
|
|
||||||
|
- A slim 96 px header replaces the 140 px nav-header. Only logo + ONEFINITY wordmark + tab bar + system pill + READY badge + octagonal STOP.
|
||||||
|
- 4 top-level sections accessed via underline-ribbon tabs in the header:
|
||||||
|
1. **Control** — jog pad, DRO table, status strip, macro row.
|
||||||
|
2. **Program** — Auto run controls, file actions, G-code listing, 3D viewer.
|
||||||
|
3. **Console** — MDI, Messages, Indicators (sub-tabs).
|
||||||
|
4. **Settings** — paged settings (replaces the Pure left rail).
|
||||||
|
- Touch targets ≥ 64 px (jog tiles 72 px, axis action icons 72 px, macro buttons 84 px).
|
||||||
|
- All action chip-soup (WiFi/Camera/Rotary/IP/Version) collapses into one "All systems · view" pill that opens a popover. Burger menu removed (Settings tab supersedes it).
|
||||||
|
- V09 jog/macro palette: flat soft slate (#3f4b63), no drop shadow; yellow (#fde047) accent for active states (step seg, tab underline, macro number badge).
|
||||||
|
- Spindle override / feed override sliders live in a bottom-edge drawer triggered by tapping the Spindle KPI tile (no permanent screen real estate).
|
||||||
|
- Hard cut: no `config.ui.layout` flag; the new shell replaces the old in a single release.
|
||||||
|
|
||||||
|
## 2. Scope of code change
|
||||||
|
|
||||||
|
The build is Pug + Stylus + Browserify Vue (Vue 1.x). `index.pug` defines the chrome; `src/pug/templates/*.pug` defines each view; `src/js/*.js` mirrors them as Vue components routed by `currentView` from the URL hash.
|
||||||
|
|
||||||
|
Files we will touch:
|
||||||
|
|
||||||
|
- `src/pug/index.pug` — replace `#layout / #menu / #main / .nav-header` with the new header + tab bar + body. Drop the burger and the side-menu include.
|
||||||
|
- `src/pug/templates/control-view.pug` — restructure into the new Control panel (jog grid + DRO table + status strip + macro row). MDI/Messages/Indicators move out.
|
||||||
|
- New `src/pug/templates/program-view.pug` — Auto sub-panel content (action bar, file bar, gcode-viewer, path-viewer).
|
||||||
|
- New `src/pug/templates/console-view.pug` — MDI / Messages / Indicators sub-tabs hosting existing `console.pug` and `indicators.pug` partials.
|
||||||
|
- `src/js/app.js` — extend `parse_hash` so `#program`, `#console`, `#settings` resolve; expose tab state for the header to highlight.
|
||||||
|
- `src/js/control-view.js` — keep jog/DRO logic, drop the Auto/MDI/Messages/Indicators internal `tab` state and template hooks.
|
||||||
|
- New `src/js/program-view.js`, `src/js/console-view.js` — extracted Vue components.
|
||||||
|
- `src/stylus/style.styl` — add `.app-shell`, `.head`, `.tabs-host`, `.ktab`, panel styles, V09 jog tokens. Keep legacy classes alive until templates fully migrated.
|
||||||
|
- `src/static/css/side-menu.css` — stop including in `index.pug`.
|
||||||
|
- Settings: keep `settings-view.pug`, `admin-general-view.pug`, `admin-network-view.pug`, `motor-view.pug`, `tool-view.pug`, `io-view.pug`, etc., and surface them through a left-rail navigator inside the Settings panel rather than the sidebar.
|
||||||
|
- Settings → Macros owns the full macro list (1…N). Control's macro row is a slice of the first 8; reordering happens in Settings.
|
||||||
|
|
||||||
|
## 3. Routing model
|
||||||
|
|
||||||
|
We keep the existing URL hash routing because everything in `src/js/app.js#parse_hash` and the deep-linked menu items (`#motor:0`, `#admin-network`, etc.) depend on it.
|
||||||
|
|
||||||
|
| URL hash | Top tab | Notes |
|
||||||
|
|-------------------------|------------|-------------------------------------------------------|
|
||||||
|
| `#control` | Control | Default |
|
||||||
|
| `#program` / `#program:auto` | Program | Auto sub-view (only sub-view for now) |
|
||||||
|
| `#console` / `#console:mdi` | Console | MDI default, also `:messages` and `:indicators` |
|
||||||
|
| `#settings` | Settings | Settings home (Display & Units) |
|
||||||
|
| `#admin-general`, `#admin-network`, `#motor:N`, `#tool`, `#io`, `#help`, `#cheat-sheet` | Settings | Existing routes remain, surfaced in the Settings left rail |
|
||||||
|
|
||||||
|
The header tab bar maps URL prefix → active tab. A tiny helper `topTabFromHash(hash)` lives in `app.js` and is reused by the header template.
|
||||||
|
|
||||||
|
## 4. Step-by-step
|
||||||
|
|
||||||
|
### Phase 1 — Mock parity (1–2 days)
|
||||||
|
1. Add `docs/mocks/v09_full_ux.html` (done) so anyone can preview the target.
|
||||||
|
2. Move the V09 palette into Stylus tokens at the top of `style.styl`:
|
||||||
|
```styl
|
||||||
|
$jog-bg = #3f4b63
|
||||||
|
$jog-hover = #4a5777
|
||||||
|
$jog-dir = #5b6885
|
||||||
|
$jog-ghost = #8c97ad
|
||||||
|
$accent = #fde047
|
||||||
|
$accent-ink = #0f172a
|
||||||
|
```
|
||||||
|
3. Build the header in `index.pug`:
|
||||||
|
```pug
|
||||||
|
.app-shell
|
||||||
|
header.head
|
||||||
|
.brand-blk
|
||||||
|
.brand-logo
|
||||||
|
.brand-name ONEFINITY
|
||||||
|
nav.tabs-host(role="tablist")
|
||||||
|
a.ktab(:class="{active: topTab === 'control'}", href="#control")
|
||||||
|
.fa.fa-gamepad
|
||||||
|
| Control
|
||||||
|
a.ktab(:class="{active: topTab === 'program'}", href="#program") …
|
||||||
|
a.ktab(:class="{active: topTab === 'console'}", href="#console") …
|
||||||
|
a.ktab(:class="{active: topTab === 'settings'}", href="#settings") …
|
||||||
|
button.sys-btn(@click="toggle_sys_popover") …
|
||||||
|
span.state-badge(:class="state_class")
|
||||||
|
estop(@click="estop")
|
||||||
|
```
|
||||||
|
4. Style the header tabs as **underline ribbon** (V02): transparent fills, slate-gray text, dark text + 5 px yellow underline on active. CSS already proven in the mock.
|
||||||
|
5. Move the rotary toggle and pi-temp warning into the system pill popover.
|
||||||
|
|
||||||
|
### Phase 2 — Control panel (2 days)
|
||||||
|
1. Rewrite the outer markup of `control-view.pug` to a CSS grid:
|
||||||
|
```
|
||||||
|
.control-grid → 720px jog-card | 1fr right-col(dro-card + status-strip)
|
||||||
|
```
|
||||||
|
Drop the `<table>`-based outer layout (axes table stays — it's a real data table).
|
||||||
|
2. Replace the legacy `<button>` elements in the jog table with `.jbtn` markup that pulls colors from `$jog-*` tokens. Keep the `@click="jog_fn(...)"` bindings unchanged.
|
||||||
|
3. Build the new `.step-seg` with the existing `jog_incr` model. The four buttons stay wired to `jog_incr = 'fine' | 'small' | 'medium' | 'large'`.
|
||||||
|
4. Build `.dro-card` from the existing `table.axes` markup. Each row gets the new 7-column grid; axis cells just need `.dro-axis`, `.dro-pos`, `.dro-sec` classes.
|
||||||
|
5. Move the four KPI tiles (`State / Velocity-Feed / Spindle / Job`) into `.status-strip`. Existing `state.v`, `state.feed`, `state.s`, `state.line` bindings are unchanged.
|
||||||
|
6. Move `.macros-div` into a `.macro-row` 8-column grid. The row binds to `config.macros.slice(0, 8)`; macros 9…N are editable and runnable only from Settings → Macros (no drawer in Control). Reordering in Settings changes which macros appear in the visible 8.
|
||||||
|
7. Drop the legacy `.tabs / #tab1 …` block from `control-view.pug` entirely.
|
||||||
|
|
||||||
|
### Phase 3 — Program panel (1.5 days)
|
||||||
|
1. New file `src/pug/templates/program-view.pug` with `.program-card` and the action / file bars.
|
||||||
|
2. Move the Auto bar (RUN, STOP, UPLOAD FOLDER, UPLOAD FILE, DOWNLOAD FILE, DELETE) and the file-select strip (Create Folder, Delete Folder, folder picker, file picker, sort) out of `control-view.pug` into here. Use the V09 button styles (`.action-btn`, `.action-btn.run`, `.action-btn.danger`, `.file-btn`, `.file-select`).
|
||||||
|
3. Embed `path-viewer` and `gcode-viewer` in `.program-body { 1fr 600px }`. Both Vue components render unchanged.
|
||||||
|
4. New `src/js/program-view.js` exporting the same data model the existing `Auto` tab uses (`gcode_files`, `state.selected`, `start_pause`, etc.). The fastest path: move the relevant computed/methods into a mixin `gcode-program-mixin.js` consumed by both old and new components during the migration.
|
||||||
|
5. Wire `<component :is="currentView + '-view'">` in `index.pug` to pick up `program-view`.
|
||||||
|
|
||||||
|
### Phase 4 — Console panel (1 day)
|
||||||
|
1. New `src/pug/templates/console-view.pug` with the inner `.ptab-bar` (MDI / Messages / Indicators) and `data-sub` panels.
|
||||||
|
2. The MDI panel reuses the existing `<input v-model="mdi" @keyup.enter="submit_mdi">` plus the on-screen keypad (G0/G1/G2/G3/G28/G92/M3/M5 + axis letters + CLEAR/SEND).
|
||||||
|
3. The Messages panel pulls from the existing `popupMessages` array + a new `messages_log` state we will accumulate from `app.js`'s `error` and `popupMessages` channels (no protocol change).
|
||||||
|
4. The Indicators panel mounts the existing `<indicators :state="state" :template="template">` component.
|
||||||
|
5. Sub-tab state is local Vue state (`activeSub: 'mdi' | 'messages' | 'indicators'`) plus URL fragment after `:` so deep links keep working.
|
||||||
|
|
||||||
|
### Phase 5 — Settings panel (1 day)
|
||||||
|
1. New `src/pug/templates/settings-view.pug` with a left rail and a content slot.
|
||||||
|
2. The left rail is data-driven from a list of existing settings views: General, Network, Motion (settings-view), Spindle (tool-view), Safety (admin-general subset), Camera, Macros (settings-view subset), I/O, Motors, Help, About.
|
||||||
|
3. The content slot uses `<component :is="settingsSub + '-view'">` so each existing pug template renders unchanged (`admin-general-view.pug`, `admin-network-view.pug`, `motor-view.pug`, `tool-view.pug`, `io-view.pug`, `settings-view.pug`, `help-view.pug`, `cheat-sheet-view.pug`).
|
||||||
|
4. Existing routes (`#admin-network`, `#motor:0`, …) resolve to Settings + the matching left-rail item. We lose nothing.
|
||||||
|
5. Decommission the side menu in `index.pug` and stop including `side-menu.css`.
|
||||||
|
|
||||||
|
### Phase 6 — Polish & rollout (0.5 days)
|
||||||
|
1. Pulse-dot animation for the READY badge (CSS keyframes already in the mock).
|
||||||
|
2. System pill popover content: WiFi state + button, Camera state + retry, Rotary toggle, IP address, firmware version, "Open Settings".
|
||||||
|
3. Disabled states: jog buttons + macro buttons honor `is_ready` like before; gray them out instead of hiding.
|
||||||
|
4. Decimal-places setting from the existing `display_units` plumbing — wire to a new `precision` config the DRO reads.
|
||||||
|
5. Build the **Spindle override drawer**: clicking the `.stat-card` for Spindle toggles `.override-drawer.open` anchored to the bottom edge of the body. The drawer hosts the two existing `<input type="range">` controls for `feed_override` and `speed_override` plus `Reset` buttons. Bind to the existing `override_feed` / `override_speed` methods.
|
||||||
|
6. **Hard cut cleanup:** delete the legacy `.nav-header`, side-menu markup, and the inline `.tabs / #tab1…#tab4` block from `control-view.pug`. Remove `src/static/css/side-menu.css` from `index.pug` includes. Sweep `style.styl` for orphan rules (`.nav-header`, `.brand`, `.menu-link`, `.pure-menu*` overrides, `.tabs > input` selectors) and delete them in the same commit so we don't ship dead CSS.
|
||||||
|
|
||||||
|
## 5. Migration risks & mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|----------------------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
|
| Existing deep links from PDFs / forum posts (`#admin-network`) break | Keep the same hashes; only their visual shell changes. `parse_hash` resolves them. |
|
||||||
|
| Vue 1.x doesn't support modern slot syntax we used in the mock | The mock is plain HTML for visual review; production code uses the existing Vue 1 patterns. No new Vue features required. |
|
||||||
|
| Touch monitor with HDMI vs USB-C may report different DPI | The new layout is fluid inside 1920 × 1080 only when fullscreen Chrome. Provide a CSS `@media (max-width: 1820px)` fallback that scales the macro row to 4 columns and stacks the right column under the jog. |
|
||||||
|
| Existing customers rely on muscle memory of the side menu | Settings tab opens directly to the same left-rail navigator. First-launch toast: "Side menu moved to Settings." |
|
||||||
|
| `path-viewer` / `gcode-viewer` are heavy three.js components | They live in the Program tab now; we lazy-mount with `v-if="currentView === 'program'"` so Control stays light. |
|
||||||
|
| MDI input could lose focus when the inner `.ptab` is switched | Keep the input mounted, just hide non-active subs with `display:none`. |
|
||||||
|
|
||||||
|
## 6. Testing checklist
|
||||||
|
|
||||||
|
- Chrome on the 10.8" 1920 × 1080 monitor, fullscreen — every panel fits without scrolling at 100 %.
|
||||||
|
- Chrome at 1366 × 768 — fallback layout works (Control collapses jog above DRO).
|
||||||
|
- Touch hit-tests: every interactive target ≥ 48 px on its shortest side, primary jog tiles ≥ 72 px.
|
||||||
|
- Existing flows still work end-to-end: home all axes, run a small program, MDI a `G0 X10`, switch to Imperial, upload a folder, delete a file.
|
||||||
|
- Hash routing: hand-type `#motor:1` and confirm Settings tab activates with Motor 1 selected.
|
||||||
|
- Spindle override drawer: tap Spindle KPI tile, sliders move feed/speed override, `Reset` returns both to 100 %, tile tap closes drawer.
|
||||||
|
- Macro row shows macros 1–8 only; reordering in Settings → Macros changes which 8 appear on Control.
|
||||||
|
- Pulse-dot animation respects `prefers-reduced-motion`.
|
||||||
|
- Hard-cut cleanup verified: `git grep` finds no references to the old `.nav-header`, `side-menu.css`, or the `#tab1…#tab4` selectors after the rename.
|
||||||
|
|
||||||
|
## 7. Estimated effort
|
||||||
|
|
||||||
|
About 6–7 working days for one developer:
|
||||||
|
|
||||||
|
1. Mock parity & header — 1.5 days
|
||||||
|
2. Control panel (incl. macro slice + DRO grid) — 2 days
|
||||||
|
3. Program panel — 1.5 days
|
||||||
|
4. Console panel — 1 day
|
||||||
|
5. Settings shell — 1 day
|
||||||
|
6. Override drawer, polish, hard-cut cleanup, regression tests — 0.5–1 day
|
||||||
|
|
||||||
|
## 8. Resolved decisions
|
||||||
|
|
||||||
|
- **Rollout: hard cut.** No `config.ui.layout` feature flag, no parallel legacy shell. The new `index.pug` tree replaces the old one in a single release; the old `.nav-header`, side menu, and embedded `.tabs` block are deleted (not gated). One pre-release internal QA pass on real hardware before tagging.
|
||||||
|
- **Macros above 8: Settings owns the master list; Control surfaces the first 8 (configurable).** The Control macro row reads from `config.macros[0..7]`; everything beyond index 7 is editable / runnable only from Settings → Macros. Users can reorder which macros land in the visible 8 there.
|
||||||
|
- **"Pin to Control" indicator slot: defer.** Not in this redesign. Tracked as a follow-up; current status strip stays fixed at State / Velocity·Feed / Spindle / Job.
|
||||||
|
- **Feed & spindle override: drawer triggered by the Spindle KPI tile.** The Spindle card in the status strip becomes tappable. Tap opens a bottom-edge drawer (≈ 220 px tall) containing the two existing range inputs (`feed_override`, `speed_override`) at touch-friendly size with `Reset to 100 %` buttons. Closes by tapping the tile again or the drawer chevron. No protocol change; reuses the existing `override_feed` / `override_speed` handlers.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
# Defaults:
|
# Defaults:
|
||||||
# HOST=onefinity.local
|
# HOST=onefinity.local
|
||||||
# REMOTE_USER=bbmc
|
# USER=bbmc
|
||||||
# PASSWORD=onefinity (used for sudo on the Pi)
|
# PASSWORD=onefinity (used for sudo on the Pi)
|
||||||
#
|
#
|
||||||
# Override:
|
# Override:
|
||||||
@@ -20,65 +20,46 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
HOST="${HOST:-onefinity.local}"
|
HOST="${HOST:-onefinity.local}"
|
||||||
|
# REMOTE_USER (not USER, which the shell pre-populates with the local
|
||||||
|
# logged-in account).
|
||||||
REMOTE_USER="${REMOTE_USER:-bbmc}"
|
REMOTE_USER="${REMOTE_USER:-bbmc}"
|
||||||
PASSWORD="${PASSWORD:-onefinity}"
|
PASSWORD="${PASSWORD:-onefinity}"
|
||||||
|
|
||||||
echo "Building UI bundle (HTML + resources)..."
|
echo "🛠 Building UI bundle..."
|
||||||
make build/http/index.html >/dev/null
|
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..."
|
# 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}" \
|
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")"
|
"ls -d /usr/local/lib/python*/dist-packages/bbctrl-*-py*.egg/bbctrl/http 2>/dev/null | head -1")"
|
||||||
if [[ -z "$REMOTE_HTTP_DIR" ]]; then
|
if [[ -z "$REMOTE_HTTP_DIR" ]]; then
|
||||||
echo "ERROR: could not find bbctrl http/ directory on $HOST"
|
echo "❌ Could not find bbctrl http/ directory on $HOST"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo " $REMOTE_HTTP_DIR"
|
echo " $REMOTE_HTTP_DIR"
|
||||||
|
|
||||||
echo "Rsyncing build/http/ -> $HOST:$REMOTE_HTTP_DIR/"
|
echo "🚚 Rsyncing build/http/ → $HOST:$REMOTE_HTTP_DIR/"
|
||||||
# Stage to a tmp dir owned by $REMOTE_USER, then sudo-rsync into
|
# Stage to a tmp dir owned by $REMOTE_USER, then sudo-mv into place.
|
||||||
# place. This avoids needing root over rsync. We do NOT use --delete
|
# This avoids needing root over rsync.
|
||||||
# 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_$$"
|
REMOTE_TMP="/tmp/onefin_ui_$$"
|
||||||
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" "mkdir -p '${REMOTE_TMP}'"
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" "mkdir -p '${REMOTE_TMP}'"
|
||||||
rsync -avz \
|
rsync -avz --delete \
|
||||||
--exclude='hostinfo.txt' \
|
--exclude='hostinfo.txt' \
|
||||||
-e "ssh -o ConnectTimeout=5" \
|
-e "ssh -o ConnectTimeout=5" \
|
||||||
build/http/ "${REMOTE_USER}@${HOST}:${REMOTE_TMP}/"
|
build/http/ "${REMOTE_USER}@${HOST}:${REMOTE_TMP}/"
|
||||||
|
|
||||||
echo "Installing into ${REMOTE_HTTP_DIR}/ (sudo)..."
|
echo "📦 Installing into ${REMOTE_HTTP_DIR}/ (sudo)..."
|
||||||
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
"echo '${PASSWORD}' | sudo -S bash -c '
|
"echo '${PASSWORD}' | sudo -S bash -c '
|
||||||
rsync -a --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \
|
rsync -a --delete --exclude=hostinfo.txt \"${REMOTE_TMP}/\" \"${REMOTE_HTTP_DIR}/\" \
|
||||||
&& rm -rf \"${REMOTE_TMP}\"
|
&& rm -rf \"${REMOTE_TMP}\"
|
||||||
'" 2>&1 | tail -3
|
'" 2>&1 | tail -3
|
||||||
|
|
||||||
# Patch bbctrl Web.py so font files get the correct MIME type. The
|
echo "🔁 Restarting bbctrl service..."
|
||||||
# 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}" \
|
ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \
|
||||||
"echo '${PASSWORD}' | sudo -S systemctl restart bbctrl" 2>&1 | tail -3
|
"echo '${PASSWORD}' | sudo -S systemctl restart bbctrl" 2>&1 | tail -3
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Deployed to http://${HOST}/"
|
echo "✅ Deployed to http://${HOST}/"
|
||||||
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
|
echo " Logs: ssh ${REMOTE_USER}@${HOST} 'journalctl -u bbctrl -f'"
|
||||||
echo " Open: open -a 'Google Chrome' http://${HOST}/"
|
echo " Open: open -a 'Google Chrome' http://${HOST}/"
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
# * The full V09 chrome (header tabs, settings rail, jog grid, DRO
|
# * The full V09 chrome (header tabs, settings rail, jog grid, DRO
|
||||||
# skeleton, status strip).
|
# skeleton, status strip).
|
||||||
# * A "DISCONNECTED" overlay because there's no controller backend.
|
# * 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
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@@ -28,4 +28,4 @@ plymouth quit
|
|||||||
|
|
||||||
# Start X in /home/pi
|
# Start X in /home/pi
|
||||||
cd /home/pi
|
cd /home/pi
|
||||||
sudo -u pi startx -- -nocursor
|
sudo -u pi startx
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ plymouth quit 2>/dev/null || true
|
|||||||
# late-boot units (bbctrl logrotate, etc.) don't block on it. Output
|
# late-boot units (bbctrl logrotate, etc.) don't block on it. Output
|
||||||
# is redirected so the journal doesn't fill up with X warnings.
|
# is redirected so the journal doesn't fill up with X warnings.
|
||||||
cd /home/pi
|
cd /home/pi
|
||||||
# `-- -nocursor` hides the X pointer; this is a touchscreen kiosk and
|
nohup sudo -u pi startx >/var/log/onefin-x.log 2>&1 &
|
||||||
# the mouse cursor only gets in the way.
|
|
||||||
nohup sudo -u pi startx -- -nocursor >/var/log/onefin-x.log 2>&1 &
|
|
||||||
disown
|
disown
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ sed -i 's/^PARTUUID=.*\//\/dev\/mmcblk0p2 \//' /etc/fstab
|
|||||||
|
|
||||||
# Enable browser in xorg
|
# Enable browser in xorg
|
||||||
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config
|
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config
|
||||||
echo "sudo -u pi startx -- -nocursor" >> /etc/rc.local
|
echo "sudo -u pi startx" >> /etc/rc.local
|
||||||
cp /mnt/host/xinitrc /home/pi/.xinitrc
|
cp /mnt/host/xinitrc /home/pi/.xinitrc
|
||||||
cp /mnt/host/ratpoisonrc /home/pi/.ratpoisonrc
|
cp /mnt/host/ratpoisonrc /home/pi/.ratpoisonrc
|
||||||
cp /mnt/host/xorg.conf /etc/X11/
|
cp /mnt/host/xorg.conf /etc/X11/
|
||||||
|
|||||||
@@ -251,19 +251,6 @@ module.exports = new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
// True when the UI is in kiosk mode — i.e. running on the
|
|
||||||
// controller's own onboard browser (Pi 3B at 1366x768) or
|
|
||||||
// explicitly forced via ?kiosk=1. Source-of-truth is the
|
|
||||||
// `kiosk-mode` class added to <html> by the inline script
|
|
||||||
// in index.pug, which already honors hostname + URL param +
|
|
||||||
// localStorage. The Pi's VideoCore IV is too slow for the
|
|
||||||
// three.js toolpath preview, so we suppress that panel in
|
|
||||||
// kiosk mode and let the gcode listing take the full width.
|
|
||||||
is_kiosk: function() {
|
|
||||||
return typeof document !== "undefined"
|
|
||||||
&& document.documentElement.classList.contains("kiosk-mode");
|
|
||||||
},
|
|
||||||
|
|
||||||
popupMessages: function() {
|
popupMessages: function() {
|
||||||
const msgs = [];
|
const msgs = [];
|
||||||
|
|
||||||
@@ -369,15 +356,6 @@ module.exports = new Vue({
|
|||||||
ready: function() {
|
ready: function() {
|
||||||
window.onhashchange = () => this.parse_hash();
|
window.onhashchange = () => this.parse_hash();
|
||||||
|
|
||||||
// Embedded Svelte subviews (A axis settings, etc.) signal
|
|
||||||
// unsaved changes via this event. The master Save button
|
|
||||||
// highlights when modified is true.
|
|
||||||
window.addEventListener("onefin:dirty", () => {
|
|
||||||
this.modified = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Resolve the initial route before the websocket connects so
|
// Resolve the initial route before the websocket connects so
|
||||||
// the shell shows the right view even on a slow / offline
|
// the shell shows the right view even on a slow / offline
|
||||||
// controller. update() will call parse_hash() again once the
|
// controller. update() will call parse_hash() again once the
|
||||||
@@ -387,8 +365,7 @@ module.exports = new Vue({
|
|||||||
// motion.*, etc.) and would throw on first paint with the
|
// motion.*, etc.) and would throw on first paint with the
|
||||||
// empty placeholder config.
|
// empty placeholder config.
|
||||||
const settingsFamily = [
|
const settingsFamily = [
|
||||||
"settings", "probing", "gcode",
|
"settings", "admin-general", "admin-network",
|
||||||
"admin-general", "admin-network",
|
|
||||||
"motor", "tool", "io", "macros",
|
"motor", "tool", "io", "macros",
|
||||||
"help", "cheat-sheet",
|
"help", "cheat-sheet",
|
||||||
];
|
];
|
||||||
@@ -487,12 +464,6 @@ module.exports = new Vue({
|
|||||||
toggle_rotary: async function(isActive) {
|
toggle_rotary: async function(isActive) {
|
||||||
try {
|
try {
|
||||||
await api.put("rotary", {status: isActive});
|
await api.put("rotary", {status: isActive});
|
||||||
// The /api/rotary endpoint rewrites motors[1]/[2]
|
|
||||||
// in config.json on the server. Refetch so the UI
|
|
||||||
// reflects the new motor config (otherwise the
|
|
||||||
// motor settings page keeps showing pre-toggle
|
|
||||||
// values until the next page reload).
|
|
||||||
await this.update();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert("Error occured");
|
alert("Error occured");
|
||||||
@@ -622,8 +593,7 @@ module.exports = new Vue({
|
|||||||
// Settings tab while keeping their existing top-level
|
// Settings tab while keeping their existing top-level
|
||||||
// hash. This preserves all existing deep links.
|
// hash. This preserves all existing deep links.
|
||||||
const settingsViews = [
|
const settingsViews = [
|
||||||
"settings", "probing", "gcode",
|
"settings", "admin-general", "admin-network",
|
||||||
"admin-general", "admin-network",
|
|
||||||
"motor", "tool", "io", "macros",
|
"motor", "tool", "io", "macros",
|
||||||
"help", "cheat-sheet",
|
"help", "cheat-sheet",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ module.exports = {
|
|||||||
return this._compute_axis("c");
|
return this._compute_axis("c");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
w: function() {
|
||||||
|
return this._compute_aux_axis();
|
||||||
|
},
|
||||||
|
|
||||||
axes: function() {
|
axes: function() {
|
||||||
return this._compute_axes();
|
return this._compute_axes();
|
||||||
}
|
}
|
||||||
@@ -185,7 +189,11 @@ module.exports = {
|
|||||||
_get_motor_id: function(axis) {
|
_get_motor_id: function(axis) {
|
||||||
for (let i = 0; i < this.config.motors.length; i++) {
|
for (let i = 0; i < this.config.motors.length; i++) {
|
||||||
const motor = this.config.motors[i];
|
const motor = this.config.motors[i];
|
||||||
if (motor.axis.toLowerCase() == axis) {
|
// motor.axis can be undefined on initial load before
|
||||||
|
// config has streamed in. Guard so the computed does
|
||||||
|
// not throw and bubble a Vue warning into the console.
|
||||||
|
if (motor && typeof motor.axis === "string" &&
|
||||||
|
motor.axis.toLowerCase() == axis) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,15 +202,79 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_check_is_enabled: function(axis){
|
_check_is_enabled: function(axis){
|
||||||
|
// Prefer config.motors[i].axis (always present once the
|
||||||
|
// config has loaded). Fall back to the per-motor state
|
||||||
|
// `Nan` field, which is what the legacy UI used. This
|
||||||
|
// avoids hiding axis rows during the brief window after
|
||||||
|
// config has loaded but before the controller has pushed
|
||||||
|
// its first state delta.
|
||||||
const axes = { x: 0, y: 1, z: 2, a: 3 };
|
const axes = { x: 0, y: 1, z: 2, a: 3 };
|
||||||
|
const wanted = axes[axis];
|
||||||
for (let i = 0; i < this.config.motors.length; i++) {
|
for (let i = 0; i < this.config.motors.length; i++) {
|
||||||
if(this.state[`${i}an`] == axes[axis]){
|
const motor = this.config.motors[i] || {};
|
||||||
|
if (typeof motor.axis === "string" &&
|
||||||
|
motor.axis.toLowerCase() == axis) {
|
||||||
|
return motor.enabled !== false;
|
||||||
|
}
|
||||||
|
// Only use the state Nan fallback for axes we know
|
||||||
|
// about (x/y/z/a). Otherwise undefined == undefined
|
||||||
|
// would mistakenly match every axis (b, c, ...).
|
||||||
|
if (typeof wanted === "number") {
|
||||||
|
const an = this.state[`${i}an`];
|
||||||
|
if (typeof an === "number" && an === wanted) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_compute_aux_axis: function() {
|
||||||
|
// Virtual W axis driven by the auxcnc ESP32. Position, homed
|
||||||
|
// flag and presence come from the bbctrl AuxAxis driver via
|
||||||
|
// state.aux_*. No motor mapping, no soft-limit warnings on
|
||||||
|
// toolpath bounds (auxcnc enforces its own).
|
||||||
|
const enabled = !!this.state.aux_enabled;
|
||||||
|
const present = !!this.state.aux_present;
|
||||||
|
const homed = !!this.state.aux_homed;
|
||||||
|
const pos = this.state.aux_pos || 0;
|
||||||
|
|
||||||
|
let klass = `${homed ? "homed" : "unhomed"} axis-w`;
|
||||||
|
let state = present ? "UNHOMED" : "OFFLINE";
|
||||||
|
let icon = present ? "question-circle" : "plug";
|
||||||
|
let title = present
|
||||||
|
? "Click the home button to home W axis."
|
||||||
|
: "Aux controller not connected on /dev/ttyUSB0.";
|
||||||
|
if (homed) {
|
||||||
|
state = "HOMED";
|
||||||
|
icon = "check-circle";
|
||||||
|
title = "W axis successfully homed.";
|
||||||
|
} else if (!present) {
|
||||||
|
klass += " error";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pos: pos,
|
||||||
|
abs: pos,
|
||||||
|
off: 0,
|
||||||
|
min: 0, max: 0, dim: 0,
|
||||||
|
pathMin: 0, pathMax: 0, pathDim: 0,
|
||||||
|
motor: -1,
|
||||||
|
enabled: enabled,
|
||||||
|
homingMode: "limit-switch",
|
||||||
|
homed: homed,
|
||||||
|
klass: klass,
|
||||||
|
state: state,
|
||||||
|
icon: icon,
|
||||||
|
title: title,
|
||||||
|
ticon: "check-circle",
|
||||||
|
tstate: "OK",
|
||||||
|
toolmsg: "W axis is not constrained by tool path bounds.",
|
||||||
|
tklass: `${homed ? "homed" : "unhomed"} axis-w`,
|
||||||
|
isAux: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
_compute_axes: function() {
|
_compute_axes: function() {
|
||||||
let homed = false;
|
let homed = false;
|
||||||
|
|
||||||
|
|||||||
@@ -249,13 +249,22 @@ module.exports = {
|
|||||||
api.put(`home/${axis}/clear`);
|
api.put(`home/${axis}/clear`);
|
||||||
},
|
},
|
||||||
|
|
||||||
home_all: async function () {
|
aux_home: function () {
|
||||||
this.ask_home = false;
|
api.put("aux/home").catch(function (err) {
|
||||||
try {
|
console.error("W home failed:", err);
|
||||||
await api.put("home");
|
});
|
||||||
} catch (e) {
|
},
|
||||||
console.error("Home all failed:", e);
|
|
||||||
}
|
aux_jog: function (delta_mm) {
|
||||||
|
api.put("aux/jog", { mm: delta_mm }).catch(function (err) {
|
||||||
|
console.error("W jog failed:", err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
aux_jog_incr: function (sign) {
|
||||||
|
const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
|
||||||
|
const delta_mm = sign * (this.metric ? amount : amount * 25.4);
|
||||||
|
this.aux_jog(delta_mm);
|
||||||
},
|
},
|
||||||
|
|
||||||
show_set_position: function (axis) {
|
show_set_position: function (axis) {
|
||||||
|
|||||||
@@ -49,17 +49,14 @@ module.exports = {
|
|||||||
methods: {
|
methods: {
|
||||||
get_io_state_class: function(active, state) {
|
get_io_state_class: function(active, state) {
|
||||||
if (typeof active == "undefined" || typeof state == "undefined") {
|
if (typeof active == "undefined" || typeof state == "undefined") {
|
||||||
return "fa-triangle-exclamation warn";
|
return "fa-exclamation-triangle warn";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tristated: render as the regular (outline) circle to
|
|
||||||
// distinguish from active/inactive solid circles. Adding
|
|
||||||
// `far` switches to the FA6 regular family.
|
|
||||||
if (state == 2) {
|
if (state == 2) {
|
||||||
return "far fa-circle";
|
return "fa-circle-o";
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = state ? "fa-circle-plus" : "fa-circle-minus";
|
const icon = state ? "fa-plus-circle" : "fa-minus-circle";
|
||||||
return `${icon} ${active ? "active" : "inactive"}`;
|
return `${icon} ${active ? "active" : "inactive"}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -87,16 +87,100 @@ module.exports = {
|
|||||||
return this.stallRPM * this.stepsPerRev * ustep / 60;
|
return this.stallRPM * this.stepsPerRev * ustep / 60;
|
||||||
},
|
},
|
||||||
|
|
||||||
// NOTE: do not add `current_xxx` computed props that mirror
|
current_axis: function() {
|
||||||
// controller state vars (`<idx>vm`, `<idx>am`, …) and pair
|
return this.state[this.index + 'an'];
|
||||||
// them with watchers that copy state -> motor config. The
|
},
|
||||||
// controller streams those vars continuously over the WS;
|
|
||||||
// any watcher that writes them back into
|
current_max_velocity: function() {
|
||||||
// `config.motors[index]` will clobber whatever the user is
|
return this.state[this.index + 'vm'];
|
||||||
// typing into the form between websocket ticks. The form
|
},
|
||||||
// edits config directly; Save (app.js) PUTs it to the
|
|
||||||
// server. The server-side rotary toggle is handled by
|
current_max_soft_limit: function() {
|
||||||
// refetching config after the PUT, not by watching state.
|
return this.state[this.index + 'tm'];
|
||||||
|
},
|
||||||
|
|
||||||
|
current_min_soft_limit: function() {
|
||||||
|
return this.state[this.index + 'tn'];
|
||||||
|
},
|
||||||
|
current_max_accel: function() {
|
||||||
|
return this.state[this.index + 'am'];
|
||||||
|
},
|
||||||
|
current_max_jerk: function() {
|
||||||
|
return this.state[this.index + 'jm'];
|
||||||
|
},
|
||||||
|
current_step_angle: function() {
|
||||||
|
return this.state[this.index + 'sa'];
|
||||||
|
},
|
||||||
|
current_travel_per_rev: function() {
|
||||||
|
return this.state[this.index + 'tr'];
|
||||||
|
},
|
||||||
|
current_microsteps: function() {
|
||||||
|
return this.state[this.index + 'mi'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
attached: function() {
|
||||||
|
// Sync all state values with motor config when component is ready
|
||||||
|
// This ensures UI shows correct values when component is first loaded
|
||||||
|
console.log("Syncing state to motor config for motor index ",this.index);
|
||||||
|
this.syncStateToConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
current_axis(new_value) {
|
||||||
|
const motor_axes = ["X", "Y", "Z", "A", "B", "C"]
|
||||||
|
if(motor_axes[new_value] != this.motor['axis']){
|
||||||
|
this.motor['axis'] = motor_axes[new_value];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_max_velocity(new_value) {
|
||||||
|
if(new_value != this.motor['max-velocity']) {
|
||||||
|
this.motor['max-velocity'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_max_soft_limit(new_value) {
|
||||||
|
if(new_value != this.motor['max-soft-limit']) {
|
||||||
|
this.motor['max-soft-limit'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_min_soft_limit(new_value) {
|
||||||
|
if(new_value != this.motor['min-soft-limit']) {
|
||||||
|
this.motor['min-soft-limit'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_max_accel(new_value) {
|
||||||
|
if(new_value != this.motor['max-accel']) {
|
||||||
|
this.motor['max-accel'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_max_jerk(new_value) {
|
||||||
|
if(new_value != this.motor['max-jerk']) {
|
||||||
|
this.motor['max-jerk'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_step_angle(new_value) {
|
||||||
|
if(new_value != this.motor['step-angle']) {
|
||||||
|
this.motor['step-angle'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_travel_per_rev(new_value) {
|
||||||
|
if(new_value != this.motor['travel-per-rev']) {
|
||||||
|
this.motor['travel-per-rev'] = new_value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
current_microsteps(new_value) {
|
||||||
|
if(new_value != this.motor['microsteps']) {
|
||||||
|
this.motor['microsteps'] = new_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@@ -126,6 +210,45 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
|
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
syncStateToConfig: function() {
|
||||||
|
// Force sync all state values to motor config
|
||||||
|
// This ensures the UI reflects the current state even if changes happened while component was unmounted
|
||||||
|
|
||||||
|
if(this.state == undefined) {
|
||||||
|
console.log("State is undefined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state[this.index + 'an'] != this.motor['axis']) {
|
||||||
|
const motor_axes = ["X", "Y", "Z", "A", "B", "C"];
|
||||||
|
this.$set('motor["axis"]', motor_axes[this.state[this.index + 'an']]);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'vm'] != this.motor['max-velocity']) {
|
||||||
|
this.$set('motor["max-velocity"]', this.state[this.index + 'vm']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'tm'] != this.motor['max-soft-limit']) {
|
||||||
|
this.$set('motor["max-soft-limit"]', this.state[this.index + 'tm']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'tn'] != this.motor['min-soft-limit']) {
|
||||||
|
this.$set('motor["min-soft-limit"]', this.state[this.index + 'tn']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'am'] != this.motor['max-accel']) {
|
||||||
|
this.$set('motor["max-accel"]', this.state[this.index + 'am']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'jm'] != this.motor['max-jerk']) {
|
||||||
|
this.$set('motor["max-jerk"]', this.state[this.index + 'jm']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'sa'] != this.motor['step-angle']) {
|
||||||
|
this.$set('motor["step-angle"]', this.state[this.index + 'sa']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'tr'] != this.motor['travel-per-rev']) {
|
||||||
|
this.$set('motor["travel-per-rev"]', this.state[this.index + 'tr']);
|
||||||
|
}
|
||||||
|
if (this.state[this.index + 'mi'] != this.motor['microsteps']) {
|
||||||
|
this.$set('motor["microsteps"]', this.state[this.index + 'mi']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,13 +101,6 @@ module.exports = {
|
|||||||
Vue.nextTick(this.update);
|
Vue.nextTick(this.update);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy: function() {
|
|
||||||
if (this._sizeWatcher) {
|
|
||||||
this._sizeWatcher.disconnect();
|
|
||||||
this._sizeWatcher = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update: async function() {
|
update: async function() {
|
||||||
if (!this.webglAvailable) {
|
if (!this.webglAvailable) {
|
||||||
@@ -208,12 +201,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dims = this.get_dims();
|
const dims = this.get_dims();
|
||||||
// Skip layouts where the target has no measurable size.
|
|
||||||
// The render loop guard below will not draw frames until
|
|
||||||
// a real size has been observed at least once.
|
|
||||||
if (!(dims.width > 0 && dims.height > 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.camera.aspect = dims.width / dims.height;
|
this.camera.aspect = dims.width / dims.height;
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
@@ -287,23 +274,12 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Renderer. Use an opaque canvas with a clear color
|
// Renderer
|
||||||
// that matches the page-side gradient so the moment
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
// the canvas is appended (and before the first 3D
|
|
||||||
// frame is drawn) the user does not see a flash from
|
|
||||||
// the page background through transparency.
|
|
||||||
this.renderer = new THREE.WebGLRenderer({
|
|
||||||
antialias: true,
|
|
||||||
alpha: false,
|
|
||||||
});
|
|
||||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
this.renderer.setClearColor(0x222222, 1);
|
this.renderer.setClearColor(0, 0);
|
||||||
// Same color on the DOM element itself so the very
|
|
||||||
// first paint (before the WebGL context has cleared)
|
|
||||||
// is dark too.
|
|
||||||
this.renderer.domElement.style.background = "#222222";
|
|
||||||
this.renderer.domElement.style.display = "block";
|
|
||||||
this.target.appendChild(this.renderer.domElement);
|
this.target.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("WebGL not supported: ", e);
|
console.log("WebGL not supported: ", e);
|
||||||
return;
|
return;
|
||||||
@@ -357,46 +333,8 @@ module.exports = {
|
|||||||
// Events
|
// Events
|
||||||
window.addEventListener("resize", this.update_view, false);
|
window.addEventListener("resize", this.update_view, false);
|
||||||
|
|
||||||
// Start the render loop only after the target has a real,
|
// Start it
|
||||||
// stable size. Without this, the first frame paints into
|
|
||||||
// a 0×0 / collapsed-flex canvas and a second frame paints
|
|
||||||
// again at the right size — visible as a flash on the
|
|
||||||
// very first mount of the Program tab.
|
|
||||||
const startRendering = () => {
|
|
||||||
if (this._rendering) return;
|
|
||||||
this._rendering = true;
|
|
||||||
this.update_view();
|
|
||||||
this.render();
|
this.render();
|
||||||
};
|
|
||||||
|
|
||||||
const dims = this.get_dims();
|
|
||||||
if (dims.width > 0 && dims.height > 0) {
|
|
||||||
startRendering();
|
|
||||||
} else if (typeof ResizeObserver !== "undefined") {
|
|
||||||
this._sizeWatcher = new ResizeObserver(entries => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
const r = entry.contentRect;
|
|
||||||
if (r.width > 0 && r.height > 0) {
|
|
||||||
this._sizeWatcher.disconnect();
|
|
||||||
this._sizeWatcher = null;
|
|
||||||
startRendering();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._sizeWatcher.observe(this.target);
|
|
||||||
} else {
|
|
||||||
// Old browser fallback: poll for a non-zero size.
|
|
||||||
const tick = () => {
|
|
||||||
const d = this.get_dims();
|
|
||||||
if (d.width > 0 && d.height > 0) {
|
|
||||||
startRendering();
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
create_surface_material: function() {
|
create_surface_material: function() {
|
||||||
@@ -708,14 +646,6 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't paint frames while the target has no size; this
|
|
||||||
// prevents an initial single-frame clear from painting
|
|
||||||
// before the layout has settled (visible as a dark flash).
|
|
||||||
const dims = this.get_dims();
|
|
||||||
if (!(dims.width > 0 && dims.height > 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.controls.update() || this.dirty) {
|
if (this.controls.update() || this.dirty) {
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
|||||||
@@ -77,32 +77,6 @@ module.exports = {
|
|||||||
return this.state.cycle == "idle";
|
return this.state.cycle == "idle";
|
||||||
},
|
},
|
||||||
|
|
||||||
// True only while a loaded G-code program is actually being
|
|
||||||
// executed (running, paused/holding, or stopping). Excludes
|
|
||||||
// jogging, homing, probing, MDI commands and other one-off
|
|
||||||
// motion that also leave state.xx == "RUNNING" but must not
|
|
||||||
// swap the jog grid out for the "Now Running" panel.
|
|
||||||
//
|
|
||||||
// Distinguishing signal is state.cycle:
|
|
||||||
// - "idle" : nothing happening
|
|
||||||
// - "jogging" : user-initiated jog
|
|
||||||
// - "homing" : home cycle
|
|
||||||
// - "probing" : probe cycle
|
|
||||||
// - "mdi" : single MDI command
|
|
||||||
// - "running" : an actual loaded program is being run
|
|
||||||
// Only "running" (combined with a selected file) is what we want.
|
|
||||||
is_program_executing: function () {
|
|
||||||
if (!this.state) return false;
|
|
||||||
const xx = this.state.xx;
|
|
||||||
const cycle = this.state.cycle;
|
|
||||||
const isExecState = xx == "RUNNING" || xx == "HOLDING" || xx == "STOPPING";
|
|
||||||
if (!isExecState) return false;
|
|
||||||
// The cycle string narrows it to a real program run; anything
|
|
||||||
// else (jogging / homing / probing / mdi) is a one-off.
|
|
||||||
if (cycle && cycle != "running" && cycle != "idle") return false;
|
|
||||||
return !!this.state.selected;
|
|
||||||
},
|
|
||||||
|
|
||||||
is_paused: function () {
|
is_paused: function () {
|
||||||
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
|
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
|
||||||
},
|
},
|
||||||
@@ -564,43 +538,23 @@ module.exports = {
|
|||||||
override_feed: function () { api.put(`override/feed/${this.feed_override}`); },
|
override_feed: function () { api.put(`override/feed/${this.feed_override}`); },
|
||||||
override_speed: function () { api.put(`override/speed/${this.speed_override}`); },
|
override_speed: function () { api.put(`override/speed/${this.speed_override}`); },
|
||||||
|
|
||||||
run_macro: async function (id) {
|
run_macro: function (id) {
|
||||||
if (this.state.macros[id].file_name == "default") {
|
if (this.state.macros[id].file_name == "default") {
|
||||||
this.showNoGcodeMessage = true;
|
this.showNoGcodeMessage = true;
|
||||||
return;
|
} else {
|
||||||
|
if (this.state.macros[id].file_name != this.state.selected) {
|
||||||
|
this.state.selected = this.state.macros[id].file_name;
|
||||||
}
|
}
|
||||||
const file_name = this.state.macros[id].file_name;
|
|
||||||
try {
|
try {
|
||||||
// Selecting a file on the server is a side effect of
|
|
||||||
// GET /api/file/<name>. The macro button used to mutate
|
|
||||||
// state.selected client-side and immediately call start, which
|
|
||||||
// raced the file fetch: if the server hadn't seen the new
|
|
||||||
// selection yet, mach.start() ran whichever file was selected
|
|
||||||
// last. Do it explicitly and await so start always sees the
|
|
||||||
// right file.
|
|
||||||
if (file_name != this.state.selected) {
|
|
||||||
this.state.selected = file_name;
|
|
||||||
// GET /api/file/<name> returns gcode text (not JSON), so use
|
|
||||||
// fetch directly. The server's FileHandler.get sets
|
|
||||||
// state.selected as a side effect; we await the response
|
|
||||||
// before starting so mach.start() reads the right file.
|
|
||||||
const resp = await fetch(
|
|
||||||
`/api/file/${encodeURIComponent(file_name)}`,
|
|
||||||
{ cache: "no-cache" }
|
|
||||||
);
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`file fetch failed: ${resp.status}`);
|
|
||||||
}
|
|
||||||
await resp.text();
|
|
||||||
}
|
|
||||||
this.load();
|
this.load();
|
||||||
if (this.state.macros[id].alert == true) {
|
if (this.state.macros[id].alert == true) {
|
||||||
this.macrosLoading = true;
|
this.macrosLoading = true;
|
||||||
} else {
|
} else {
|
||||||
await this.start_pause();
|
setImmediate(() => this.start_pause());
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Error running macro: ", error);
|
console.warn("Error running program: ", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
is_kiosk: function () { return !!this.$root.is_kiosk; },
|
|
||||||
|
|
||||||
display_units: {
|
display_units: {
|
||||||
cache: false,
|
cache: false,
|
||||||
get: function () { return this.$root.display_units; },
|
get: function () { return this.$root.display_units; },
|
||||||
|
|||||||
@@ -36,17 +36,8 @@ module.exports = {
|
|||||||
return {
|
return {
|
||||||
sub: this.$root.sub_tab || "settings",
|
sub: this.$root.sub_tab || "settings",
|
||||||
ridx: this.$root.index, // local copy of the motor index
|
ridx: this.$root.index, // local copy of the motor index
|
||||||
// Whether the controller config has streamed in. The Svelte
|
|
||||||
// settings views crash on first paint with the placeholder
|
|
||||||
// config (settings.units / settings.easy-adapter / motion.*
|
|
||||||
// are all undefined). Gate the inner mount on this flag.
|
|
||||||
config_ready: false,
|
|
||||||
rail_items: [
|
rail_items: [
|
||||||
{ sub: "settings", href: "#settings", icon: "fa-display", label: "Display & Units" },
|
{ sub: "settings", href: "#settings", icon: "fa-display", label: "Display & Units" },
|
||||||
{ sub: "probing", href: "#probing", icon: "fa-bullseye", label: "Probing" },
|
|
||||||
{ sub: "gcode", href: "#gcode", icon: "fa-code", label: "G-code & Motion" },
|
|
||||||
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
|
|
||||||
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
|
|
||||||
{ sub: "admin-network", href: "#admin-network", icon: "fa-network-wired", label: "Network" },
|
{ sub: "admin-network", href: "#admin-network", icon: "fa-network-wired", label: "Network" },
|
||||||
{ sub: "admin-general", href: "#admin-general", icon: "fa-shield-halved", label: "General / Firmware" },
|
{ sub: "admin-general", href: "#admin-general", icon: "fa-shield-halved", label: "General / Firmware" },
|
||||||
{ sub: "tool", href: "#tool", icon: "fa-bolt", label: "Spindle & Tool" },
|
{ sub: "tool", href: "#tool", icon: "fa-bolt", label: "Spindle & Tool" },
|
||||||
@@ -56,7 +47,13 @@ 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: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
|
||||||
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
|
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -66,12 +63,6 @@ module.exports = {
|
|||||||
this._onHash = () => this.refresh_from_hash();
|
this._onHash = () => this.refresh_from_hash();
|
||||||
window.addEventListener("hashchange", this._onHash);
|
window.addEventListener("hashchange", this._onHash);
|
||||||
this.refresh_from_hash();
|
this.refresh_from_hash();
|
||||||
this._configPoll = setInterval(() => {
|
|
||||||
const c = this.$root && this.$root.config;
|
|
||||||
const ready = !!(c && c.full_version && c.full_version !== "<loading>"
|
|
||||||
&& c.settings && typeof c.settings === "object");
|
|
||||||
if (ready !== this.config_ready) this.config_ready = ready;
|
|
||||||
}, 200);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
attached: function () {
|
attached: function () {
|
||||||
@@ -96,7 +87,6 @@ module.exports = {
|
|||||||
if (this._onHash) {
|
if (this._onHash) {
|
||||||
window.removeEventListener("hashchange", this._onHash);
|
window.removeEventListener("hashchange", this._onHash);
|
||||||
}
|
}
|
||||||
if (this._configPoll) clearInterval(this._configPoll);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -109,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;
|
||||||
@@ -117,48 +113,19 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
on_rail_click: function (item, ev) {
|
on_rail_click: function (item, ev) {
|
||||||
if (!item) return;
|
// Soft-link rail items use an anchor and a scrollIntoView call.
|
||||||
// Always preventDefault on rail clicks. Letting the browser
|
if (item && item.anchor) {
|
||||||
// anchor-scroll to <div id="settings"> etc. inside .app-body
|
ev.preventDefault();
|
||||||
// can pull the .app-head out of view; we drive navigation
|
// Navigate to settings if not already there, then scroll.
|
||||||
// ourselves through location.hash and our hashchange handler.
|
|
||||||
if (ev && ev.preventDefault) ev.preventDefault();
|
|
||||||
|
|
||||||
if (item.anchor) {
|
|
||||||
// Soft-link rail items use a #settings hash plus an in-page
|
|
||||||
// anchor scroll once the Svelte page has mounted. We scroll
|
|
||||||
// ONLY the .settings-content overflow container by setting
|
|
||||||
// its scrollTop directly — element.scrollIntoView() walks all
|
|
||||||
// ancestor scroll containers and can tug the .app-body / html
|
|
||||||
// layout, which under tablet mode pulls the fixed header out
|
|
||||||
// of view.
|
|
||||||
if (location.hash !== item.href) location.hash = item.href;
|
if (location.hash !== item.href) location.hash = item.href;
|
||||||
const reset = () => {
|
this._w_axis_focus = (item.sub === "w-axis");
|
||||||
// Force any inadvertent ancestor scroll back to 0 before
|
// Defer the scroll so Vue mounts the inner Svelte page first.
|
||||||
// we move .settings-content explicitly.
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
const body = document.querySelector(".app-body");
|
|
||||||
if (body) body.scrollTop = 0;
|
|
||||||
document.documentElement.scrollTop = 0;
|
|
||||||
};
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reset();
|
|
||||||
const el = document.getElementById(item.anchor);
|
const el = document.getElementById(item.anchor);
|
||||||
const scroller = document.querySelector(".settings-content");
|
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
if (el && scroller) {
|
}, 250);
|
||||||
const elTop = el.getBoundingClientRect().top;
|
|
||||||
const scTop = scroller.getBoundingClientRect().top;
|
|
||||||
scroller.scrollTop = scroller.scrollTop + (elTop - scTop) - 12;
|
|
||||||
}
|
|
||||||
// Re-assert ancestor scroll = 0 in case the assignment above
|
|
||||||
// moved things.
|
|
||||||
requestAnimationFrame(reset);
|
|
||||||
}, 320);
|
|
||||||
} else {
|
} else {
|
||||||
if (location.hash !== item.href) location.hash = item.href;
|
this._w_axis_focus = false;
|
||||||
// Reset .app-body scroll so each route starts at the top.
|
|
||||||
const body = document.querySelector(".app-body");
|
|
||||||
if (body) body.scrollTop = 0;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,14 @@
|
|||||||
// V09 wraps the legacy Svelte SettingsView and filters its big page
|
|
||||||
// down to a single rail section so each rail item shows only the
|
|
||||||
// relevant controls. The Svelte component is left untouched (it is
|
|
||||||
// shared with the legacy UI) — we just hide the `<h2>` and `<fieldset>`
|
|
||||||
// elements whose `data-sec` does not match the active section.
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
template: "#settings-view-template",
|
template: "#settings-view-template",
|
||||||
|
|
||||||
props: {
|
|
||||||
// "display" | "probing" | "gcode". Default is "display" which
|
|
||||||
// keeps the rail's "Display & Units" item working unchanged.
|
|
||||||
section: { default: "display" },
|
|
||||||
},
|
|
||||||
|
|
||||||
attached: function() {
|
attached: function() {
|
||||||
this.svelteComponent = SvelteComponents.createComponent(
|
this.svelteComponent = SvelteComponents.createComponent(
|
||||||
"SettingsView",
|
"SettingsView",
|
||||||
document.getElementById("settings")
|
document.getElementById("settings")
|
||||||
);
|
);
|
||||||
// Defer one tick so Svelte has rendered the section markup.
|
|
||||||
setTimeout(() => this.apply_section_filter(), 0);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
detached: function() {
|
detached: function() {
|
||||||
if (this.svelteComponent) this.svelteComponent.$destroy();
|
this.svelteComponent.$destroy();
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
section: function () {
|
|
||||||
this.apply_section_filter();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
apply_section_filter: function () {
|
|
||||||
const root = document.getElementById("settings");
|
|
||||||
if (!root) return;
|
|
||||||
const want = this.section || "display";
|
|
||||||
// Hide every section block that does not match.
|
|
||||||
root.querySelectorAll("[data-sec]").forEach(el => {
|
|
||||||
el.style.display = el.dataset.sec === want ? "" : "none";
|
|
||||||
});
|
|
||||||
// Hide the global <h1>Settings</h1> on subsections so the
|
|
||||||
// page reads as a focused panel.
|
|
||||||
const h1 = root.querySelector(".settings-view > h1");
|
|
||||||
if (h1) {
|
|
||||||
if (want === "display") {
|
|
||||||
h1.textContent = "Display & Units";
|
|
||||||
} else if (want === "probing") {
|
|
||||||
h1.textContent = "Probing";
|
|
||||||
} else if (want === "gcode") {
|
|
||||||
h1.textContent = "G-code & Motion";
|
|
||||||
} else {
|
|
||||||
h1.textContent = "Settings";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ html(lang="en")
|
|||||||
|
|
||||||
style: include ../static/css/pure-min.css
|
style: include ../static/css/pure-min.css
|
||||||
|
|
||||||
style: include ../static/css/fa6.min.css
|
style: include ../static/css/font-awesome.min.css
|
||||||
style: include ../static/css/Audiowide.css
|
style: include ../static/css/Audiowide.css
|
||||||
style: include ../static/css/clusterize.css
|
style: include ../static/css/clusterize.css
|
||||||
style: include ../svelte-components/node_modules/svelte-material-ui/bare.css
|
style: include ../svelte-components/node_modules/svelte-material-ui/bare.css
|
||||||
@@ -18,51 +18,6 @@ html(lang="en")
|
|||||||
style: include:stylus ../stylus/style.styl
|
style: include:stylus ../stylus/style.styl
|
||||||
|
|
||||||
body(v-cloak)
|
body(v-cloak)
|
||||||
// Tablet (kiosk) mode — pins the .app-shell to 1920x1080 and
|
|
||||||
// scales it to fit the actual viewport so the UI always looks
|
|
||||||
// exactly like the 10.8" 1920x1080 portable monitor.
|
|
||||||
//
|
|
||||||
// Toggle: ?tablet=1 to enable
|
|
||||||
// ?tablet=0 to disable
|
|
||||||
// Sticky in localStorage; once set, no querystring is needed.
|
|
||||||
script.
|
|
||||||
(function () {
|
|
||||||
try {
|
|
||||||
var p = new URLSearchParams(location.search);
|
|
||||||
if (p.has("tablet")) {
|
|
||||||
var on = p.get("tablet") !== "0" && p.get("tablet") !== "false";
|
|
||||||
localStorage.setItem("ui-tablet-mode", on ? "1" : "0");
|
|
||||||
}
|
|
||||||
if (localStorage.getItem("ui-tablet-mode") === "1") {
|
|
||||||
document.documentElement.classList.add("tablet-mode");
|
|
||||||
}
|
|
||||||
function fit() {
|
|
||||||
if (!document.documentElement.classList.contains("tablet-mode")) return;
|
|
||||||
var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
|
||||||
document.documentElement.style.setProperty("--tablet-scale", s);
|
|
||||||
}
|
|
||||||
fit();
|
|
||||||
window.addEventListener("resize", fit);
|
|
||||||
|
|
||||||
// Kiosk mode: when the UI is loaded by the controller's
|
|
||||||
// own onboard browser (Chromium pointing at localhost on
|
|
||||||
// the Pi 3B at 1366x768), apply a tighter layout that
|
|
||||||
// packs the V09 UI into the smaller, slower display.
|
|
||||||
// Override with ?kiosk=0 to force the desktop layout.
|
|
||||||
if (p.has("kiosk")) {
|
|
||||||
var k = p.get("kiosk") !== "0" && p.get("kiosk") !== "false";
|
|
||||||
localStorage.setItem("ui-kiosk-mode", k ? "1" : "0");
|
|
||||||
}
|
|
||||||
var stored = localStorage.getItem("ui-kiosk-mode");
|
|
||||||
var auto = location.hostname === "localhost"
|
|
||||||
|| location.hostname === "127.0.0.1"
|
|
||||||
|| location.hostname === "::1";
|
|
||||||
if (stored === "1" || (stored !== "0" && auto)) {
|
|
||||||
document.documentElement.classList.add("kiosk-mode");
|
|
||||||
}
|
|
||||||
} catch (_e) {}
|
|
||||||
})();
|
|
||||||
|
|
||||||
#svelte-dialog-host
|
#svelte-dialog-host
|
||||||
|
|
||||||
#overlay(v-if="status != 'connected'")
|
#overlay(v-if="status != 'connected'")
|
||||||
@@ -102,7 +57,7 @@ html(lang="en")
|
|||||||
|
|
||||||
.pi-temp-warning(v-if="80 <= state.rpi_temp",
|
.pi-temp-warning(v-if="80 <= state.rpi_temp",
|
||||||
title="Raspberry Pi temperature too high.")
|
title="Raspberry Pi temperature too high.")
|
||||||
.fa.fa-temperature-full
|
.fa.fa-thermometer-full
|
||||||
|
|
||||||
span.state-badge(:class="state_class", :title="mach_state_full")
|
span.state-badge(:class="state_class", :title="mach_state_full")
|
||||||
span.dot
|
span.dot
|
||||||
@@ -120,7 +75,7 @@ html(lang="en")
|
|||||||
.sp-val v{{config.full_version}}
|
.sp-val v{{config.full_version}}
|
||||||
a.sp-act(v-if="show_upgrade()", href="#admin-general")
|
a.sp-act(v-if="show_upgrade()", href="#admin-general")
|
||||||
| Upgrade to v{{latestVersion}}
|
| Upgrade to v{{latestVersion}}
|
||||||
.fa.fa-circle-exclamation.upgrade-attention
|
.fa.fa-exclamation-circle.upgrade-attention
|
||||||
.sp-row
|
.sp-row
|
||||||
.sp-icon: .fa.fa-network-wired
|
.sp-icon: .fa.fa-network-wired
|
||||||
.sp-text
|
.sp-text
|
||||||
@@ -167,22 +122,13 @@ html(lang="en")
|
|||||||
.fa.fa-save
|
.fa.fa-save
|
||||||
| Save{{modified ? '*' : ''}}
|
| Save{{modified ? '*' : ''}}
|
||||||
|
|
||||||
// Routed view. We keep instances alive across tab swaps so:
|
// Routed view (no keep-alive: Vue 1 has issues re-evaluating
|
||||||
// - The Program tab's WebGL <path-viewer> canvas does not
|
// dynamic :class / v-if bindings on cached components when the
|
||||||
// get destroyed and recreated each time (which caused a
|
// route changes within the same kept-alive tree)
|
||||||
// dark flash as the GL context cleared the new canvas
|
|
||||||
// before its first frame).
|
|
||||||
// - The Program tab's clusterize.js gcode list does not
|
|
||||||
// re-virtualize from scratch on every visit.
|
|
||||||
// - The Settings shell's child Svelte components stay
|
|
||||||
// mounted, preserving any in-flight form state.
|
|
||||||
// The settings-shell handles its own inner v-if cascade so
|
|
||||||
// the Vue 1 reactivity quirk that motivated removing
|
|
||||||
// keep-alive earlier no longer applies here.
|
|
||||||
.app-body
|
.app-body
|
||||||
component(:is="currentView + '-view'", :index="index",
|
component(:is="currentView + '-view'", :index="index",
|
||||||
:config="config", :template="template", :state="state",
|
:config="config", :template="template", :state="state",
|
||||||
:sub-tab="sub_tab", keep-alive)
|
:sub-tab="sub_tab")
|
||||||
|
|
||||||
message.error-message(:show.sync="errorShow")
|
message.error-message(:show.sync="errorShow")
|
||||||
div(slot="header")
|
div(slot="header")
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ script#console-view-template(type="text/x-template")
|
|||||||
// ----- Messages -----
|
// ----- Messages -----
|
||||||
.messages-pane(v-show="sub === 'messages'")
|
.messages-pane(v-show="sub === 'messages'")
|
||||||
.msg-empty(v-if="!$root.messages_log.length")
|
.msg-empty(v-if="!$root.messages_log.length")
|
||||||
.fa.fa-circle-check
|
.fa.fa-check-circle
|
||||||
| No messages.
|
| No messages.
|
||||||
.msg(v-for="m in $root.messages_log",
|
.msg(v-for="m in $root.messages_log",
|
||||||
:class="m.level === 'warning' ? 'warn' : 'info'", track-by="$index")
|
:class="m.level === 'warning' ? 'warn' : 'info'", track-by="$index")
|
||||||
|
|||||||
@@ -35,9 +35,8 @@ script#control-view-template(type="text/x-template")
|
|||||||
button.pure-button.button-error(@click="GCodeNotFound=false") OK
|
button.pure-button.button-error(@click="GCodeNotFound=false") OK
|
||||||
|
|
||||||
message(:show.sync="show_probe_dialog")
|
message(:show.sync="show_probe_dialog")
|
||||||
h3(slot="header") Choose probe type
|
h3(slot="header") Probe Rotary
|
||||||
div(slot="body")
|
div(slot="body")
|
||||||
p Pick which probe routine to run.
|
|
||||||
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('xyz')") Probe XYZ
|
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('xyz')") Probe XYZ
|
||||||
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('z')") Probe Z
|
button.pure-button(:class="state['pw'] ? '' : 'load-on'", @click="showProbeDialog('z')") Probe Z
|
||||||
div(slot="footer")
|
div(slot="footer")
|
||||||
@@ -47,9 +46,7 @@ script#control-view-template(type="text/x-template")
|
|||||||
.control-grid
|
.control-grid
|
||||||
|
|
||||||
// ===== JOG =====
|
// ===== JOG =====
|
||||||
// Hidden only while a G-code program is running / paused /
|
.jog-card
|
||||||
// stopping. Jogging / homing / MDI moves do not hide it.
|
|
||||||
.jog-card(v-if="!is_program_executing")
|
|
||||||
.jog-head
|
.jog-head
|
||||||
.jog-title
|
.jog-title
|
||||||
| Jog
|
| Jog
|
||||||
@@ -76,11 +73,11 @@ script#control-view-template(type="text/x-template")
|
|||||||
|
|
||||||
// Row 2
|
// Row 2
|
||||||
button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X−
|
button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X−
|
||||||
button.jbtn(@click="showMoveToZeroDialog('xy')")
|
button.jbtn.ghost(@click="showMoveToZeroDialog('xy')")
|
||||||
span.lbl XY
|
span.lbl XY
|
||||||
span Origin
|
span Origin
|
||||||
button.jbtn(@click="jog_fn(1, 0, 0, 0)") X+
|
button.jbtn(@click="jog_fn(1, 0, 0, 0)") X+
|
||||||
button.jbtn(@click="showMoveToZeroDialog('z')")
|
button.jbtn.ghost(@click="showMoveToZeroDialog('z')")
|
||||||
span.lbl Z
|
span.lbl Z
|
||||||
span Origin
|
span Origin
|
||||||
|
|
||||||
@@ -92,8 +89,25 @@ script#control-view-template(type="text/x-template")
|
|||||||
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
|
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
|
||||||
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z−
|
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z−
|
||||||
|
|
||||||
// Row 4 — A axis (rotary) when rotary is enabled.
|
// Row 4 — W axis (auxcnc) when enabled
|
||||||
template(v-if="state['2an'] == 3")
|
template(v-if="w.enabled")
|
||||||
|
button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
|
||||||
|
.fa.fa-arrow-down.ico
|
||||||
|
span.lbl W−
|
||||||
|
button.jbtn.ghost(@click="aux_home()", :disabled="!w.enabled")
|
||||||
|
span.lbl Home
|
||||||
|
span W
|
||||||
|
button.jbtn(@click="aux_jog_incr(+1)", :disabled="!w.enabled")
|
||||||
|
.fa.fa-arrow-up.ico
|
||||||
|
span.lbl W+
|
||||||
|
button.jbtn(@click="show_probe_dialog=true",
|
||||||
|
:class="{'load-on': !state['pw']}")
|
||||||
|
.fa.fa-bullseye.ico
|
||||||
|
span.lbl Probe
|
||||||
|
|
||||||
|
// Row 4 — A axis (rotary) when no W and rotary is enabled
|
||||||
|
// (Vue 1 has no v-else-if; we negate w.enabled explicitly.)
|
||||||
|
template(v-if="!w.enabled && state['2an'] == 3")
|
||||||
button.jbtn.dir(@click="jog_fn(0, 0, 0, -1)")
|
button.jbtn.dir(@click="jog_fn(0, 0, 0, -1)")
|
||||||
.fa.fa-rotate-left.ico
|
.fa.fa-rotate-left.ico
|
||||||
span.lbl A−
|
span.lbl A−
|
||||||
@@ -109,78 +123,22 @@ script#control-view-template(type="text/x-template")
|
|||||||
span.lbl Probe
|
span.lbl Probe
|
||||||
|
|
||||||
// Row 4 — fallback probe / zero / home shortcuts
|
// Row 4 — fallback probe / zero / home shortcuts
|
||||||
template(v-if="state['2an'] != 3")
|
template(v-if="!w.enabled && state['2an'] != 3")
|
||||||
button.jbtn(@click="showProbeDialog('xyz')",
|
button.jbtn(@click="showProbeDialog('xyz')",
|
||||||
:class="{'load-on': !state['pw']}")
|
:class="{'load-on': !state['pw']}")
|
||||||
.fa.fa-bullseye.ico
|
.fa.fa-bullseye.ico
|
||||||
span.lbl Probe XYZ
|
span.lbl Probe XYZ
|
||||||
button.jbtn.ghost(@click="zero()", :disabled="!can_set_axis")
|
button.jbtn.ghost(@click="zero()", :disabled="!can_set_axis")
|
||||||
.fa.fa-location-dot.ico
|
.fa.fa-map-marker.ico
|
||||||
span.lbl Zero all
|
span.lbl Zero all
|
||||||
button.jbtn(@click="showProbeDialog('z')",
|
button.jbtn(@click="showProbeDialog('z')",
|
||||||
:class="{'load-on': !state['pw']}")
|
:class="{'load-on': !state['pw']}")
|
||||||
.fa.fa-bullseye.ico
|
.fa.fa-bullseye.ico
|
||||||
span.lbl Probe Z
|
span.lbl Probe Z
|
||||||
button.jbtn.ghost(@click="home()")
|
button.jbtn.ghost(@click="home()", :disabled="!is_idle")
|
||||||
.fa.fa-home.ico
|
.fa.fa-home.ico
|
||||||
span.lbl Home all
|
span.lbl Home all
|
||||||
|
|
||||||
// ===== NOW RUNNING (replaces jog grid only while a G-code
|
|
||||||
// program is actually executing). Jogging is excluded.
|
|
||||||
.running-panel(v-if="is_program_executing")
|
|
||||||
.running-top
|
|
||||||
div
|
|
||||||
.running-file
|
|
||||||
.fa.fa-file-code
|
|
||||||
span(v-if="state.selected") {{state.selected}}
|
|
||||||
span(v-else) {{(mach_state || 'BUSY').toLowerCase()}}
|
|
||||||
.running-meta
|
|
||||||
span(v-if="is_running") {{ (mach_state || 'RUNNING').toLowerCase() }}
|
|
||||||
span(v-if="is_holding") paused
|
|
||||||
span(v-if="is_holding && pause_reason") · {{pause_reason}}
|
|
||||||
span(v-if="is_stopping") stopping
|
|
||||||
span(v-if="toolpath.lines") · line {{state.line || 0 | number}} / {{toolpath.lines | number}}
|
|
||||||
span(v-if="plan_time_remaining") · ETA {{plan_time_remaining | time}}
|
|
||||||
.running-pct
|
|
||||||
| {{((progress || 0) * 100) | fixed 0}}
|
|
||||||
span %
|
|
||||||
.running-progress
|
|
||||||
div(:style="'width:' + ((progress || 0) * 100) + '%'")
|
|
||||||
.running-stats
|
|
||||||
.running-stat
|
|
||||||
.lbl Velocity
|
|
||||||
.val
|
|
||||||
unit-value(:value="state.v", precision="2", unit="", iunit="", scale="0.0254")
|
|
||||||
| {{metric ? 'm/min' : 'IPM'}}
|
|
||||||
.running-stat
|
|
||||||
.lbl Feed
|
|
||||||
.val
|
|
||||||
unit-value(:value="state.feed", precision="0", unit="", iunit="")
|
|
||||||
| {{metric ? 'mm/min' : 'IPM'}}
|
|
||||||
.running-stat
|
|
||||||
.lbl Spindle
|
|
||||||
.val
|
|
||||||
| {{(state.speed || 0) | fixed 0}}
|
|
||||||
span(v-if="state.s != null && !isNaN(state.s)") ({{state.s | fixed 0}})
|
|
||||||
| RPM
|
|
||||||
.running-stat
|
|
||||||
.lbl Tool
|
|
||||||
.val T{{state.tool || 0}}
|
|
||||||
.running-row
|
|
||||||
// While RUNNING the primary action is Pause; while HOLDING / STOPPING it's Resume.
|
|
||||||
button.tx-btn.pause(v-if="is_running", @click="pause()")
|
|
||||||
.fa.fa-pause
|
|
||||||
span.lbl PAUSE
|
|
||||||
button.tx-btn.run(v-if="is_holding || is_stopping", @click="unpause()")
|
|
||||||
.fa.fa-play
|
|
||||||
span.lbl RESUME
|
|
||||||
button.tx-btn.stop(@click="stop()")
|
|
||||||
.fa.fa-stop
|
|
||||||
span.lbl STOP
|
|
||||||
button.tx-btn.step(v-if="is_holding", @click="step()")
|
|
||||||
.fa.fa-forward-step
|
|
||||||
span.lbl STEP
|
|
||||||
|
|
||||||
// ===== DRO + status strip =====
|
// ===== DRO + status strip =====
|
||||||
.right-col
|
.right-col
|
||||||
|
|
||||||
@@ -190,39 +148,66 @@ script#control-view-template(type="text/x-template")
|
|||||||
div Position
|
div Position
|
||||||
div Absolute
|
div Absolute
|
||||||
div Offset
|
div Offset
|
||||||
.actions-cell
|
div State
|
||||||
// Master Home All. Each row's Actions cell has a per-axis
|
div Toolpath
|
||||||
// home button; this header-level button homes every
|
div(style="text-align:right") Actions
|
||||||
// enabled axis (legacy Onefinity behavior).
|
|
||||||
button.icon-btn(:disabled="!is_idle",
|
|
||||||
title="Home all axes.", @click="home_all()")
|
|
||||||
.fa.fa-house-chimney
|
|
||||||
|
|
||||||
// Per-axis rows — keep unit-value + bindings from axis-vars
|
// Per-axis rows — keep unit-value + bindings from axis-vars
|
||||||
each axis in 'xyzabc'
|
each axis in 'xyzabc'
|
||||||
.dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
|
.dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
|
||||||
v-if=`${axis}.enabled`,
|
v-if=`${axis}.enabled`,
|
||||||
:title=`${axis}.toolmsg ? (${axis}.title + ' — ' + ${axis}.toolmsg) : ${axis}.title`)
|
:title=`${axis}.title`)
|
||||||
.dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
|
.dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
|
||||||
.dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
|
.dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
|
||||||
.dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
|
||||||
.dro-sec: unit-value(:value=`${axis}.off`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.off`, precision=3)
|
||||||
|
.dro-state
|
||||||
|
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.homed ? 'chip-green' : 'chip-amber')`)
|
||||||
|
.fa(:class=`'fa-' + ${axis}.icon`)
|
||||||
|
| {{#{axis}.state}}
|
||||||
|
.dro-toolpath
|
||||||
|
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.tklass.indexOf('warn') !== -1 ? 'chip-amber' : 'chip-green')`,
|
||||||
|
@click=`showToolpathMessageDialog('${axis}')`)
|
||||||
|
.fa(:class=`'fa-' + ${axis}.ticon`)
|
||||||
|
| {{#{axis}.tstate}}
|
||||||
.actions-cell
|
.actions-cell
|
||||||
button.icon-btn(:disabled="!can_set_axis",
|
button.icon-btn(:disabled="!can_set_axis",
|
||||||
:title=`'Set ${axis.toUpperCase()} axis position.'`,
|
:title=`'Set ${axis.toUpperCase()} axis position.'`,
|
||||||
@click=`show_set_position('${axis}')`)
|
@click=`show_set_position('${axis}')`)
|
||||||
.fa.fa-gear
|
.fa.fa-cog
|
||||||
button.icon-btn(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'state-red' : (${axis}.tklass.indexOf('warn') !== -1 ? 'state-amber' : 'state-green')`,
|
button.icon-btn(:disabled="!can_set_axis",
|
||||||
:disabled="!can_set_axis",
|
:title=`'Zero ${axis.toUpperCase()} axis offset.'`,
|
||||||
:title=`${axis}.toolmsg || ('Zero ${axis.toUpperCase()} axis offset.')`,
|
|
||||||
@click=`zero('${axis}')`)
|
@click=`zero('${axis}')`)
|
||||||
.fa.fa-location-dot
|
.fa.fa-map-marker
|
||||||
button.icon-btn(:class=`${axis}.klass.indexOf('error') !== -1 ? 'state-red' : (${axis}.homed ? 'state-green' : 'state-amber')`,
|
button.icon-btn(:disabled="!is_idle",
|
||||||
:disabled="!is_idle",
|
:title=`'Home ${axis.toUpperCase()} axis.'`,
|
||||||
:title=`${axis}.title`,
|
|
||||||
@click=`home('${axis}')`)
|
@click=`home('${axis}')`)
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
|
// W axis (auxiliary) — no offset, no set-zero / no set-position
|
||||||
|
.dro-row(:class="w.klass + ' ' + w.tklass", v-if="w.enabled",
|
||||||
|
:title="w.title")
|
||||||
|
.dro-axis.axis-w W
|
||||||
|
.dro-pos: unit-value(:value="w.pos", precision=4)
|
||||||
|
.dro-sec: unit-value(:value="w.abs", precision=3)
|
||||||
|
.dro-sec —
|
||||||
|
.dro-state
|
||||||
|
span.chip(:class="w.homed ? 'chip-green' : 'chip-amber'")
|
||||||
|
.fa(:class="'fa-' + w.icon")
|
||||||
|
| {{w.state}}
|
||||||
|
.dro-toolpath
|
||||||
|
span.chip.chip-green
|
||||||
|
.fa(:class="'fa-' + w.ticon")
|
||||||
|
| {{w.tstate}}
|
||||||
|
.actions-cell
|
||||||
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
|
.fa.fa-cog
|
||||||
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
|
.fa.fa-map-marker
|
||||||
|
button.icon-btn(:disabled="!w.enabled",
|
||||||
|
title="Home W axis.", @click="aux_home()")
|
||||||
|
.fa.fa-home
|
||||||
|
|
||||||
// ----- Status strip -----
|
// ----- Status strip -----
|
||||||
.status-strip
|
.status-strip
|
||||||
.stat-card
|
.stat-card
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ script#indicators-template(type="text/x-template")
|
|||||||
|
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
.fa.fa-circle-plus.io
|
.fa.fa-plus-circle.io
|
||||||
th Hi/+3.3v
|
th Hi/+3.3v
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
.fa.fa-circle-minus.io
|
.fa.fa-minus-circle.io
|
||||||
th Lo/Gnd
|
th Lo/Gnd
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
@@ -22,7 +22,7 @@ script#indicators-template(type="text/x-template")
|
|||||||
th Inactive
|
th Inactive
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
.far.fa-circle.io
|
.fa.fa-circle-o.io
|
||||||
th Tristated/Disabled
|
th Tristated/Disabled
|
||||||
|
|
||||||
table.inputs
|
table.inputs
|
||||||
@@ -169,14 +169,14 @@ script#indicators-template(type="text/x-template")
|
|||||||
|
|
||||||
tr
|
tr
|
||||||
th Motor
|
th Motor
|
||||||
th(title="Overtemperature fault"): .fa.fa-temperature-full
|
th(title="Overtemperature fault"): .fa.fa-thermometer-full
|
||||||
th(title="Overcurrent motor channel A") A #[.fa.fa-bolt]
|
th(title="Overcurrent motor channel A") A #[.fa.fa-bolt]
|
||||||
th(title="Predriver fault motor channel A")
|
th(title="Predriver fault motor channel A")
|
||||||
| A #[.fa.fa-triangle-exclamation]
|
| A #[.fa.fa-exclamation-triangle]
|
||||||
th(title="Overcurrent motor channel B") B #[.fa.fa-bolt]
|
th(title="Overcurrent motor channel B") B #[.fa.fa-bolt]
|
||||||
th(title="Predriver fault motor channel B")
|
th(title="Predriver fault motor channel B")
|
||||||
| B #[.fa.fa-triangle-exclamation]
|
| B #[.fa.fa-exclamation-triangle]
|
||||||
th(title="Driver communication failure"): .fa.fa-handshake
|
th(title="Driver communication failure"): .fa.fa-handshake-o
|
||||||
th(title="Reset all motor flags")
|
th(title="Reset all motor flags")
|
||||||
.fa.fa-eraser(@click="motor_reset()")
|
.fa.fa-eraser(@click="motor_reset()")
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ script#path-viewer-template(type="text/x-template")
|
|||||||
.path-viewer-toolbar
|
.path-viewer-toolbar
|
||||||
.tool-button(title="Toggle path view size.",
|
.tool-button(title="Toggle path view size.",
|
||||||
@click="small = !small", :class="{active: !small}")
|
@click="small = !small", :class="{active: !small}")
|
||||||
.fa.fa-up-down-left-right
|
.fa.fa-arrows-alt
|
||||||
|
|
||||||
.tool-button(@click="showTool = !showTool", :class="{active: showTool}",
|
.tool-button(@click="showTool = !showTool", :class="{active: showTool}",
|
||||||
title="Show/hide tool.")
|
title="Show/hide tool.")
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ script#program-view-template(type="text/x-template")
|
|||||||
span STOP
|
span STOP
|
||||||
button.action-btn(@click="open_folder", :disabled="!is_ready",
|
button.action-btn(@click="open_folder", :disabled="!is_ready",
|
||||||
title="Upload a new GCode folder.")
|
title="Upload a new GCode folder.")
|
||||||
.fa.fa-folder-plus.ico
|
.fa.fa-folder-arrow-up.ico
|
||||||
span UPLOAD FOLDER
|
span UPLOAD FOLDER
|
||||||
form.gcode-folder-input.file-upload
|
form.gcode-folder-input.file-upload
|
||||||
input#folderInput(type="file", @change="upload_folder",
|
input#folderInput(type="file", @change="upload_folder",
|
||||||
@@ -126,15 +126,10 @@ script#program-view-template(type="text/x-template")
|
|||||||
.fa.fa-arrow-down-wide-short
|
.fa.fa-arrow-down-wide-short
|
||||||
| {{files_sortby}}
|
| {{files_sortby}}
|
||||||
|
|
||||||
// Body: gcode listing on the left, 3D viewer on the right.
|
// Body: gcode listing on the left, 3D viewer on the right
|
||||||
// The 3D path-viewer is suppressed when the UI is loaded by
|
.program-body
|
||||||
// the Pi's onboard kiosk browser — the VideoCore IV cannot
|
|
||||||
// run three.js at a usable frame rate. Off-Pi clients still
|
|
||||||
// see the full split.
|
|
||||||
.program-body(:class="{'no-preview': is_kiosk}")
|
|
||||||
gcode-viewer
|
gcode-viewer
|
||||||
path-viewer(v-if="!is_kiosk", :toolpath="toolpath",
|
path-viewer(:toolpath="toolpath", :state="state", :config="config")
|
||||||
:state="state", :config="config")
|
|
||||||
|
|
||||||
.progress-bar(v-if="toolpath_progress && toolpath_progress < 1",
|
.progress-bar(v-if="toolpath_progress && toolpath_progress < 1",
|
||||||
title="Simulating GCode to check for errors, calculate ETA and generate 3D view.")
|
title="Simulating GCode to check for errors, calculate ETA and generate 3D view.")
|
||||||
|
|||||||
@@ -24,33 +24,21 @@ script#settings-shell-view-template(type="text/x-template")
|
|||||||
// Explicit v-if cascade so the inner template swaps reactively
|
// Explicit v-if cascade so the inner template swaps reactively
|
||||||
// when sub changes (Vue 1's `<component :is>` does not always
|
// when sub changes (Vue 1's `<component :is>` does not always
|
||||||
// re-evaluate dynamic strings inside a kept-alive parent).
|
// re-evaluate dynamic strings inside a kept-alive parent).
|
||||||
// The Svelte settings views read many config keys eagerly on
|
settings-view-inner(v-if="sub === 'settings'",
|
||||||
// attach (settings.units, settings.easy-adapter, motion.*),
|
|
||||||
// so we gate the inner mount on config_ready.
|
|
||||||
settings-view-inner(v-if="sub === 'settings' && config_ready",
|
|
||||||
section="display",
|
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
settings-view-inner(v-if="sub === 'probing' && config_ready",
|
admin-general-view(v-if="sub === 'admin-general'",
|
||||||
section="probing",
|
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
settings-view-inner(v-if="sub === 'gcode' && config_ready",
|
admin-network-view(v-if="sub === 'admin-network'",
|
||||||
section="gcode",
|
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
admin-general-view(v-if="sub === 'admin-general' && config_ready",
|
motor-view(v-if="sub === 'motor'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
admin-network-view(v-if="sub === 'admin-network' && config_ready",
|
tool-view(v-if="sub === 'tool'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
motor-view(v-if="sub === 'motor' && config_ready",
|
io-view(v-if="sub === 'io'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
tool-view(v-if="sub === 'tool' && config_ready",
|
macros-view(v-if="sub === 'macros'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
io-view(v-if="sub === 'io' && config_ready",
|
help-view(v-if="sub === 'help'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
macros-view(v-if="sub === 'macros' && config_ready",
|
cheat-sheet-view(v-if="sub === 'cheat-sheet'",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
help-view(v-if="sub === 'help' && config_ready",
|
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
|
||||||
cheat-sheet-view(v-if="sub === 'cheat-sheet' && config_ready",
|
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
|
||||||
.settings-loading(v-if="!config_ready")
|
|
||||||
| Loading configuration…
|
|
||||||
|
|||||||
477
src/py/bbctrl/AuxAxis.py
Normal file
477
src/py/bbctrl/AuxAxis.py
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# AuxAxis - W-axis serial driver for the auxcnc ESP32 controller
|
||||||
|
#
|
||||||
|
# Owns /dev/ttyUSB0 (or whatever serial.port is configured to). Provides
|
||||||
|
# blocking RPCs for use from a hook thread. Maintains:
|
||||||
|
#
|
||||||
|
# - aux_present : True if serial is open and we've seen a boot banner
|
||||||
|
# - aux_homed : True if we've successfully run HOME since last reset
|
||||||
|
# - aux_pos : current logical position in mm (from ESP step counter
|
||||||
|
# * (1 / steps_per_mm * dir_sign))
|
||||||
|
#
|
||||||
|
# Real-time decisions (limit switch monitoring, step pulse generation) live
|
||||||
|
# on the ESP. The host is responsible for units, soft limits, and tracking
|
||||||
|
# whether we've ever boot-cycled the ESP since last home.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError:
|
||||||
|
serial = None
|
||||||
|
|
||||||
|
|
||||||
|
# Default config; overridden by ./aux.json or ctrl.config.
|
||||||
|
DEFAULTS = {
|
||||||
|
'enabled': False,
|
||||||
|
'port': '/dev/ttyUSB0',
|
||||||
|
'baud': 115200,
|
||||||
|
'steps_per_mm': 80.0, # logical steps per mm of W travel
|
||||||
|
'dir_sign': 1, # +1 or -1: maps logical+ to motor+ steps
|
||||||
|
'min_w': 0.0, # soft limit min (mm)
|
||||||
|
'max_w': 100.0, # soft limit max (mm)
|
||||||
|
'max_feed_mm_min': 600.0, # informational; rate caps are on the ESP
|
||||||
|
'home_dir': '-', # which direction is "toward limit" (host's view)
|
||||||
|
'home_position_mm': 0.0, # mm value to assign at home
|
||||||
|
# ESP-side homing rates (steps/sec). Pushed via HOMECFG on connect.
|
||||||
|
'home_fast_sps': 4000,
|
||||||
|
'home_slow_sps': 400,
|
||||||
|
'home_backoff_steps': 200,
|
||||||
|
'home_maxtravel_steps': 200000,
|
||||||
|
'step_max_sps': 4000,
|
||||||
|
'step_accel_sps2': 16000,
|
||||||
|
'step_start_sps': 200,
|
||||||
|
'limit_low': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AuxAxisError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuxAxis(object):
|
||||||
|
def __init__(self, ctrl):
|
||||||
|
self.ctrl = ctrl
|
||||||
|
self.log = ctrl.log.get('AuxAxis')
|
||||||
|
|
||||||
|
self._cfg = dict(DEFAULTS)
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
self._sp = None
|
||||||
|
self._sp_lock = threading.Lock() # serial write/RPC serialization
|
||||||
|
self._rx_lock = threading.Lock() # read-line buffer access
|
||||||
|
self._reader_thread = None
|
||||||
|
self._stop = threading.Event()
|
||||||
|
|
||||||
|
# Pending replies waiting for a [topic] line. Single-slot since we
|
||||||
|
# serialize RPCs via _sp_lock.
|
||||||
|
self._pending_topics = []
|
||||||
|
self._pending_replies = []
|
||||||
|
self._pending_cv = threading.Condition()
|
||||||
|
|
||||||
|
# Async lines that aren't replies (e.g. logs) are simply logged.
|
||||||
|
self._present = False
|
||||||
|
self._homed = False
|
||||||
|
self._pos_steps = 0 # ESP step counter mirror
|
||||||
|
|
||||||
|
# Publish initial state
|
||||||
|
self._publish_state()
|
||||||
|
|
||||||
|
if not self._cfg['enabled']:
|
||||||
|
self.log.info('Aux axis disabled in config')
|
||||||
|
return
|
||||||
|
|
||||||
|
if serial is None:
|
||||||
|
self.log.error('pyserial not available; aux axis disabled')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._open()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ config
|
||||||
|
|
||||||
|
def _config_path(self):
|
||||||
|
return self.ctrl.get_path(filename='aux.json')
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
path = self._config_path()
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
user = json.load(f)
|
||||||
|
# Be permissive; ignore unknown keys.
|
||||||
|
for k, v in user.items():
|
||||||
|
if k in self._cfg:
|
||||||
|
self._cfg[k] = v
|
||||||
|
self.log.info('Loaded aux config from %s' % path)
|
||||||
|
except Exception:
|
||||||
|
self.log.error('Failed to read aux.json: %s'
|
||||||
|
% traceback.format_exc())
|
||||||
|
|
||||||
|
def save_config(self, cfg):
|
||||||
|
merged = dict(DEFAULTS)
|
||||||
|
for k, v in cfg.items():
|
||||||
|
if k in DEFAULTS:
|
||||||
|
merged[k] = v
|
||||||
|
path = self._config_path()
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump(merged, f, indent=2)
|
||||||
|
self._cfg = merged
|
||||||
|
self.log.info('Saved aux config')
|
||||||
|
# Push the relevant pieces to the ESP if connected.
|
||||||
|
if self._present:
|
||||||
|
try:
|
||||||
|
self._push_homecfg()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning('Could not push HOMECFG after save: %s' % e)
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
return dict(self._cfg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ public
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self):
|
||||||
|
return bool(self._cfg.get('enabled', False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def present(self):
|
||||||
|
return self._present
|
||||||
|
|
||||||
|
@property
|
||||||
|
def homed(self):
|
||||||
|
return self._homed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position_mm(self):
|
||||||
|
return self._steps_to_mm(self._pos_steps)
|
||||||
|
|
||||||
|
def home(self):
|
||||||
|
"""Run the homing cycle on the ESP. Blocks until done. Raises on
|
||||||
|
failure. Updates aux_homed and aux_pos."""
|
||||||
|
self._require_present()
|
||||||
|
line = self._rpc('HOME', topic='home', timeout=120.0)
|
||||||
|
# line is the body after '[home] '
|
||||||
|
if line.startswith('done'):
|
||||||
|
# ESP set its counter to home_zero; mirror that.
|
||||||
|
new_pos = self._parse_kv_int(line, 'pos', 0)
|
||||||
|
self._pos_steps = new_pos
|
||||||
|
self._homed = True
|
||||||
|
# Translate to home_position_mm. Conceptually the host says
|
||||||
|
# "after homing, W is here in mm". We achieve that by setting
|
||||||
|
# the ESP counter (WPOS) so the mm conversion works out.
|
||||||
|
target_pos = self._mm_to_steps(self._cfg['home_position_mm'])
|
||||||
|
if target_pos != new_pos:
|
||||||
|
self._rpc('WPOS %d' % target_pos, topic='ok', timeout=2.0)
|
||||||
|
self._pos_steps = target_pos
|
||||||
|
self._publish_state()
|
||||||
|
return
|
||||||
|
# failure
|
||||||
|
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||||
|
raise AuxAxisError('Homing failed: %s' % reason)
|
||||||
|
|
||||||
|
def move_abs_mm(self, target_mm):
|
||||||
|
"""Move to absolute logical W position (mm). Blocks until done."""
|
||||||
|
self._require_present()
|
||||||
|
self._check_limits(target_mm)
|
||||||
|
target_steps = self._mm_to_steps(target_mm)
|
||||||
|
delta = target_steps - self._pos_steps
|
||||||
|
if delta == 0:
|
||||||
|
return
|
||||||
|
self._do_steps(delta)
|
||||||
|
|
||||||
|
def move_rel_mm(self, delta_mm):
|
||||||
|
"""Move by delta mm relative to current position. Blocks until done."""
|
||||||
|
self._require_present()
|
||||||
|
target_mm = self.position_mm + delta_mm
|
||||||
|
self._check_limits(target_mm)
|
||||||
|
target_steps = self._mm_to_steps(target_mm)
|
||||||
|
delta = target_steps - self._pos_steps
|
||||||
|
if delta == 0:
|
||||||
|
return
|
||||||
|
self._do_steps(delta)
|
||||||
|
|
||||||
|
def set_position_mm(self, mm):
|
||||||
|
"""Set current W to <mm> without moving (G92-style for W)."""
|
||||||
|
self._require_present()
|
||||||
|
steps = self._mm_to_steps(mm)
|
||||||
|
self._rpc('WPOS %d' % steps, topic='ok', timeout=2.0)
|
||||||
|
self._pos_steps = steps
|
||||||
|
# WPOS clears homed on the ESP; mirror it.
|
||||||
|
self._homed = False
|
||||||
|
self._publish_state()
|
||||||
|
|
||||||
|
def jog_steps(self, steps):
|
||||||
|
"""Raw step move bypassing mm conversion and soft limits.
|
||||||
|
Used by manual jog UI when axis isn't homed yet."""
|
||||||
|
self._require_present()
|
||||||
|
if steps == 0:
|
||||||
|
return
|
||||||
|
self._do_steps(int(steps), ignore_limits=True)
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""Cancel any running ESP motion immediately."""
|
||||||
|
if not self._present:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Don't take the RPC lock; ABORT must be able to interrupt.
|
||||||
|
self._send_raw('ABORT')
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning('ABORT send failed: %s' % e)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._stop.set()
|
||||||
|
try:
|
||||||
|
if self._sp is not None:
|
||||||
|
self._sp.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ guts
|
||||||
|
|
||||||
|
def _require_present(self):
|
||||||
|
if not self.enabled:
|
||||||
|
raise AuxAxisError('Aux axis disabled')
|
||||||
|
if not self._present:
|
||||||
|
raise AuxAxisError('Aux axis not connected')
|
||||||
|
|
||||||
|
def _check_limits(self, target_mm):
|
||||||
|
lo = float(self._cfg['min_w'])
|
||||||
|
hi = float(self._cfg['max_w'])
|
||||||
|
if hi <= lo:
|
||||||
|
return # no limits
|
||||||
|
if target_mm < lo - 1e-6 or target_mm > hi + 1e-6:
|
||||||
|
raise AuxAxisError(
|
||||||
|
'W=%.3f out of soft limits [%.3f, %.3f]' % (target_mm, lo, hi))
|
||||||
|
|
||||||
|
def _mm_to_steps(self, mm):
|
||||||
|
spm = float(self._cfg['steps_per_mm'])
|
||||||
|
sign = 1 if int(self._cfg.get('dir_sign', 1)) >= 0 else -1
|
||||||
|
return int(round(mm * spm * sign))
|
||||||
|
|
||||||
|
def _steps_to_mm(self, steps):
|
||||||
|
spm = float(self._cfg['steps_per_mm']) or 1.0
|
||||||
|
sign = 1 if int(self._cfg.get('dir_sign', 1)) >= 0 else -1
|
||||||
|
return (steps / spm) * sign
|
||||||
|
|
||||||
|
def _do_steps(self, signed_count, ignore_limits=False):
|
||||||
|
max_rate = int(self._cfg['step_max_sps'])
|
||||||
|
accel = int(self._cfg['step_accel_sps2'])
|
||||||
|
safe_flag = 0 if ignore_limits else 1
|
||||||
|
cmd = 'STEPS %d maxrate=%d accel=%d safe=%d' % (
|
||||||
|
signed_count, max_rate, accel, safe_flag)
|
||||||
|
line = self._rpc(cmd, topic='step', timeout=300.0)
|
||||||
|
# line: "done count=N pos=P limit=L" or "aborted count=N pos=P [reason=...]"
|
||||||
|
if line.startswith('done'):
|
||||||
|
self._pos_steps = self._parse_kv_int(line, 'pos', self._pos_steps)
|
||||||
|
self._publish_state()
|
||||||
|
return
|
||||||
|
# aborted
|
||||||
|
self._pos_steps = self._parse_kv_int(line, 'pos', self._pos_steps)
|
||||||
|
self._publish_state()
|
||||||
|
reason = self._parse_kv_str(line, 'reason')
|
||||||
|
if reason == 'limit':
|
||||||
|
self._homed = False
|
||||||
|
raise AuxAxisError('W move aborted by limit switch')
|
||||||
|
raise AuxAxisError('W move aborted: %s' % line)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ serial I/O
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
port = self._cfg['port']
|
||||||
|
baud = int(self._cfg['baud'])
|
||||||
|
try:
|
||||||
|
self._sp = serial.Serial(port, baud, timeout=0.2)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error('Could not open %s: %s' % (port, e))
|
||||||
|
self._sp = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log.info('Opened %s @ %d' % (port, baud))
|
||||||
|
self._reader_thread = threading.Thread(
|
||||||
|
target=self._reader_loop, name='AuxAxis-rx', daemon=True)
|
||||||
|
self._reader_thread.start()
|
||||||
|
|
||||||
|
# Give the ESP a moment to settle, then push HOMECFG and query state.
|
||||||
|
# This runs in a background thread to avoid blocking startup.
|
||||||
|
threading.Thread(target=self._on_connect, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_connect(self):
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
self._push_homecfg()
|
||||||
|
self._refresh_state()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning('Aux post-connect setup failed: %s' % e)
|
||||||
|
|
||||||
|
def _push_homecfg(self):
|
||||||
|
c = self._cfg
|
||||||
|
cmd = ('HOMECFG dir=%s fast=%d slow=%d backoff=%d maxtravel=%d '
|
||||||
|
'zero=0 accel=%d step_max=%d step_start=%d limit_low=%d') % (
|
||||||
|
c['home_dir'],
|
||||||
|
int(c['home_fast_sps']),
|
||||||
|
int(c['home_slow_sps']),
|
||||||
|
int(c['home_backoff_steps']),
|
||||||
|
int(c['home_maxtravel_steps']),
|
||||||
|
int(c['step_accel_sps2']),
|
||||||
|
int(c['step_max_sps']),
|
||||||
|
int(c['step_start_sps']),
|
||||||
|
1 if c['limit_low'] else 0,
|
||||||
|
)
|
||||||
|
self._rpc(cmd, topic='homecfg', timeout=3.0)
|
||||||
|
|
||||||
|
def _refresh_state(self):
|
||||||
|
try:
|
||||||
|
r = self._rpc('WPOS?', topic='wpos', timeout=2.0)
|
||||||
|
self._pos_steps = int(r.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
r = self._rpc('HOMED?', topic='homed', timeout=2.0)
|
||||||
|
self._homed = (r.strip() == '1')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._publish_state()
|
||||||
|
|
||||||
|
def _reader_loop(self):
|
||||||
|
buf = b''
|
||||||
|
while not self._stop.is_set():
|
||||||
|
sp = self._sp
|
||||||
|
if sp is None:
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
chunk = sp.read(256)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning('Aux serial read error: %s' % e)
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
buf += chunk
|
||||||
|
while True:
|
||||||
|
nl = buf.find(b'\n')
|
||||||
|
if nl < 0:
|
||||||
|
break
|
||||||
|
line = buf[:nl].rstrip(b'\r').decode('utf-8', errors='replace')
|
||||||
|
buf = buf[nl+1:]
|
||||||
|
self._on_line(line)
|
||||||
|
|
||||||
|
def _on_line(self, line):
|
||||||
|
if not line:
|
||||||
|
return
|
||||||
|
# Boot banner -> reset homed flag.
|
||||||
|
if line.startswith('[boot]'):
|
||||||
|
self.log.warning('Aux ESP booted: %s' % line)
|
||||||
|
self._homed = False
|
||||||
|
self._present = True
|
||||||
|
self._publish_state()
|
||||||
|
self.ctrl.state.add_message(
|
||||||
|
'W axis controller restarted - re-home before use')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Topic dispatch: "[topic] body..."
|
||||||
|
if line.startswith('[') and ']' in line:
|
||||||
|
rb = line.index(']')
|
||||||
|
topic = line[1:rb]
|
||||||
|
body = line[rb+1:].lstrip()
|
||||||
|
# Mark present on first known topic.
|
||||||
|
if not self._present:
|
||||||
|
self._present = True
|
||||||
|
self._publish_state()
|
||||||
|
# Match against the head of the pending queue.
|
||||||
|
with self._pending_cv:
|
||||||
|
if (self._pending_topics
|
||||||
|
and topic in self._pending_topics[0]):
|
||||||
|
# Pop and deliver
|
||||||
|
self._pending_topics.pop(0)
|
||||||
|
self._pending_replies.append(body)
|
||||||
|
self._pending_cv.notify_all()
|
||||||
|
return
|
||||||
|
# Async informational line; just log.
|
||||||
|
self.log.info('aux: %s' % line)
|
||||||
|
else:
|
||||||
|
self.log.info('aux: %s' % line)
|
||||||
|
|
||||||
|
def _send_raw(self, cmd):
|
||||||
|
sp = self._sp
|
||||||
|
if sp is None:
|
||||||
|
raise AuxAxisError('Serial not open')
|
||||||
|
if not cmd.endswith('\n'):
|
||||||
|
cmd = cmd + '\n'
|
||||||
|
sp.write(cmd.encode('utf-8'))
|
||||||
|
sp.flush()
|
||||||
|
|
||||||
|
def _rpc(self, cmd, topic, timeout=5.0):
|
||||||
|
"""Send `cmd`, wait for a reply line whose topic is in `topic`.
|
||||||
|
topic may be a single string or a tuple/list of acceptable topics
|
||||||
|
(e.g. ('home', 'err'))."""
|
||||||
|
if isinstance(topic, str):
|
||||||
|
topics = (topic, 'err')
|
||||||
|
else:
|
||||||
|
topics = tuple(topic) + ('err',)
|
||||||
|
|
||||||
|
with self._sp_lock:
|
||||||
|
with self._pending_cv:
|
||||||
|
self._pending_topics.append(topics)
|
||||||
|
self._pending_replies = [] # reset
|
||||||
|
self.log.info('aux >> %s' % cmd.strip())
|
||||||
|
self._send_raw(cmd)
|
||||||
|
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
with self._pending_cv:
|
||||||
|
while not self._pending_replies:
|
||||||
|
remaining = deadline - time.time()
|
||||||
|
if remaining <= 0:
|
||||||
|
# Drop the pending slot so we don't capture a
|
||||||
|
# late reply meant for the next caller.
|
||||||
|
try:
|
||||||
|
self._pending_topics.remove(topics)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
raise AuxAxisError(
|
||||||
|
'Timeout waiting for %s reply to "%s"'
|
||||||
|
% (topics, cmd.strip()))
|
||||||
|
self._pending_cv.wait(timeout=remaining)
|
||||||
|
reply = self._pending_replies.pop(0)
|
||||||
|
self.log.info('aux << %s' % reply)
|
||||||
|
if reply.startswith('err') or reply.startswith('error'):
|
||||||
|
raise AuxAxisError('ESP error: %s' % reply)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_kv_int(line, key, default=0):
|
||||||
|
# Parse "key=N" (signed integer) out of a line.
|
||||||
|
for tok in line.split():
|
||||||
|
if tok.startswith(key + '='):
|
||||||
|
try:
|
||||||
|
return int(tok.split('=', 1)[1])
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
return default
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_kv_str(line, key, default=''):
|
||||||
|
for tok in line.split():
|
||||||
|
if tok.startswith(key + '='):
|
||||||
|
return tok.split('=', 1)[1]
|
||||||
|
return default
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ state push
|
||||||
|
|
||||||
|
def _publish_state(self):
|
||||||
|
st = self.ctrl.state
|
||||||
|
try:
|
||||||
|
st.set('aux_present', bool(self._present))
|
||||||
|
st.set('aux_homed', bool(self._homed))
|
||||||
|
st.set('aux_pos', round(self.position_mm, 4))
|
||||||
|
st.set('aux_enabled', bool(self.enabled))
|
||||||
|
except Exception:
|
||||||
|
# During very early startup, state may not be ready.
|
||||||
|
pass
|
||||||
237
src/py/bbctrl/AuxPreprocessor.py
Normal file
237
src/py/bbctrl/AuxPreprocessor.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# AuxPreprocessor - rewrite W-axis G-code into hook calls
|
||||||
|
#
|
||||||
|
# The bbctrl planner only understands xyzabc. We expose a virtual W axis by
|
||||||
|
# rewriting the G-code file *before* it is fed to gplan, replacing each W
|
||||||
|
# move with a (MSG,HOOK:aux:...) line that the host's hook handler turns
|
||||||
|
# into a STEPS or HOME command on the ESP.
|
||||||
|
#
|
||||||
|
# Rules:
|
||||||
|
# - Mixed-axis blocks (W together with XYZABC) are split into two
|
||||||
|
# sequential blocks. By default the W move runs first; configurable.
|
||||||
|
# - G90/G91/G20/G21 modal state is tracked so we can convert relative-W
|
||||||
|
# and inch-W into the absolute mm value the hook handler expects.
|
||||||
|
# - G28 W0 / G28.2 W0 -> HOOK:aux_home
|
||||||
|
# - G92 Wx -> HOOK:aux_setzero:<mm>
|
||||||
|
# - G53 + W not specially handled (W only knows machine coords)
|
||||||
|
# - Lines inside parentheses or after `;` are passed through.
|
||||||
|
#
|
||||||
|
# The preprocessor is intentionally conservative: anything it doesn't
|
||||||
|
# understand involving W is left alone with a warning, so motion lands in
|
||||||
|
# gplan which will complain loudly rather than silently misbehaving.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
# Match a word like "W12.5" or "W-3" or "w0". Also matches inside the same
|
||||||
|
# line as XYZ words. We pull W out specifically.
|
||||||
|
_W_TOKEN_RE = re.compile(r'(?<![A-Za-z_0-9])[Ww]\s*([-+]?\d*\.?\d+)')
|
||||||
|
|
||||||
|
# Detect any axis-bearing word (so we can tell mixed-axis lines apart).
|
||||||
|
_AXIS_WORD_RE = re.compile(r'(?<![A-Za-z_0-9])[XYZABCxyzabc]\s*[-+]?\d*\.?\d+')
|
||||||
|
|
||||||
|
# Strip line comments so we don't get fooled by "(W axis)".
|
||||||
|
_PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
|
||||||
|
|
||||||
|
# Modal G-code groups we care about.
|
||||||
|
_MODAL_RE = re.compile(r'(?<![A-Za-z_0-9])[Gg]\s*0*(\d+(?:\.\d+)?)')
|
||||||
|
|
||||||
|
|
||||||
|
class AuxPreprocessorError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuxPreprocessor(object):
|
||||||
|
def __init__(self, log=None, w_first=True):
|
||||||
|
self.log = log
|
||||||
|
# If True, on a mixed-axis line (e.g. G1 X10 W5), emit the W move
|
||||||
|
# first, then the XYZ move. Set False to invert.
|
||||||
|
self.w_first = w_first
|
||||||
|
|
||||||
|
def _info(self, msg):
|
||||||
|
if self.log:
|
||||||
|
self.log.info(msg)
|
||||||
|
|
||||||
|
def _warn(self, msg):
|
||||||
|
if self.log:
|
||||||
|
self.log.warning(msg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ scan
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def file_uses_w(path):
|
||||||
|
"""Quick check: does this file contain any W-axis word? Used to skip
|
||||||
|
preprocessing entirely for files that don't care about W."""
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
for line in f:
|
||||||
|
code = _PAREN_COMMENT_RE.sub('', line)
|
||||||
|
code = code.split(';', 1)[0]
|
||||||
|
if _W_TOKEN_RE.search(code):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ core
|
||||||
|
|
||||||
|
def _strip_w(self, line):
|
||||||
|
"""Return (line_without_w, w_value_str_or_None). Only first W kept."""
|
||||||
|
m = _W_TOKEN_RE.search(line)
|
||||||
|
if m is None:
|
||||||
|
return line, None
|
||||||
|
# Remove just the matched W<num> token, preserving surrounding spaces.
|
||||||
|
rewritten = line[:m.start()] + line[m.end():]
|
||||||
|
return rewritten, m.group(1)
|
||||||
|
|
||||||
|
def _has_other_axis(self, code_no_w):
|
||||||
|
return _AXIS_WORD_RE.search(code_no_w) is not None
|
||||||
|
|
||||||
|
def _detect_modals(self, code, modal):
|
||||||
|
"""Update modal dict in-place from G-codes on this line."""
|
||||||
|
for mm in _MODAL_RE.finditer(code):
|
||||||
|
try:
|
||||||
|
g = float(mm.group(1))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if g == 90: modal['abs'] = True
|
||||||
|
elif g == 91: modal['abs'] = False
|
||||||
|
elif g == 20: modal['inch'] = True
|
||||||
|
elif g == 21: modal['inch'] = False
|
||||||
|
# G28 / G28.2 / G92 are detected case-by-case below.
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_g28_like(code):
|
||||||
|
# Match G28 or G28.2 (homing).
|
||||||
|
return bool(re.search(r'(?<![A-Za-z_0-9])[Gg]\s*0*28(?:\.2)?(?![\w.])', code))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_g92(code):
|
||||||
|
return bool(re.search(r'(?<![A-Za-z_0-9])[Gg]\s*0*92(?![\w.])', code))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ run
|
||||||
|
|
||||||
|
def process(self, src_path, dst_path):
|
||||||
|
"""Read src_path, write rewritten G-code to dst_path. Returns True
|
||||||
|
if any rewrite happened."""
|
||||||
|
modal = {'abs': True, 'inch': False} # G90 G21 are common defaults
|
||||||
|
rewrote_any = False
|
||||||
|
|
||||||
|
with open(src_path, 'r', encoding='utf-8', errors='replace') as fin, \
|
||||||
|
open(dst_path, 'w', encoding='utf-8') as fout:
|
||||||
|
for raw in fin:
|
||||||
|
line = raw.rstrip('\n')
|
||||||
|
|
||||||
|
# Comment-only or blank lines pass through verbatim.
|
||||||
|
code = _PAREN_COMMENT_RE.sub('', line)
|
||||||
|
code = code.split(';', 1)[0]
|
||||||
|
if not code.strip():
|
||||||
|
fout.write(raw)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update modal from G-codes on this line first (so absolute
|
||||||
|
# vs incremental matches what the planner sees for XYZ).
|
||||||
|
self._detect_modals(code, modal)
|
||||||
|
|
||||||
|
if not _W_TOKEN_RE.search(code):
|
||||||
|
fout.write(raw)
|
||||||
|
continue
|
||||||
|
|
||||||
|
rewrote_any = True
|
||||||
|
|
||||||
|
# G28[.2] W... -> aux_home (W value is ignored except as
|
||||||
|
# a flag that W is being homed).
|
||||||
|
if self._is_g28_like(code):
|
||||||
|
code_no_w, _ = self._strip_w(line)
|
||||||
|
fout.write('(MSG,HOOK:aux_home:)\n')
|
||||||
|
# Only keep the residual line if other axes were also
|
||||||
|
# present (e.g. G28.2 X0 Y0 W0 still homes X+Y). A bare
|
||||||
|
# "G28" without axis args means "home all" in gcode
|
||||||
|
# which we explicitly DON'T want to trigger from a
|
||||||
|
# W-only home command.
|
||||||
|
rest_code = _PAREN_COMMENT_RE.sub('', code_no_w)
|
||||||
|
rest_code = rest_code.split(';', 1)[0]
|
||||||
|
if self._has_other_axis(rest_code):
|
||||||
|
fout.write(code_no_w + '\n')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# G92 W... -> set W zero (or other value) without motion.
|
||||||
|
if self._is_g92(code):
|
||||||
|
line_no_w, w_val = self._strip_w(line)
|
||||||
|
target_mm = self._w_to_mm(w_val, modal, set_pos=True)
|
||||||
|
fout.write('(MSG,HOOK:aux_setzero:%g)\n' % target_mm)
|
||||||
|
rest_code = _PAREN_COMMENT_RE.sub('', line_no_w)
|
||||||
|
rest_code = rest_code.split(';', 1)[0]
|
||||||
|
if self._has_other_axis(rest_code):
|
||||||
|
fout.write(line_no_w + '\n')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Plain motion: G0/G1 etc with W word.
|
||||||
|
line_no_w, w_val = self._strip_w(line)
|
||||||
|
target_mm = self._w_to_mm(w_val, modal, set_pos=False)
|
||||||
|
# Distinguish absolute vs relative: encode both, the hook
|
||||||
|
# handler will pick the right operation.
|
||||||
|
if modal['abs']:
|
||||||
|
hook_line = '(MSG,HOOK:aux:%g)' % target_mm
|
||||||
|
else:
|
||||||
|
hook_line = '(MSG,HOOK:aux_rel:%g)' % target_mm
|
||||||
|
|
||||||
|
rest_code = _PAREN_COMMENT_RE.sub('', line_no_w)
|
||||||
|
rest_code = rest_code.split(';', 1)[0]
|
||||||
|
has_xyz = self._has_other_axis(rest_code)
|
||||||
|
|
||||||
|
if not has_xyz:
|
||||||
|
# Pure W move; drop the (now-empty) original line.
|
||||||
|
fout.write(hook_line + '\n')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mixed-axis: split. Default order is W first.
|
||||||
|
if self.w_first:
|
||||||
|
fout.write(hook_line + '\n')
|
||||||
|
fout.write(line_no_w + '\n')
|
||||||
|
else:
|
||||||
|
fout.write(line_no_w + '\n')
|
||||||
|
fout.write(hook_line + '\n')
|
||||||
|
|
||||||
|
return rewrote_any
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ unit conv
|
||||||
|
|
||||||
|
def _w_to_mm(self, w_str, modal, set_pos):
|
||||||
|
try:
|
||||||
|
v = float(w_str)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise AuxPreprocessorError('Invalid W value: %r' % w_str)
|
||||||
|
if modal['inch']:
|
||||||
|
v *= 25.4
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_file(src_path, log=None, w_first=True):
|
||||||
|
"""Convenience: rewrite src_path in place if it uses W.
|
||||||
|
Returns True if the file was rewritten."""
|
||||||
|
if not AuxPreprocessor.file_uses_w(src_path):
|
||||||
|
return False
|
||||||
|
pre = AuxPreprocessor(log=log, w_first=w_first)
|
||||||
|
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',
|
||||||
|
dir=os.path.dirname(src_path) or None)
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
rewrote = pre.process(src_path, tmp)
|
||||||
|
if rewrote:
|
||||||
|
shutil.move(tmp, src_path)
|
||||||
|
return True
|
||||||
|
os.unlink(tmp)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
os.unlink(tmp)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
@@ -71,6 +71,11 @@ class Ctrl(object):
|
|||||||
self.jog = bbctrl.Jog(self)
|
self.jog = bbctrl.Jog(self)
|
||||||
with Trace.span('ctrl.pwr'):
|
with Trace.span('ctrl.pwr'):
|
||||||
self.pwr = bbctrl.Pwr(self)
|
self.pwr = bbctrl.Pwr(self)
|
||||||
|
with Trace.span('ctrl.hooks'):
|
||||||
|
self.hooks = bbctrl.Hooks(self)
|
||||||
|
with Trace.span('ctrl.aux'):
|
||||||
|
self.aux = bbctrl.AuxAxis(self)
|
||||||
|
self._register_aux_hooks()
|
||||||
|
|
||||||
with Trace.span('ctrl.mach.connect'):
|
with Trace.span('ctrl.mach.connect'):
|
||||||
self.mach.connect()
|
self.mach.connect()
|
||||||
@@ -127,8 +132,46 @@ class Ctrl(object):
|
|||||||
self.preplanner.start()
|
self.preplanner.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _register_aux_hooks(self):
|
||||||
|
"""Wire up the auxcnc HOOK: events to AuxAxis methods."""
|
||||||
|
log = self.log.get('AuxAxis')
|
||||||
|
|
||||||
|
def _hook_move(ctx):
|
||||||
|
data = (ctx.get('data') or '').strip()
|
||||||
|
if not data:
|
||||||
|
raise Exception('aux hook missing target')
|
||||||
|
self.aux.move_abs_mm(float(data))
|
||||||
|
|
||||||
|
def _hook_move_rel(ctx):
|
||||||
|
data = (ctx.get('data') or '').strip()
|
||||||
|
if not data:
|
||||||
|
raise Exception('aux_rel hook missing delta')
|
||||||
|
self.aux.move_rel_mm(float(data))
|
||||||
|
|
||||||
|
def _hook_home(ctx):
|
||||||
|
self.aux.home()
|
||||||
|
|
||||||
|
def _hook_setzero(ctx):
|
||||||
|
data = (ctx.get('data') or '').strip()
|
||||||
|
mm = float(data) if data else 0.0
|
||||||
|
self.aux.set_position_mm(mm)
|
||||||
|
|
||||||
|
self.hooks.register_internal('aux', _hook_move,
|
||||||
|
block_unpause=True, auto_resume=True)
|
||||||
|
self.hooks.register_internal('aux_rel', _hook_move_rel,
|
||||||
|
block_unpause=True, auto_resume=True)
|
||||||
|
self.hooks.register_internal('aux_home', _hook_home,
|
||||||
|
block_unpause=True, auto_resume=True,
|
||||||
|
timeout=180)
|
||||||
|
self.hooks.register_internal('aux_setzero', _hook_setzero,
|
||||||
|
block_unpause=True, auto_resume=True)
|
||||||
|
log.info('Aux hooks registered')
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.log.get('Ctrl').info('Closing %s' % self.id)
|
self.log.get('Ctrl').info('Closing %s' % self.id)
|
||||||
self.ioloop.close()
|
self.ioloop.close()
|
||||||
self.avr.close()
|
self.avr.close()
|
||||||
self.mach.planner.close()
|
self.mach.planner.close()
|
||||||
|
try: self.aux.close()
|
||||||
|
except Exception: pass
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ class FileHandler(bbctrl.APIHandler):
|
|||||||
|
|
||||||
del (self.uploadFile)
|
del (self.uploadFile)
|
||||||
|
|
||||||
|
# If the uploaded G-code uses the virtual W axis, rewrite the
|
||||||
|
# file in place so the planner sees (MSG,HOOK:aux:*) lines
|
||||||
|
# instead of W tokens it can't parse.
|
||||||
|
try:
|
||||||
|
from bbctrl.AuxPreprocessor import preprocess_file
|
||||||
|
log = self.get_log('AuxPreprocessor')
|
||||||
|
if preprocess_file(filename.decode('utf8'), log=log):
|
||||||
|
log.info('Rewrote W-axis tokens in %s' %
|
||||||
|
self.uploadFilename)
|
||||||
|
except Exception:
|
||||||
|
self.get_log('AuxPreprocessor').exception(
|
||||||
|
'W-axis preprocess failed; uploading unchanged')
|
||||||
|
|
||||||
self.get_ctrl().preplanner.invalidate(self.uploadFilename)
|
self.get_ctrl().preplanner.invalidate(self.uploadFilename)
|
||||||
self.get_ctrl().state.add_file(self.uploadFilename)
|
self.get_ctrl().state.add_file(self.uploadFilename)
|
||||||
|
|
||||||
|
|||||||
429
src/py/bbctrl/Hooks.py
Normal file
429
src/py/bbctrl/Hooks.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Hooks - External event triggers during G-code execution
|
||||||
|
#
|
||||||
|
# Integrates with the controller's pause/unpause cycle to run external
|
||||||
|
# actions (webhooks, scripts) at specific points during G-code execution.
|
||||||
|
#
|
||||||
|
# ## How tool-change hooks work (the important one):
|
||||||
|
#
|
||||||
|
# G-code: T5 M6
|
||||||
|
#
|
||||||
|
# 1. Planner replaces M6 with tool-change override G-code (configurable).
|
||||||
|
# Default: "M0 M6 (MSG, Change tool)"
|
||||||
|
#
|
||||||
|
# 2. Planner emits: set(tool,5), pause(program), message("Change tool")
|
||||||
|
# These are sent to the AVR as serial commands.
|
||||||
|
#
|
||||||
|
# 3. AVR finishes current move, enters HOLDING state.
|
||||||
|
# Reports back: xx=HOLDING, pr="Program pause"
|
||||||
|
#
|
||||||
|
# 4. Pi: Mach._update() sees HOLDING, flushes CommandQueue.
|
||||||
|
# CommandQueue executes callbacks: state.set('tool', 5) fires.
|
||||||
|
#
|
||||||
|
# 5. Hooks._on_state_change() sees tool changed.
|
||||||
|
# Sets self._hook_busy = True, runs the hook in a thread.
|
||||||
|
# While _hook_busy, Mach.unpause() is blocked via can_unpause().
|
||||||
|
#
|
||||||
|
# 6. Machine sits in HOLDING. UI shows "Change tool" message.
|
||||||
|
# User cannot resume yet (unpause is gated).
|
||||||
|
#
|
||||||
|
# 7. Hook thread finishes (toolchanger done). Sets _hook_busy = False.
|
||||||
|
# If auto_resume is set, calls unpause automatically.
|
||||||
|
# Otherwise user clicks Continue in UI.
|
||||||
|
#
|
||||||
|
# 8. Mach.unpause() → planner.restart() → AVR UNPAUSE → motion resumes.
|
||||||
|
#
|
||||||
|
# ## Configuration (hooks.json):
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "tool-change": {
|
||||||
|
# "type": "webhook",
|
||||||
|
# "url": "http://toolchanger.local/api/change",
|
||||||
|
# "method": "POST",
|
||||||
|
# "timeout": 120,
|
||||||
|
# "block_unpause": true,
|
||||||
|
# "auto_resume": true
|
||||||
|
# },
|
||||||
|
# "program-start": {
|
||||||
|
# "type": "script",
|
||||||
|
# "command": "/usr/local/bin/dust-collector on",
|
||||||
|
# "block_unpause": false
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# block_unpause: if true, unpause is blocked until hook completes
|
||||||
|
# auto_resume: if true AND block_unpause, auto-unpause after hook done
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
|
|
||||||
|
# Events that can be hooked
|
||||||
|
HOOK_EVENTS = [
|
||||||
|
'tool-change', # M6 - tool change requested
|
||||||
|
'program-start', # Program begins running
|
||||||
|
'program-end', # M2/M30 - program ends
|
||||||
|
'pause', # M0/M1 - program pause
|
||||||
|
'estop', # Emergency stop triggered
|
||||||
|
'homing-start', # Homing cycle begins
|
||||||
|
'homing-end', # Homing cycle completes
|
||||||
|
'custom', # Triggered by (MSG,HOOK:name:data) comments
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Hooks:
|
||||||
|
def __init__(self, ctrl):
|
||||||
|
self.ctrl = ctrl
|
||||||
|
self.log = ctrl.log.get('Hooks')
|
||||||
|
self.hooks = {}
|
||||||
|
|
||||||
|
# Hook execution state
|
||||||
|
self._hook_busy = False # True while a blocking hook runs
|
||||||
|
self._hook_busy_event = None # Which event is blocking
|
||||||
|
self._hook_error = None # Error from last hook, if any
|
||||||
|
self._hook_thread = None
|
||||||
|
|
||||||
|
# In-process hook handlers registered by Python modules. Keyed by
|
||||||
|
# event name (matches what the G-code emits as HOOK:<event>).
|
||||||
|
# Take precedence over hooks.json entries with the same name.
|
||||||
|
self._internal = {}
|
||||||
|
|
||||||
|
# Track state for edge detection — must be set before add_listener
|
||||||
|
# because add_listener fires immediately with current state
|
||||||
|
self._last_cycle = ctrl.state.get('cycle', 'idle')
|
||||||
|
self._last_state = ctrl.state.get('xx', '')
|
||||||
|
self._last_tool = ctrl.state.get('tool', 0)
|
||||||
|
self._last_pause_reason = ctrl.state.get('pr', '')
|
||||||
|
# Highest message id we've already inspected for HOOK: lines.
|
||||||
|
self._last_msg_id = -1
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
# Listen for state changes
|
||||||
|
ctrl.state.add_listener(self._on_state_change)
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
# -- Config management --
|
||||||
|
|
||||||
|
def _get_config_path(self):
|
||||||
|
return self.ctrl.get_path(filename='hooks.json')
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
path = self._get_config_path()
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
self.hooks = json.load(f)
|
||||||
|
self.log.info('Loaded %d hook(s) from %s' %
|
||||||
|
(len(self.hooks), path))
|
||||||
|
except Exception:
|
||||||
|
self.log.error('Failed to load hooks.json: %s' %
|
||||||
|
traceback.format_exc())
|
||||||
|
else:
|
||||||
|
self.log.info('No hooks.json found, hooks disabled')
|
||||||
|
|
||||||
|
def save_config(self, config):
|
||||||
|
"""Save hook configuration (called from API)."""
|
||||||
|
path = self._get_config_path()
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
self.hooks = config
|
||||||
|
self.log.info('Saved %d hook(s)' % len(config))
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
return self.hooks
|
||||||
|
|
||||||
|
# -- Unpause gating (called from Mach) --
|
||||||
|
|
||||||
|
def can_unpause(self):
|
||||||
|
"""Returns True if no blocking hook is running.
|
||||||
|
Called by Mach.unpause() to gate resume."""
|
||||||
|
if self._hook_busy:
|
||||||
|
self.log.info('Unpause blocked: hook "%s" still running' %
|
||||||
|
self._hook_busy_event)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""Return current hook execution status for the UI."""
|
||||||
|
return {
|
||||||
|
'busy': self._hook_busy,
|
||||||
|
'event': self._hook_busy_event,
|
||||||
|
'error': self._hook_error,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- State change listener --
|
||||||
|
|
||||||
|
def _on_state_change(self, update):
|
||||||
|
"""Called on every state update from the controller."""
|
||||||
|
if not self._initialized:
|
||||||
|
return
|
||||||
|
state = self.ctrl.state
|
||||||
|
|
||||||
|
# Detect tool change (tool number changed while HOLDING)
|
||||||
|
if 'tool' in update:
|
||||||
|
new_tool = update['tool']
|
||||||
|
if new_tool != self._last_tool:
|
||||||
|
self._fire('tool-change', {
|
||||||
|
'old_tool': self._last_tool,
|
||||||
|
'new_tool': new_tool,
|
||||||
|
})
|
||||||
|
self._last_tool = new_tool
|
||||||
|
|
||||||
|
# Detect cycle changes
|
||||||
|
if 'cycle' in update:
|
||||||
|
new_cycle = update['cycle']
|
||||||
|
if new_cycle != self._last_cycle:
|
||||||
|
if new_cycle == 'running' and self._last_cycle == 'idle':
|
||||||
|
self._fire('program-start', {})
|
||||||
|
elif new_cycle == 'idle' and self._last_cycle == 'running':
|
||||||
|
self._fire('program-end', {})
|
||||||
|
elif new_cycle == 'homing':
|
||||||
|
self._fire('homing-start', {})
|
||||||
|
elif self._last_cycle == 'homing' and new_cycle == 'idle':
|
||||||
|
self._fire('homing-end', {})
|
||||||
|
self._last_cycle = new_cycle
|
||||||
|
|
||||||
|
# Detect AVR state changes
|
||||||
|
if 'xc' in update or 'xx' in update:
|
||||||
|
new_state = state.get('xx', '')
|
||||||
|
if new_state != self._last_state:
|
||||||
|
if new_state == 'ESTOPPED':
|
||||||
|
# Cancel any running hook on estop. The hook thread
|
||||||
|
# cannot be killed from Python, but we can ask the
|
||||||
|
# AuxAxis to send ABORT to the ESP so its in-flight
|
||||||
|
# motion stops.
|
||||||
|
if self._hook_busy:
|
||||||
|
self.log.warning('E-stop: cancelling hook "%s"' %
|
||||||
|
self._hook_busy_event)
|
||||||
|
try:
|
||||||
|
aux = getattr(self.ctrl, 'aux', None)
|
||||||
|
if aux is not None:
|
||||||
|
aux.abort()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._hook_busy = False
|
||||||
|
self._hook_busy_event = None
|
||||||
|
self._fire('estop', {})
|
||||||
|
self._last_state = new_state
|
||||||
|
|
||||||
|
# Detect pause
|
||||||
|
if 'pr' in update:
|
||||||
|
pr = update['pr']
|
||||||
|
if pr and pr != self._last_pause_reason:
|
||||||
|
self._fire('pause', {'reason': pr})
|
||||||
|
self._last_pause_reason = pr
|
||||||
|
|
||||||
|
# Detect custom hook messages emitted via (MSG,HOOK:event_name:data)
|
||||||
|
# gcode comments. State stores them as a list under 'messages'
|
||||||
|
# ([{'id': N, 'text': '...'}, ...]); fire only on new ids.
|
||||||
|
if 'messages' in update:
|
||||||
|
msgs = update['messages']
|
||||||
|
if isinstance(msgs, list):
|
||||||
|
for m in msgs:
|
||||||
|
try:
|
||||||
|
mid = m.get('id', -1)
|
||||||
|
text = m.get('text', '')
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
if mid <= self._last_msg_id:
|
||||||
|
continue
|
||||||
|
self._last_msg_id = mid
|
||||||
|
if isinstance(text, str) and text.startswith('HOOK:'):
|
||||||
|
parts = text[5:].split(':', 1)
|
||||||
|
event = parts[0]
|
||||||
|
data = parts[1] if len(parts) > 1 else ''
|
||||||
|
self._fire('custom', {
|
||||||
|
'event': event,
|
||||||
|
'data': data,
|
||||||
|
}, custom_name=event)
|
||||||
|
|
||||||
|
# -- Hook execution --
|
||||||
|
|
||||||
|
def register_internal(self, name, fn, block_unpause=True,
|
||||||
|
auto_resume=True, timeout=120):
|
||||||
|
"""Register an in-process handler for HOOK:<name> events.
|
||||||
|
|
||||||
|
fn(context) -> None. May raise. Runs synchronously in the hook
|
||||||
|
thread; while it runs and block_unpause=True, Mach.unpause is
|
||||||
|
gated."""
|
||||||
|
self._internal[name] = {
|
||||||
|
'type': 'internal',
|
||||||
|
'fn': fn,
|
||||||
|
'block_unpause': block_unpause,
|
||||||
|
'auto_resume': auto_resume,
|
||||||
|
'timeout': timeout,
|
||||||
|
}
|
||||||
|
self.log.info('Registered internal hook: %s' % name)
|
||||||
|
|
||||||
|
def _fire(self, event, context, custom_name=None):
|
||||||
|
"""Fire a hook event."""
|
||||||
|
# Internal handlers win over hooks.json entries.
|
||||||
|
hook = None
|
||||||
|
if custom_name:
|
||||||
|
hook = self._internal.get(custom_name)
|
||||||
|
if not hook:
|
||||||
|
hook = self._internal.get(event)
|
||||||
|
if not hook:
|
||||||
|
hook = self.hooks.get(event)
|
||||||
|
if custom_name and not hook:
|
||||||
|
hook = self.hooks.get(custom_name)
|
||||||
|
if not hook:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log.info('Hook firing: %s %s' % (event, json.dumps(context)))
|
||||||
|
|
||||||
|
# Add standard context
|
||||||
|
state = self.ctrl.state
|
||||||
|
context.update({
|
||||||
|
'event': event,
|
||||||
|
'position': (state.get_position()
|
||||||
|
if hasattr(state, 'get_position') else {}),
|
||||||
|
'state': state.get('xx', ''),
|
||||||
|
'cycle': state.get('cycle', 'idle'),
|
||||||
|
})
|
||||||
|
|
||||||
|
block_unpause = hook.get('block_unpause', event == 'tool-change')
|
||||||
|
auto_resume = hook.get('auto_resume', False)
|
||||||
|
|
||||||
|
if block_unpause:
|
||||||
|
# Run in thread, block unpause until done
|
||||||
|
self._hook_busy = True
|
||||||
|
self._hook_busy_event = event
|
||||||
|
self._hook_error = None
|
||||||
|
|
||||||
|
# Update UI state so frontend knows we're busy
|
||||||
|
self.ctrl.state.set('hook_busy', True)
|
||||||
|
self.ctrl.state.set('hook_event', event)
|
||||||
|
|
||||||
|
self._hook_thread = threading.Thread(
|
||||||
|
target=self._run_hook_blocking,
|
||||||
|
args=(hook, event, context, auto_resume),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self._hook_thread.start()
|
||||||
|
else:
|
||||||
|
# Fire and forget (non-blocking)
|
||||||
|
self._execute_hook(hook, context)
|
||||||
|
|
||||||
|
def _run_hook_blocking(self, hook, event, context, auto_resume):
|
||||||
|
"""Runs in a background thread. Blocks unpause until complete."""
|
||||||
|
try:
|
||||||
|
self._execute_hook(hook, context)
|
||||||
|
self.log.info('Hook "%s" completed successfully' % event)
|
||||||
|
except Exception as e:
|
||||||
|
self._hook_error = str(e)
|
||||||
|
self.log.error('Hook "%s" failed: %s' % (event, e))
|
||||||
|
finally:
|
||||||
|
self._hook_busy = False
|
||||||
|
self._hook_busy_event = None
|
||||||
|
|
||||||
|
# Schedule UI update on the ioloop thread
|
||||||
|
self.ctrl.ioloop.call_later(0, self._hook_finished, auto_resume)
|
||||||
|
|
||||||
|
def _hook_finished(self, auto_resume):
|
||||||
|
"""Called on the ioloop after a blocking hook completes."""
|
||||||
|
self.ctrl.state.set('hook_busy', False)
|
||||||
|
self.ctrl.state.set('hook_event', '')
|
||||||
|
|
||||||
|
if self._hook_error:
|
||||||
|
self.ctrl.state.set('hook_error', self._hook_error)
|
||||||
|
self.log.error('Hook error: %s' % self._hook_error)
|
||||||
|
else:
|
||||||
|
self.ctrl.state.set('hook_error', '')
|
||||||
|
|
||||||
|
if auto_resume and not self._hook_error:
|
||||||
|
self.log.info('Hook done, auto-resuming')
|
||||||
|
try:
|
||||||
|
self.ctrl.mach.unpause()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error('Auto-resume failed: %s' % e)
|
||||||
|
|
||||||
|
def _execute_hook(self, hook, context):
|
||||||
|
"""Execute a single hook (webhook, script, or internal). May block."""
|
||||||
|
hook_type = hook.get('type', 'webhook')
|
||||||
|
|
||||||
|
if hook_type == 'webhook':
|
||||||
|
self._fire_webhook(hook, context)
|
||||||
|
elif hook_type == 'script':
|
||||||
|
self._fire_script(hook, context)
|
||||||
|
elif hook_type == 'internal':
|
||||||
|
fn = hook.get('fn')
|
||||||
|
if fn is None:
|
||||||
|
raise Exception('Internal hook missing fn')
|
||||||
|
fn(context)
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown hook type: %s' % hook_type)
|
||||||
|
|
||||||
|
def _fire_webhook(self, hook, context):
|
||||||
|
"""Fire a webhook HTTP request."""
|
||||||
|
url = hook.get('url')
|
||||||
|
if not url:
|
||||||
|
raise Exception('Webhook missing url')
|
||||||
|
|
||||||
|
method = hook.get('method', 'POST').upper()
|
||||||
|
timeout = hook.get('timeout', 30)
|
||||||
|
headers = dict(hook.get('headers', {}))
|
||||||
|
body = dict(hook.get('body', {}))
|
||||||
|
|
||||||
|
# Merge context into body
|
||||||
|
body['_context'] = context
|
||||||
|
|
||||||
|
data = json.dumps(body).encode('utf-8')
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
req = Request(url, data=data, headers=headers, method=method)
|
||||||
|
self.log.info('Webhook %s %s' % (method, url))
|
||||||
|
|
||||||
|
resp = urlopen(req, timeout=timeout)
|
||||||
|
self.log.info('Webhook response: %d' % resp.status)
|
||||||
|
|
||||||
|
if resp.status >= 400:
|
||||||
|
raise Exception('Webhook returned %d' % resp.status)
|
||||||
|
|
||||||
|
def _fire_script(self, hook, context):
|
||||||
|
"""Fire a local script/command. Blocks until complete."""
|
||||||
|
command = hook.get('command')
|
||||||
|
if not command:
|
||||||
|
raise Exception('Script hook missing command')
|
||||||
|
|
||||||
|
timeout = hook.get('timeout', 120)
|
||||||
|
|
||||||
|
# Pass context as environment variables
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['HOOK_EVENT'] = context.get('event', '')
|
||||||
|
env['HOOK_STATE'] = context.get('state', '')
|
||||||
|
env['HOOK_CYCLE'] = context.get('cycle', '')
|
||||||
|
env['HOOK_DATA'] = json.dumps(context)
|
||||||
|
|
||||||
|
if 'old_tool' in context:
|
||||||
|
env['HOOK_OLD_TOOL'] = str(context['old_tool'])
|
||||||
|
if 'new_tool' in context:
|
||||||
|
env['HOOK_NEW_TOOL'] = str(context['new_tool'])
|
||||||
|
|
||||||
|
self.log.info('Script: %s' % command)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
command, shell=True, env=env,
|
||||||
|
timeout=timeout,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = result.stdout.decode('utf-8', errors='replace').strip()
|
||||||
|
stderr = result.stderr.decode('utf-8', errors='replace').strip()
|
||||||
|
|
||||||
|
if stdout:
|
||||||
|
self.log.info('Script stdout: %s' % stdout)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception('Script failed (%d): %s' %
|
||||||
|
(result.returncode, stderr or 'non-zero exit'))
|
||||||
@@ -256,6 +256,9 @@ class Mach(Comm):
|
|||||||
if cmd[0] == '$': self._query_var(cmd)
|
if cmd[0] == '$': self._query_var(cmd)
|
||||||
elif cmd[0] == '\\': super().queue_command(cmd[1:])
|
elif cmd[0] == '\\': super().queue_command(cmd[1:])
|
||||||
else:
|
else:
|
||||||
|
# Rewrite W-axis tokens in MDI input the same way the
|
||||||
|
# FileHandler rewrites uploaded files.
|
||||||
|
cmd = self._rewrite_w_mdi(cmd)
|
||||||
self._begin_cycle('mdi')
|
self._begin_cycle('mdi')
|
||||||
self.planner.mdi(cmd, with_limits)
|
self.planner.mdi(cmd, with_limits)
|
||||||
super().resume()
|
super().resume()
|
||||||
@@ -263,6 +266,35 @@ class Mach(Comm):
|
|||||||
self.mlog.info("Exception during MDI: %s" % err)
|
self.mlog.info("Exception during MDI: %s" % err)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _rewrite_w_mdi(self, cmd):
|
||||||
|
"""Apply the W-axis preprocessor to a single MDI line. Returns
|
||||||
|
possibly-multi-line G-code with HOOK: comments inserted."""
|
||||||
|
try:
|
||||||
|
from bbctrl.AuxPreprocessor import AuxPreprocessor, _W_TOKEN_RE
|
||||||
|
if not _W_TOKEN_RE.search(cmd):
|
||||||
|
return cmd
|
||||||
|
import io, tempfile, os
|
||||||
|
# AuxPreprocessor.process is file-based; route through
|
||||||
|
# tempfiles so we don't fork the regex/state logic.
|
||||||
|
pre = AuxPreprocessor(log=self.mlog)
|
||||||
|
with tempfile.NamedTemporaryFile('w', suffix='.nc',
|
||||||
|
delete=False) as fi:
|
||||||
|
fi.write(cmd if cmd.endswith('\n') else cmd + '\n')
|
||||||
|
ipath = fi.name
|
||||||
|
opath = ipath + '.out'
|
||||||
|
try:
|
||||||
|
pre.process(ipath, opath)
|
||||||
|
rewritten = open(opath).read()
|
||||||
|
finally:
|
||||||
|
try: os.unlink(ipath)
|
||||||
|
except OSError: pass
|
||||||
|
try: os.unlink(opath)
|
||||||
|
except OSError: pass
|
||||||
|
return rewritten
|
||||||
|
except Exception as e:
|
||||||
|
self.mlog.warning('W-axis MDI rewrite failed: %s' % e)
|
||||||
|
return cmd
|
||||||
|
|
||||||
def set(self, code, value):
|
def set(self, code, value):
|
||||||
super().queue_command('${}={}'.format(code, value))
|
super().queue_command('${}={}'.format(code, value))
|
||||||
|
|
||||||
@@ -349,6 +381,10 @@ class Mach(Comm):
|
|||||||
|
|
||||||
def unpause(self):
|
def unpause(self):
|
||||||
if self._is_paused():
|
if self._is_paused():
|
||||||
|
# Gate unpause on hook completion
|
||||||
|
if hasattr(self.ctrl, 'hooks') and \
|
||||||
|
not self.ctrl.hooks.can_unpause():
|
||||||
|
return
|
||||||
self.ctrl.state.set('optional_pause', False)
|
self.ctrl.state.set('optional_pause', False)
|
||||||
self._unpause()
|
self._unpause()
|
||||||
|
|
||||||
|
|||||||
@@ -766,6 +766,93 @@ class RotaryHandler(bbctrl.APIHandler):
|
|||||||
log.error('Unexpected error: {}'.format(e))
|
log.error('Unexpected error: {}'.format(e))
|
||||||
|
|
||||||
|
|
||||||
|
class HooksGetHandler(bbctrl.APIHandler):
|
||||||
|
def get(self):
|
||||||
|
self.write_json(self.get_ctrl().hooks.get_config())
|
||||||
|
|
||||||
|
|
||||||
|
class HooksSaveHandler(bbctrl.APIHandler):
|
||||||
|
def put_ok(self):
|
||||||
|
self.get_ctrl().hooks.save_config(self.json)
|
||||||
|
|
||||||
|
|
||||||
|
class HooksStatusHandler(bbctrl.APIHandler):
|
||||||
|
def get(self):
|
||||||
|
self.write_json(self.get_ctrl().hooks.get_status())
|
||||||
|
|
||||||
|
|
||||||
|
class HooksFireHandler(bbctrl.APIHandler):
|
||||||
|
def put_ok(self, event):
|
||||||
|
data = self.json if hasattr(self, 'json') and self.json else {}
|
||||||
|
self.get_ctrl().hooks._fire(event, data)
|
||||||
|
|
||||||
|
|
||||||
|
# ----- W axis (auxcnc) endpoints --------------------------------------------
|
||||||
|
|
||||||
|
class AuxConfigGetHandler(bbctrl.APIHandler):
|
||||||
|
def get(self):
|
||||||
|
self.write_json(self.get_ctrl().aux.get_config())
|
||||||
|
|
||||||
|
|
||||||
|
class AuxConfigSaveHandler(bbctrl.APIHandler):
|
||||||
|
def put_ok(self):
|
||||||
|
self.get_ctrl().aux.save_config(self.json or {})
|
||||||
|
|
||||||
|
|
||||||
|
class AuxStatusHandler(bbctrl.APIHandler):
|
||||||
|
def get(self):
|
||||||
|
aux = self.get_ctrl().aux
|
||||||
|
self.write_json({
|
||||||
|
'enabled': aux.enabled,
|
||||||
|
'present': aux.present,
|
||||||
|
'homed': aux.homed,
|
||||||
|
'pos_mm': aux.position_mm,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class AuxHomeHandler(bbctrl.APIHandler):
|
||||||
|
def put_ok(self):
|
||||||
|
# Run synchronously via the AuxAxis' own RPC; this blocks the
|
||||||
|
# request. Fine because the UI shows a spinner.
|
||||||
|
self.get_ctrl().aux.home()
|
||||||
|
|
||||||
|
|
||||||
|
class AuxAbortHandler(bbctrl.APIHandler):
|
||||||
|
def put_ok(self):
|
||||||
|
self.get_ctrl().aux.abort()
|
||||||
|
|
||||||
|
|
||||||
|
class AuxJogHandler(bbctrl.APIHandler):
|
||||||
|
"""Body: {"mm": 1.5} for relative-mm move,
|
||||||
|
{"steps": 200} for raw step move (bypasses soft limits)."""
|
||||||
|
def put_ok(self):
|
||||||
|
body = self.json or {}
|
||||||
|
aux = self.get_ctrl().aux
|
||||||
|
if 'mm' in body:
|
||||||
|
aux.move_rel_mm(float(body['mm']))
|
||||||
|
elif 'steps' in body:
|
||||||
|
aux.jog_steps(int(body['steps']))
|
||||||
|
else:
|
||||||
|
raise HTTPError(400, 'mm or steps required')
|
||||||
|
|
||||||
|
|
||||||
|
class AuxMoveHandler(bbctrl.APIHandler):
|
||||||
|
"""Body: {"mm": 12.5} absolute move in mm."""
|
||||||
|
def put_ok(self):
|
||||||
|
body = self.json or {}
|
||||||
|
if 'mm' not in body:
|
||||||
|
raise HTTPError(400, 'mm required')
|
||||||
|
self.get_ctrl().aux.move_abs_mm(float(body['mm']))
|
||||||
|
|
||||||
|
|
||||||
|
class AuxSetZeroHandler(bbctrl.APIHandler):
|
||||||
|
"""Body: {"mm": 0} set current position to <mm>."""
|
||||||
|
def put_ok(self):
|
||||||
|
body = self.json or {}
|
||||||
|
mm = float(body.get('mm', 0.0))
|
||||||
|
self.get_ctrl().aux.set_position_mm(mm)
|
||||||
|
|
||||||
|
|
||||||
class RemoteDiagnosticsHandler(bbctrl.APIHandler):
|
class RemoteDiagnosticsHandler(bbctrl.APIHandler):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
@@ -798,7 +885,6 @@ class RemoteDiagnosticsHandler(bbctrl.APIHandler):
|
|||||||
'message': e.reason or "Unknown"
|
'message': e.reason or "Unknown"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class TimingHandler(bbctrl.APIHandler):
|
class TimingHandler(bbctrl.APIHandler):
|
||||||
"""Return the bbctrl process startup timeline as JSON.
|
"""Return the bbctrl process startup timeline as JSON.
|
||||||
|
|
||||||
@@ -992,6 +1078,18 @@ class Web(tornado.web.Application):
|
|||||||
(r'/api/time', TimeHandler),
|
(r'/api/time', TimeHandler),
|
||||||
(r'/api/rotary', RotaryHandler),
|
(r'/api/rotary', RotaryHandler),
|
||||||
(r'/api/remote-diagnostics', RemoteDiagnosticsHandler),
|
(r'/api/remote-diagnostics', RemoteDiagnosticsHandler),
|
||||||
|
(r'/api/hooks', HooksGetHandler),
|
||||||
|
(r'/api/hooks/save', HooksSaveHandler),
|
||||||
|
(r'/api/hooks/status', HooksStatusHandler),
|
||||||
|
(r'/api/hooks/fire/([\w-]+)', HooksFireHandler),
|
||||||
|
(r'/api/aux/config', AuxConfigGetHandler),
|
||||||
|
(r'/api/aux/config/save', AuxConfigSaveHandler),
|
||||||
|
(r'/api/aux/status', AuxStatusHandler),
|
||||||
|
(r'/api/aux/home', AuxHomeHandler),
|
||||||
|
(r'/api/aux/abort', AuxAbortHandler),
|
||||||
|
(r'/api/aux/jog', AuxJogHandler),
|
||||||
|
(r'/api/aux/move', AuxMoveHandler),
|
||||||
|
(r'/api/aux/set-zero', AuxSetZeroHandler),
|
||||||
(r'/(.*)', StaticFileHandler,
|
(r'/(.*)', StaticFileHandler,
|
||||||
{'path': bbctrl.get_resource('http/'),
|
{'path': bbctrl.get_resource('http/'),
|
||||||
'default_filename': 'index.html'}),
|
'default_filename': 'index.html'}),
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ from bbctrl.AVR import AVR
|
|||||||
from bbctrl.AVREmu import AVREmu
|
from bbctrl.AVREmu import AVREmu
|
||||||
from bbctrl.IOLoop import IOLoop
|
from bbctrl.IOLoop import IOLoop
|
||||||
from bbctrl.MonitorTemp import MonitorTemp
|
from bbctrl.MonitorTemp import MonitorTemp
|
||||||
|
from bbctrl.Hooks import Hooks
|
||||||
|
from bbctrl.AuxAxis import AuxAxis
|
||||||
import bbctrl.Cmd as Cmd
|
import bbctrl.Cmd as Cmd
|
||||||
import bbctrl.v4l2 as v4l2
|
import bbctrl.v4l2 as v4l2
|
||||||
import bbctrl.Log as log
|
import bbctrl.Log as log
|
||||||
|
|||||||
BIN
src/resources/fonts/fontawesome-webfont.ttf
Normal file
BIN
src/resources/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
src/resources/fonts/fontawesome-webfont.woff
Normal file
BIN
src/resources/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
src/resources/fonts/fontawesome-webfont.woff2
Normal file
BIN
src/resources/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9
src/static/css/fa6.min.css
vendored
9
src/static/css/fa6.min.css
vendored
File diff suppressed because one or more lines are too long
4
src/static/css/font-awesome.min.css
vendored
Normal file
4
src/static/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -30,16 +30,6 @@ $jog-ghost-hov = #9ba6bb
|
|||||||
$jog-ink = #fff
|
$jog-ink = #fff
|
||||||
$jog-ghost-ink = $ink
|
$jog-ghost-ink = $ink
|
||||||
|
|
||||||
// Lock html + body so nothing other than the explicit .app-body or
|
|
||||||
// inner scroll containers can scroll. Without this, autofocus inside
|
|
||||||
// nested Svelte components (Settings, Admin Network, etc.) can call
|
|
||||||
// scrollIntoView() on the html element and push our fixed header off
|
|
||||||
// the top of the viewport.
|
|
||||||
html, body
|
|
||||||
height 100%
|
|
||||||
overflow hidden
|
|
||||||
overscroll-behavior none
|
|
||||||
|
|
||||||
body
|
body
|
||||||
margin 0
|
margin 0
|
||||||
font-family 'Inter', system-ui, -apple-system, sans-serif
|
font-family 'Inter', system-ui, -apple-system, sans-serif
|
||||||
@@ -88,66 +78,6 @@ tt
|
|||||||
width 100%
|
width 100%
|
||||||
overflow hidden
|
overflow hidden
|
||||||
background $body-bg
|
background $body-bg
|
||||||
// Hint to the browser that this layer is a stable composited
|
|
||||||
// surface so tab swaps inside it don't invalidate the whole page.
|
|
||||||
contain layout paint
|
|
||||||
isolation isolate
|
|
||||||
|
|
||||||
// Program tab pre-warmer. Mounts the program-view at app start so
|
|
||||||
// the WebGL canvas is already initialized when the user first
|
|
||||||
// clicks the Program tab — avoids the first-time dark flash that
|
|
||||||
// happens when WebGL is created on demand. We render the component
|
|
||||||
// at full size off-screen with the same width as the live tab so
|
|
||||||
// the canvas dims match.
|
|
||||||
.program-warmer
|
|
||||||
position absolute
|
|
||||||
left -10000px
|
|
||||||
top 0
|
|
||||||
width 1920px
|
|
||||||
height 980px
|
|
||||||
pointer-events none
|
|
||||||
visibility hidden
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Tablet / kiosk mode
|
|
||||||
//
|
|
||||||
// When <html class="tablet-mode"> is set (via ?tablet=1, sticky in
|
|
||||||
// localStorage), pin the app shell to a fixed 1920 x 1080 box and
|
|
||||||
// scale it to fit the real viewport. The scale ratio is published as
|
|
||||||
// the --tablet-scale CSS variable by the inline script in index.pug.
|
|
||||||
//
|
|
||||||
// On the actual 10.8" 1920x1080 portable monitor the scale is 1; on
|
|
||||||
// a desktop browser at e.g. 1440x900 you get a faithfully shrunk
|
|
||||||
// preview of exactly what the kiosk renders.
|
|
||||||
// =====================================================================
|
|
||||||
html.tablet-mode
|
|
||||||
background #0f172a
|
|
||||||
overflow hidden
|
|
||||||
|
|
||||||
html.tablet-mode body
|
|
||||||
margin 0
|
|
||||||
width 100vw
|
|
||||||
height 100vh
|
|
||||||
overflow hidden
|
|
||||||
display flex
|
|
||||||
align-items center
|
|
||||||
justify-content center
|
|
||||||
|
|
||||||
html.tablet-mode .app-shell
|
|
||||||
width 1920px
|
|
||||||
height 1080px
|
|
||||||
flex 0 0 auto
|
|
||||||
transform scale(var(--tablet-scale, 1))
|
|
||||||
transform-origin center center
|
|
||||||
box-shadow 0 30px 60px rgba(0, 0, 0, 0.45)
|
|
||||||
border-radius 14px
|
|
||||||
|
|
||||||
// Keep dialog hosts above the scaled shell.
|
|
||||||
html.tablet-mode #svelte-dialog-host
|
|
||||||
position fixed
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
z-index 10000
|
|
||||||
|
|
||||||
.app-body
|
.app-body
|
||||||
flex 1
|
flex 1
|
||||||
@@ -170,10 +100,7 @@ html.tablet-mode #svelte-dialog-host
|
|||||||
padding 0 24px
|
padding 0 24px
|
||||||
background $bg
|
background $bg
|
||||||
border-bottom 1px solid $line
|
border-bottom 1px solid $line
|
||||||
// sticky so the header stays visible even if a nested scroll
|
position relative
|
||||||
// container manages to move under it.
|
|
||||||
position sticky
|
|
||||||
top 0
|
|
||||||
z-index 30
|
z-index 30
|
||||||
|
|
||||||
.brand-blk
|
.brand-blk
|
||||||
@@ -717,12 +644,8 @@ span.unit
|
|||||||
text-transform capitalize
|
text-transform capitalize
|
||||||
|
|
||||||
.path-viewer-content
|
.path-viewer-content
|
||||||
// Solid dark background matching the WebGL renderer's clear
|
background-color #333
|
||||||
// colour. We used to use a gradient (#666 -> #222) but the
|
background linear-gradient(to bottom, #666 0%, #222 100%);
|
||||||
// visible-during-mount color difference between the gradient
|
|
||||||
// top and the GL clear bottom caused a brief dark flash when
|
|
||||||
// the canvas was reattached on every tab switch.
|
|
||||||
background-color #222
|
|
||||||
margin-bottom 0.5em
|
margin-bottom 0.5em
|
||||||
|
|
||||||
&.small
|
&.small
|
||||||
@@ -1150,150 +1073,6 @@ tt.save
|
|||||||
font-size 0.85em
|
font-size 0.85em
|
||||||
margin-left 2px
|
margin-left 2px
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// NOW RUNNING panel
|
|
||||||
// Replaces the jog grid (left column) while the machine is not idle.
|
|
||||||
// Same outer dimensions as .jog-card so the rest of the layout doesn't
|
|
||||||
// reflow.
|
|
||||||
// =====================================================================
|
|
||||||
.control-page .running-panel
|
|
||||||
flex 1 1 auto
|
|
||||||
min-height 0
|
|
||||||
display flex
|
|
||||||
flex-direction column
|
|
||||||
gap 18px
|
|
||||||
padding 24px
|
|
||||||
border-radius 18px
|
|
||||||
color #fff
|
|
||||||
background linear-gradient(160deg, #0f172a 0%, #1e293b 100%)
|
|
||||||
|
|
||||||
.running-top
|
|
||||||
display flex
|
|
||||||
align-items flex-start
|
|
||||||
justify-content space-between
|
|
||||||
gap 18px
|
|
||||||
|
|
||||||
.running-file
|
|
||||||
font-size 1.6rem
|
|
||||||
font-weight 900
|
|
||||||
line-height 1.1
|
|
||||||
display flex
|
|
||||||
align-items center
|
|
||||||
gap 4px
|
|
||||||
|
|
||||||
.fa
|
|
||||||
color $accent
|
|
||||||
|
|
||||||
.running-meta
|
|
||||||
font-size 0.9rem
|
|
||||||
color #cbd5e1
|
|
||||||
font-weight 600
|
|
||||||
margin-top 6px
|
|
||||||
text-transform capitalize
|
|
||||||
|
|
||||||
.running-pct
|
|
||||||
font-family 'JetBrains Mono', monospace
|
|
||||||
font-size 3.6rem
|
|
||||||
font-weight 900
|
|
||||||
line-height 1
|
|
||||||
color #fff
|
|
||||||
|
|
||||||
span
|
|
||||||
font-size 2rem
|
|
||||||
color $accent
|
|
||||||
font-weight 800
|
|
||||||
margin-left 4px
|
|
||||||
|
|
||||||
.running-progress
|
|
||||||
height 14px
|
|
||||||
background rgba(255, 255, 255, 0.18)
|
|
||||||
border-radius 9999px
|
|
||||||
overflow hidden
|
|
||||||
width 100%
|
|
||||||
|
|
||||||
> div
|
|
||||||
height 100%
|
|
||||||
background $accent
|
|
||||||
transition width 0.4s ease-out
|
|
||||||
|
|
||||||
.running-stats
|
|
||||||
display grid
|
|
||||||
grid-template-columns repeat(4, 1fr)
|
|
||||||
gap 14px
|
|
||||||
|
|
||||||
.running-stat
|
|
||||||
background rgba(255, 255, 255, 0.06)
|
|
||||||
border-radius 14px
|
|
||||||
padding 14px 16px
|
|
||||||
|
|
||||||
.lbl
|
|
||||||
font-size 0.7rem
|
|
||||||
letter-spacing 0.14em
|
|
||||||
text-transform uppercase
|
|
||||||
color #cbd5e1
|
|
||||||
font-weight 700
|
|
||||||
|
|
||||||
.val
|
|
||||||
font-family 'JetBrains Mono', monospace
|
|
||||||
font-weight 900
|
|
||||||
font-size 1.5rem
|
|
||||||
margin-top 4px
|
|
||||||
color #fff
|
|
||||||
|
|
||||||
.running-row
|
|
||||||
display grid
|
|
||||||
grid-template-columns repeat(auto-fit, minmax(140px, 1fr))
|
|
||||||
gap 14px
|
|
||||||
margin-top auto
|
|
||||||
|
|
||||||
.running-row .tx-btn
|
|
||||||
display inline-flex
|
|
||||||
flex-direction column
|
|
||||||
align-items center
|
|
||||||
justify-content center
|
|
||||||
gap 6px
|
|
||||||
height 120px
|
|
||||||
border-radius 18px
|
|
||||||
border none
|
|
||||||
font-weight 800
|
|
||||||
letter-spacing 0.05em
|
|
||||||
cursor pointer
|
|
||||||
background #1e293b
|
|
||||||
color #fff
|
|
||||||
transition transform 0.06s, background 0.15s
|
|
||||||
|
|
||||||
.running-row .tx-btn .fa
|
|
||||||
font-size 2.4rem
|
|
||||||
|
|
||||||
.running-row .tx-btn .lbl
|
|
||||||
font-size 0.85rem
|
|
||||||
opacity 0.9
|
|
||||||
|
|
||||||
.running-row .tx-btn:hover
|
|
||||||
background #334155
|
|
||||||
|
|
||||||
.running-row .tx-btn:active
|
|
||||||
transform translateY(2px)
|
|
||||||
|
|
||||||
.running-row .tx-btn.pause
|
|
||||||
background #f59e0b
|
|
||||||
color #0f172a
|
|
||||||
|
|
||||||
.running-row .tx-btn.pause:hover
|
|
||||||
background #d97706
|
|
||||||
|
|
||||||
.running-row .tx-btn.run
|
|
||||||
background #16a34a
|
|
||||||
|
|
||||||
.running-row .tx-btn.run:hover
|
|
||||||
background #15803d
|
|
||||||
|
|
||||||
.running-row .tx-btn.stop
|
|
||||||
background #dc2626
|
|
||||||
|
|
||||||
.running-row .tx-btn.stop:hover
|
|
||||||
background #b91c1c
|
|
||||||
|
|
||||||
// Step segmented control
|
// Step segmented control
|
||||||
.step-seg
|
.step-seg
|
||||||
display inline-flex
|
display inline-flex
|
||||||
@@ -1333,16 +1112,12 @@ tt.save
|
|||||||
flex-direction column
|
flex-direction column
|
||||||
align-items center
|
align-items center
|
||||||
justify-content center
|
justify-content center
|
||||||
gap 6px
|
gap 4px
|
||||||
user-select none
|
user-select none
|
||||||
-webkit-tap-highlight-color transparent
|
-webkit-tap-highlight-color transparent
|
||||||
cursor pointer
|
cursor pointer
|
||||||
font-weight 700
|
font-weight 700
|
||||||
// Single sizing used by both the 1920x1080 portable touchscreen and
|
font-size 1.05rem
|
||||||
// the Pi 1366x768 kiosk — large enough to be readable at arm's
|
|
||||||
// length on the smaller display, still proportionate on the bigger
|
|
||||||
// one. No mode-specific override.
|
|
||||||
font-size 1.6rem
|
|
||||||
border none
|
border none
|
||||||
background $jog-bg
|
background $jog-bg
|
||||||
color $jog-ink
|
color $jog-ink
|
||||||
@@ -1351,13 +1126,13 @@ tt.save
|
|||||||
min-width 0
|
min-width 0
|
||||||
|
|
||||||
.ico
|
.ico
|
||||||
font-size 2.4rem
|
font-size 1.6rem
|
||||||
|
|
||||||
.lbl
|
.lbl
|
||||||
font-size 1.5rem
|
font-size 0.8rem
|
||||||
color inherit
|
color inherit
|
||||||
opacity 0.95
|
opacity 0.85
|
||||||
font-weight 700
|
font-weight 600
|
||||||
|
|
||||||
&:hover:not([disabled])
|
&:hover:not([disabled])
|
||||||
background $jog-hover
|
background $jog-hover
|
||||||
@@ -1396,7 +1171,7 @@ tt.save
|
|||||||
|
|
||||||
.control-page .dro-head, .control-page .dro-row
|
.control-page .dro-head, .control-page .dro-row
|
||||||
display grid
|
display grid
|
||||||
grid-template-columns 84px 1.4fr 1fr 1fr 280px
|
grid-template-columns 84px 1.4fr 1fr 1fr 170px 170px 280px
|
||||||
column-gap 0.75rem
|
column-gap 0.75rem
|
||||||
align-items center
|
align-items center
|
||||||
padding 14px 22px
|
padding 14px 22px
|
||||||
@@ -1410,15 +1185,6 @@ tt.save
|
|||||||
letter-spacing 0.1em
|
letter-spacing 0.1em
|
||||||
color $muted-2
|
color $muted-2
|
||||||
|
|
||||||
// Master Home All sits in the header's Actions cell. Make it
|
|
||||||
// visually subordinate to the per-axis home buttons in the rows
|
|
||||||
// below — same family, smaller scale.
|
|
||||||
.icon-btn
|
|
||||||
width 44px
|
|
||||||
height 44px
|
|
||||||
font-size 0.95rem
|
|
||||||
border-radius 9px
|
|
||||||
|
|
||||||
.control-page .dro-row
|
.control-page .dro-row
|
||||||
border-bottom 1px solid $line-soft
|
border-bottom 1px solid $line-soft
|
||||||
flex 1
|
flex 1
|
||||||
@@ -1457,6 +1223,9 @@ tt.save
|
|||||||
.dro-axis.axis-c
|
.dro-axis.axis-c
|
||||||
color #d946ef
|
color #d946ef
|
||||||
|
|
||||||
|
.dro-axis.axis-w
|
||||||
|
color #7c3aed
|
||||||
|
|
||||||
.dro-pos
|
.dro-pos
|
||||||
font-family 'JetBrains Mono', monospace
|
font-family 'JetBrains Mono', monospace
|
||||||
font-size 36px
|
font-size 36px
|
||||||
@@ -1526,39 +1295,6 @@ tt.save
|
|||||||
opacity 0.45
|
opacity 0.45
|
||||||
cursor not-allowed
|
cursor not-allowed
|
||||||
|
|
||||||
// State-tinted variants used on home + zero buttons in DRO rows
|
|
||||||
// to communicate per-axis homing / toolpath fit at a glance,
|
|
||||||
// replacing the explicit HOMED / OK chips that used to live in
|
|
||||||
// their own columns.
|
|
||||||
&.state-green
|
|
||||||
background #dcfce7
|
|
||||||
border-color #86efac
|
|
||||||
color #166534
|
|
||||||
|
|
||||||
&:hover:not([disabled])
|
|
||||||
background #bbf7d0
|
|
||||||
|
|
||||||
&.state-amber
|
|
||||||
background #fef3c7
|
|
||||||
border-color #fcd34d
|
|
||||||
color #92400e
|
|
||||||
|
|
||||||
&:hover:not([disabled])
|
|
||||||
background #fde68a
|
|
||||||
|
|
||||||
&.state-red
|
|
||||||
background #fee2e2
|
|
||||||
border-color #fca5a5
|
|
||||||
color #991b1b
|
|
||||||
|
|
||||||
&:hover:not([disabled])
|
|
||||||
background #fecaca
|
|
||||||
|
|
||||||
&[disabled].state-green,
|
|
||||||
&[disabled].state-amber,
|
|
||||||
&[disabled].state-red
|
|
||||||
opacity 0.7
|
|
||||||
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
display flex
|
display flex
|
||||||
justify-content flex-end
|
justify-content flex-end
|
||||||
@@ -1891,15 +1627,6 @@ tt.save
|
|||||||
min-height 0
|
min-height 0
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
|
||||||
// On the Pi's onboard kiosk browser the 3D toolpath preview is
|
|
||||||
// suppressed (Pi 3B's VideoCore IV can't run three.js fast
|
|
||||||
// enough), so the gcode listing claims the full width.
|
|
||||||
&.no-preview
|
|
||||||
grid-template-columns 1fr
|
|
||||||
|
|
||||||
> .gcode
|
|
||||||
border-right none
|
|
||||||
|
|
||||||
> .gcode
|
> .gcode
|
||||||
border-right 1px solid $line-soft
|
border-right 1px solid $line-soft
|
||||||
background #fafafa
|
background #fafafa
|
||||||
@@ -1916,28 +1643,22 @@ tt.save
|
|||||||
> .path-viewer
|
> .path-viewer
|
||||||
overflow hidden
|
overflow hidden
|
||||||
min-height 0
|
min-height 0
|
||||||
height 100%
|
|
||||||
background #222 // matches the WebGL clear colour so any
|
|
||||||
// first-frame mismatch reads as the same dark
|
|
||||||
// panel rather than a flash.
|
|
||||||
display flex
|
display flex
|
||||||
flex-direction column
|
flex-direction column
|
||||||
|
|
||||||
.path-viewer-content
|
.path-viewer-content
|
||||||
flex 1 1 auto
|
flex 1 1 auto
|
||||||
width 100% !important
|
width 100% !important
|
||||||
height 100% !important
|
height auto !important
|
||||||
min-height 0
|
min-height 0
|
||||||
float none !important
|
float none !important
|
||||||
margin 0 !important
|
margin 0 !important
|
||||||
background #222
|
|
||||||
|
|
||||||
&.small .path-viewer-content
|
&.small .path-viewer-content
|
||||||
width 100% !important
|
width 100% !important
|
||||||
height 100% !important
|
height auto !important
|
||||||
float none !important
|
float none !important
|
||||||
margin 0 !important
|
margin 0 !important
|
||||||
background #222
|
|
||||||
|
|
||||||
.progress-bar
|
.progress-bar
|
||||||
height 28px
|
height 28px
|
||||||
@@ -2333,321 +2054,3 @@ tt.save
|
|||||||
|
|
||||||
h1, h2, h3
|
h1, h2, h3
|
||||||
margin-top 0
|
margin-top 0
|
||||||
|
|
||||||
.settings-loading
|
|
||||||
color $muted
|
|
||||||
font-style italic
|
|
||||||
padding 24px
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// KIOSK MODE — compact layout for the controller's own onboard browser
|
|
||||||
// (Pi 3B at 1366x768). Activated by `html.kiosk-mode` (auto-applied
|
|
||||||
// when location.hostname is localhost). All overrides target the V09
|
|
||||||
// shell so the desktop / portable touchscreen layout is unaffected.
|
|
||||||
// =====================================================================
|
|
||||||
html.kiosk-mode
|
|
||||||
font-size 13px
|
|
||||||
|
|
||||||
.app-head
|
|
||||||
flex 0 0 56px
|
|
||||||
height 56px
|
|
||||||
padding 0 12px
|
|
||||||
gap 10px
|
|
||||||
|
|
||||||
.brand-blk
|
|
||||||
display none
|
|
||||||
|
|
||||||
.estop
|
|
||||||
transform scale(0.6)
|
|
||||||
transform-origin right center
|
|
||||||
|
|
||||||
.tabs-host
|
|
||||||
height 56px
|
|
||||||
padding-left 0
|
|
||||||
|
|
||||||
.ktab
|
|
||||||
height 56px
|
|
||||||
padding 0 14px
|
|
||||||
font-size 14px
|
|
||||||
gap 6px
|
|
||||||
|
|
||||||
.fa
|
|
||||||
font-size 16px
|
|
||||||
|
|
||||||
.ktab .ktab-underline,
|
|
||||||
.ktab.active::after
|
|
||||||
bottom 0
|
|
||||||
|
|
||||||
.app-body
|
|
||||||
padding 8px
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
// Control page: tighten everything
|
|
||||||
.control-page
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
// Keep two columns at 1366x768 — vertical space is the constraint.
|
|
||||||
// Shrink the jog column from 720px to 540px so the DRO has more
|
|
||||||
// breathing room.
|
|
||||||
.control-page .control-grid
|
|
||||||
grid-template-columns 540px 1fr
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
.control-page .right-col
|
|
||||||
grid-template-rows 1fr 110px
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
.control-page .jog-card
|
|
||||||
padding 10px
|
|
||||||
|
|
||||||
.control-page .jog-head
|
|
||||||
margin-bottom 8px
|
|
||||||
|
|
||||||
.control-page .jog-title
|
|
||||||
font-size 14px
|
|
||||||
|
|
||||||
.control-page .jog-grid
|
|
||||||
gap 6px
|
|
||||||
|
|
||||||
.control-page .dro-head, .control-page .dro-row
|
|
||||||
grid-template-columns 56px 1fr 0.85fr 0.85fr 1fr
|
|
||||||
column-gap 0.4rem
|
|
||||||
padding 6px 10px
|
|
||||||
|
|
||||||
.control-page .dro-head
|
|
||||||
font-size 0.65rem
|
|
||||||
|
|
||||||
.control-page .dro-row
|
|
||||||
font-size 0.95rem
|
|
||||||
|
|
||||||
// Axis-action buttons in DRO rows (settings/zero/home).
|
|
||||||
.control-page .dro-row .icon-btn
|
|
||||||
width 56px
|
|
||||||
height 56px
|
|
||||||
font-size 1.25rem
|
|
||||||
border-radius 11px
|
|
||||||
|
|
||||||
.control-page .status-strip
|
|
||||||
grid-template-columns repeat(2, 1fr)
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
.control-page .stat-card
|
|
||||||
padding 8px 12px
|
|
||||||
|
|
||||||
.stat-label
|
|
||||||
font-size 9px
|
|
||||||
|
|
||||||
.stat-val
|
|
||||||
font-size 18px
|
|
||||||
margin-top 2px
|
|
||||||
|
|
||||||
.stat-sub
|
|
||||||
font-size 11px
|
|
||||||
margin-top 0
|
|
||||||
|
|
||||||
// Macros: 8 -> 4 column grid; shorter buttons.
|
|
||||||
.control-page .macro-row
|
|
||||||
grid-template-columns repeat(4, 1fr)
|
|
||||||
gap 6px
|
|
||||||
|
|
||||||
.macro-btn
|
|
||||||
height 56px
|
|
||||||
font-size 0.85rem
|
|
||||||
border-radius 10px
|
|
||||||
|
|
||||||
// Now-running panel: tighten paddings, smaller percent text.
|
|
||||||
.control-page .running-panel
|
|
||||||
padding 12px
|
|
||||||
gap 10px
|
|
||||||
|
|
||||||
.running-file
|
|
||||||
font-size 1.15rem
|
|
||||||
|
|
||||||
.running-pct
|
|
||||||
font-size 2.2rem
|
|
||||||
|
|
||||||
span
|
|
||||||
font-size 1.2rem
|
|
||||||
|
|
||||||
.running-stats
|
|
||||||
grid-template-columns repeat(2, 1fr)
|
|
||||||
gap 6px
|
|
||||||
|
|
||||||
.running-stat
|
|
||||||
padding 8px 10px
|
|
||||||
|
|
||||||
.val
|
|
||||||
font-size 1rem
|
|
||||||
|
|
||||||
// Program page: gcode listing fills (path-viewer is hidden via JS).
|
|
||||||
.program-page
|
|
||||||
gap 8px
|
|
||||||
|
|
||||||
// Settings shell: tighter rail.
|
|
||||||
.settings-shell .rail
|
|
||||||
padding 8px
|
|
||||||
|
|
||||||
.settings-shell .rail .rail-item
|
|
||||||
padding 8px 10px
|
|
||||||
font-size 0.9rem
|
|
||||||
|
|
||||||
.settings-content
|
|
||||||
padding 10px
|
|
||||||
|
|
||||||
// System pill / sidebar headers smaller.
|
|
||||||
.system-pill, .sidebar-pill
|
|
||||||
font-size 0.8rem
|
|
||||||
|
|
||||||
// Inside system-pill, the icon + text need explicit spacing.
|
|
||||||
.system-pill > * + *,
|
|
||||||
.sidebar-pill > * + *
|
|
||||||
margin-left 6px
|
|
||||||
|
|
||||||
// Modal dialogs scaled down for the smaller viewport.
|
|
||||||
.modal-bg
|
|
||||||
font-size 13px
|
|
||||||
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// LEGACY-CHROMIUM FLEX-GAP FALLBACK
|
|
||||||
// =====================================================================
|
|
||||||
// Chromium 72 (which ships on the controller's Pi 3B) does not support
|
|
||||||
// `gap` on flexbox containers — it landed in Chrome 84 (2020-05). Grid
|
|
||||||
// `gap` IS supported (Chrome 57+) so anything `display: grid` is fine.
|
|
||||||
//
|
|
||||||
// We can't use @supports not (gap: 1px) because Chromium 72 reports
|
|
||||||
// it supports `gap` (the spec considers it a grid-only property in
|
|
||||||
// older snapshots). Instead, we add explicit margin-based spacing for
|
|
||||||
// every known flex container in the V09 shell that visibly breaks on
|
|
||||||
// the Pi kiosk. The modern CSS gap rule still applies in newer Chrome
|
|
||||||
// — these rules are inert (margin-left:auto rules elsewhere keep
|
|
||||||
// their meaning) because the gap pushes children apart anyway.
|
|
||||||
|
|
||||||
// App header — brand block, tabs, system pill, estop
|
|
||||||
.app-head > * + *
|
|
||||||
margin-left 18px
|
|
||||||
|
|
||||||
.app-head .brand-blk > * + *
|
|
||||||
margin-left 14px
|
|
||||||
|
|
||||||
// Tabs ribbon — the tab button itself uses flex+gap to space its
|
|
||||||
// icon and label. Without flex-gap support those collapse.
|
|
||||||
.ktab > * + *
|
|
||||||
margin-left 0.55rem
|
|
||||||
|
|
||||||
// Header system pill (sys-btn) and machine state badge
|
|
||||||
.sys-btn > * + *
|
|
||||||
margin-left 0.55rem
|
|
||||||
.state-badge > * + *
|
|
||||||
margin-left 0.6rem
|
|
||||||
|
|
||||||
// Jog card title row
|
|
||||||
.control-page .jog-head > * + *
|
|
||||||
margin-left 12px
|
|
||||||
|
|
||||||
// Now-running panel — top, file/meta, stats, row, transport
|
|
||||||
.control-page .running-panel > * + *
|
|
||||||
margin-top 18px
|
|
||||||
.running-top > * + *
|
|
||||||
margin-left 18px
|
|
||||||
.running-row > * + *
|
|
||||||
margin-left 14px
|
|
||||||
.transport-row > * + *
|
|
||||||
margin-left 14px
|
|
||||||
|
|
||||||
// Console card tabs
|
|
||||||
.console-card .ptab-bar > * + *
|
|
||||||
margin-left 6px
|
|
||||||
|
|
||||||
// Settings shell rail and content
|
|
||||||
.settings-shell > * + *
|
|
||||||
margin-left 18px
|
|
||||||
|
|
||||||
// Macro buttons (.macro-btn icon + label)
|
|
||||||
.macro-btn > * + *
|
|
||||||
margin-left 0.6rem
|
|
||||||
|
|
||||||
// Jog buttons (.jbtn ico + lbl) — column flex, vertical gap
|
|
||||||
.jbtn > * + *
|
|
||||||
margin-top 4px
|
|
||||||
|
|
||||||
// DRO actions cell already uses gap; emulate via margin
|
|
||||||
.actions-cell > * + *
|
|
||||||
margin-left 10px
|
|
||||||
|
|
||||||
// Generic ".header"-style flex rows in older subpages
|
|
||||||
.app-body .pure-form > * + *
|
|
||||||
margin-top 4px
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// KIOSK-MODE-SPECIFIC LEGACY FALLBACKS
|
|
||||||
// =====================================================================
|
|
||||||
html.kiosk-mode
|
|
||||||
.app-head > * + *
|
|
||||||
margin-left 10px
|
|
||||||
|
|
||||||
.control-page
|
|
||||||
> * + *
|
|
||||||
margin-top 8px
|
|
||||||
|
|
||||||
.control-page .control-grid
|
|
||||||
// grid-gap works on Chromium 72 so nothing here.
|
|
||||||
|
|
||||||
.control-page .right-col
|
|
||||||
// grid-gap works.
|
|
||||||
grid-template-rows 1fr 158px // tighter than the desktop 158px
|
|
||||||
|
|
||||||
.running-row > * + *
|
|
||||||
margin-left 6px
|
|
||||||
|
|
||||||
.control-page .jog-head > * + *
|
|
||||||
margin-left 8px
|
|
||||||
|
|
||||||
|
|
||||||
// Settings rail must be scrollable in kiosk mode \u2014 the 14+
|
|
||||||
// item list overflows the 768px viewport at default heights.
|
|
||||||
.settings-shell
|
|
||||||
grid-template-columns 220px 1fr
|
|
||||||
gap 10px
|
|
||||||
|
|
||||||
.settings-rail
|
|
||||||
position static
|
|
||||||
align-self stretch
|
|
||||||
max-height 100%
|
|
||||||
overflow-y auto
|
|
||||||
|
|
||||||
.settings-rail .set-item
|
|
||||||
height 36px
|
|
||||||
font-size 0.85rem
|
|
||||||
padding 0 10px
|
|
||||||
|
|
||||||
.fa
|
|
||||||
width 14px
|
|
||||||
font-size 0.9rem
|
|
||||||
|
|
||||||
.settings-rail .set-section
|
|
||||||
margin 6px 4px 2px
|
|
||||||
font-size 0.62rem
|
|
||||||
|
|
||||||
.settings-rail .set-rail-foot
|
|
||||||
margin-top 4px
|
|
||||||
padding-top 6px
|
|
||||||
|
|
||||||
.sp-shutdown, .sp-save
|
|
||||||
height 32px
|
|
||||||
font-size 0.85rem
|
|
||||||
|
|
||||||
|
|
||||||
// Program tab flex-gap fallbacks for Chromium 72.
|
|
||||||
// Action bar (RUN/STOP/UPLOAD/.../DELETE) and the action buttons
|
|
||||||
// themselves (icon stacked over label).
|
|
||||||
.action-bar > * + *
|
|
||||||
margin-left 12px
|
|
||||||
.action-btn > * + *
|
|
||||||
margin-top 4px
|
|
||||||
|
|
||||||
// File bar (Create Folder / folder select / file select / sort).
|
|
||||||
.file-bar > * + *
|
|
||||||
margin-left 10px
|
|
||||||
.file-btn > * + *
|
|
||||||
margin-left 0.4rem
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import configTemplate from "../../../resources/config-template.json";
|
import configTemplate from "../../../resources/config-template.json";
|
||||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||||
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
|
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
|
||||||
|
import WAxisSettings from "./WAxisSettings.svelte";
|
||||||
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
|
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
|
||||||
import Button, { Label } from "@smui/button";
|
import Button, { Label } from "@smui/button";
|
||||||
|
|
||||||
@@ -18,8 +19,8 @@
|
|||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
|
|
||||||
<div class="pure-form pure-form-aligned">
|
<div class="pure-form pure-form-aligned">
|
||||||
<h2 id="sec-display" data-sec="display">User Interface</h2>
|
<h2>User Interface</h2>
|
||||||
<fieldset data-sec="display">
|
<fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="screen-rotation" />
|
<label for="screen-rotation" />
|
||||||
<Button
|
<Button
|
||||||
@@ -45,8 +46,8 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2 id="sec-units" data-sec="display">Units</h2>
|
<h2>Units</h2>
|
||||||
<fieldset data-sec="display">
|
<fieldset>
|
||||||
<ConfigTemplatedInput key={`settings.units`} />
|
<ConfigTemplatedInput key={`settings.units`} />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
Note, units sets both the machine default units and the units used in motor configuration. GCode program-start,
|
Note, units sets both the machine default units and the units used in motor configuration. GCode program-start,
|
||||||
@@ -54,13 +55,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2 id="sec-easy-adapter" data-sec="display">Easy Adapter</h2>
|
<h2>Easy Adapter</h2>
|
||||||
<fieldset data-sec="display">
|
<fieldset>
|
||||||
<ConfigTemplatedInput key={`settings.easy-adapter`} />
|
<ConfigTemplatedInput key={`settings.easy-adapter`} />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2 id="sec-probing" data-sec="probing">Probing</h2>
|
<h2>Probing</h2>
|
||||||
<fieldset data-sec="probing">
|
<fieldset>
|
||||||
<ConfigTemplatedInput key={`settings.probing-prompts`} />
|
<ConfigTemplatedInput key={`settings.probing-prompts`} />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
Onefinity highly recommends that you keep the safety prompts
|
Onefinity highly recommends that you keep the safety prompts
|
||||||
@@ -87,15 +88,20 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset data-sec="gcode">
|
<fieldset>
|
||||||
<h2 id="sec-gcode" data-sec="gcode">GCode</h2>
|
<h2>GCode</h2>
|
||||||
{#each Object.keys(configTemplate.gcode) as key}
|
{#each Object.keys(configTemplate.gcode) as key}
|
||||||
<ConfigTemplatedInput key={`gcode.${key}`} />
|
<ConfigTemplatedInput key={`gcode.${key}`} />
|
||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2 id="sec-path-accuracy" data-sec="gcode">Path Accuracy</h2>
|
<h2 id="w-axis">W Axis (auxcnc)</h2>
|
||||||
<fieldset data-sec="gcode">
|
<fieldset>
|
||||||
|
<WAxisSettings />
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h2>Path Accuracy</h2>
|
||||||
|
<fieldset>
|
||||||
<ConfigTemplatedInput key={`settings.max-deviation`} />
|
<ConfigTemplatedInput key={`settings.max-deviation`} />
|
||||||
|
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
@@ -118,8 +124,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h2 id="sec-cornering" data-sec="gcode">Cornering Speed (Advanced)</h2>
|
<h2>Cornering Speed (Advanced)</h2>
|
||||||
<fieldset data-sec="gcode">
|
<fieldset>
|
||||||
<ConfigTemplatedInput key={`settings.junction-accel`} />
|
<ConfigTemplatedInput key={`settings.junction-accel`} />
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
Junction acceleration limits the cornering speed the planner
|
Junction acceleration limits the cornering speed the planner
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
>
|
>
|
||||||
<div slot="trailingIcon">
|
<div slot="trailingIcon">
|
||||||
{#if valid}
|
{#if valid}
|
||||||
<Icon class="fa fa-circle-check" style="color: green;" />
|
<Icon class="fa fa-check-circle-o" style="color: green;" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<HelperText persistent slot="helper">{helperText}</HelperText>
|
<HelperText persistent slot="helper">{helperText}</HelperText>
|
||||||
|
|||||||
262
src/svelte-components/src/components/WAxisSettings.svelte
Normal file
262
src/svelte-components/src/components/WAxisSettings.svelte
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Button, { Label } from "@smui/button";
|
||||||
|
import * as api from "$lib/api";
|
||||||
|
|
||||||
|
// Mirrors the DEFAULTS in src/py/bbctrl/AuxAxis.py. The "enabled"
|
||||||
|
// flag is read-only here; toggling the W axis on/off is done via
|
||||||
|
// aux.json on disk, so adding/removing the hardware doesn't have a
|
||||||
|
// surprise UI that bricks bring-up.
|
||||||
|
type AuxConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
port: string;
|
||||||
|
baud: number;
|
||||||
|
steps_per_mm: number;
|
||||||
|
dir_sign: number;
|
||||||
|
min_w: number;
|
||||||
|
max_w: number;
|
||||||
|
max_feed_mm_min: number;
|
||||||
|
home_dir: string;
|
||||||
|
home_position_mm: number;
|
||||||
|
home_fast_sps: number;
|
||||||
|
home_slow_sps: number;
|
||||||
|
home_backoff_steps: number;
|
||||||
|
home_maxtravel_steps: number;
|
||||||
|
step_max_sps: number;
|
||||||
|
step_accel_sps2: number;
|
||||||
|
step_start_sps: number;
|
||||||
|
limit_low: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg: AuxConfig | null = null;
|
||||||
|
let status: { enabled: boolean; present: boolean; homed: boolean; pos_mm: number } | null = null;
|
||||||
|
let busy = false;
|
||||||
|
let saveMessage = "";
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
cfg = await api.GET("aux/config");
|
||||||
|
status = await api.GET("aux/status");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load aux config/status:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!cfg) return;
|
||||||
|
busy = true;
|
||||||
|
saveMessage = "";
|
||||||
|
try {
|
||||||
|
await api.PUT("aux/config/save", cfg);
|
||||||
|
saveMessage = "Saved.";
|
||||||
|
await refresh();
|
||||||
|
setTimeout(() => (saveMessage = ""), 3000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save aux config:", e);
|
||||||
|
saveMessage = "Save failed - see console.";
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-axis-settings">
|
||||||
|
{#if !cfg}
|
||||||
|
<p class="tip">Loading W axis configuration...</p>
|
||||||
|
{:else}
|
||||||
|
<div class="status">
|
||||||
|
{#if status}
|
||||||
|
<span>
|
||||||
|
Status:
|
||||||
|
{#if !status.enabled}
|
||||||
|
disabled
|
||||||
|
{:else if !status.present}
|
||||||
|
offline
|
||||||
|
{:else if status.homed}
|
||||||
|
homed at {status.pos_mm.toFixed(3)} mm
|
||||||
|
{:else}
|
||||||
|
connected, unhomed
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-form pure-form-aligned">
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group" title="Enable the auxcnc W axis. Edit aux.json to toggle.">
|
||||||
|
<label for="enabled">enabled</label>
|
||||||
|
<input id="enabled" type="checkbox" checked={cfg.enabled} disabled />
|
||||||
|
<label for="" class="units">(edit aux.json)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Serial port for the auxcnc ESP32.">
|
||||||
|
<label for="port">serial port</label>
|
||||||
|
<input id="port" type="text" bind:value={cfg.port} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Serial baud rate.">
|
||||||
|
<label for="baud">baud</label>
|
||||||
|
<input id="baud" type="number" bind:value={cfg.baud} min={1200} step={1} />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Mechanics</h3>
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group" title="Logical steps per mm of W travel.">
|
||||||
|
<label for="steps_per_mm">steps per mm</label>
|
||||||
|
<input id="steps_per_mm" type="number" bind:value={cfg.steps_per_mm} step="any" />
|
||||||
|
<label for="" class="units">steps/mm</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Direction sign: +1 or -1. Flip if W+ moves the wrong way.">
|
||||||
|
<label for="dir_sign">direction sign</label>
|
||||||
|
<select id="dir_sign" bind:value={cfg.dir_sign}>
|
||||||
|
<option value={1}>+1</option>
|
||||||
|
<option value={-1}>-1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Soft-limit minimum W in mm.">
|
||||||
|
<label for="min_w">soft min</label>
|
||||||
|
<input id="min_w" type="number" bind:value={cfg.min_w} step="any" />
|
||||||
|
<label for="" class="units">mm</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Soft-limit maximum W in mm.">
|
||||||
|
<label for="max_w">soft max</label>
|
||||||
|
<input id="max_w" type="number" bind:value={cfg.max_w} step="any" />
|
||||||
|
<label for="" class="units">mm</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Informational max feed; rate caps live on the ESP.">
|
||||||
|
<label for="max_feed_mm_min">max feed</label>
|
||||||
|
<input id="max_feed_mm_min" type="number" bind:value={cfg.max_feed_mm_min} step="any" />
|
||||||
|
<label for="" class="units">mm/min</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Homing</h3>
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group" title="Direction the axis moves when looking for the home limit switch.">
|
||||||
|
<label for="home_dir">home direction</label>
|
||||||
|
<select id="home_dir" bind:value={cfg.home_dir}>
|
||||||
|
<option value="-">- (toward W-)</option>
|
||||||
|
<option value="+">+ (toward W+)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="W position assigned when homing completes.">
|
||||||
|
<label for="home_position_mm">home position</label>
|
||||||
|
<input id="home_position_mm" type="number" bind:value={cfg.home_position_mm} step="any" />
|
||||||
|
<label for="" class="units">mm</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Fast seek rate during homing search.">
|
||||||
|
<label for="home_fast_sps">fast seek</label>
|
||||||
|
<input id="home_fast_sps" type="number" bind:value={cfg.home_fast_sps} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps/s</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Slow seek rate during homing latch.">
|
||||||
|
<label for="home_slow_sps">slow seek</label>
|
||||||
|
<input id="home_slow_sps" type="number" bind:value={cfg.home_slow_sps} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps/s</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Backoff after the limit triggers, before the slow seek.">
|
||||||
|
<label for="home_backoff_steps">backoff</label>
|
||||||
|
<input id="home_backoff_steps" type="number" bind:value={cfg.home_backoff_steps} step={1} min={0} />
|
||||||
|
<label for="" class="units">steps</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Maximum travel before homing aborts as a runaway.">
|
||||||
|
<label for="home_maxtravel_steps">max travel</label>
|
||||||
|
<input id="home_maxtravel_steps" type="number" bind:value={cfg.home_maxtravel_steps} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Limit switch active-low? Off = active-high.">
|
||||||
|
<label for="limit_low">limit active low</label>
|
||||||
|
<input id="limit_low" type="checkbox" bind:checked={cfg.limit_low} />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Step Profile</h3>
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group" title="Maximum step rate during normal moves.">
|
||||||
|
<label for="step_max_sps">max rate</label>
|
||||||
|
<input id="step_max_sps" type="number" bind:value={cfg.step_max_sps} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps/s</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Acceleration in steps per second squared.">
|
||||||
|
<label for="step_accel_sps2">acceleration</label>
|
||||||
|
<input id="step_accel_sps2" type="number" bind:value={cfg.step_accel_sps2} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps/s²</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Initial step rate at the start of a move.">
|
||||||
|
<label for="step_start_sps">start rate</label>
|
||||||
|
<input id="step_start_sps" type="number" bind:value={cfg.step_start_sps} step={1} min={1} />
|
||||||
|
<label for="" class="units">steps/s</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<Button
|
||||||
|
touch
|
||||||
|
variant="raised"
|
||||||
|
on:click={save}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
<Label>Save W Axis Settings</Label>
|
||||||
|
</Button>
|
||||||
|
{#if saveMessage}
|
||||||
|
<span class="save-msg">{saveMessage}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tip">
|
||||||
|
Changes are written to aux.json. Homing rates and the
|
||||||
|
limit polarity are pushed to the ESP immediately; any
|
||||||
|
running motion is unaffected. Re-home the W axis after
|
||||||
|
changing direction, sign, or step settings.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.w-axis-settings {
|
||||||
|
.status {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
font-size: 90%;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-left: 210px;
|
||||||
|
margin-top: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-msg {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-left: 210px;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 90%;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user