Compare commits
64 Commits
private-mo
...
4470fcee0a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4470fcee0a | |||
| 39e308d9ae | |||
| 8e6e72a8b9 | |||
| 226b44053c | |||
| 0493a4ddc7 | |||
| c4c20c6d0a | |||
| 4a494a101d | |||
| ca7e30aa05 | |||
| 983e06b53d | |||
| 53b65dc30e | |||
| f545438fa8 | |||
| 3b622d3d17 | |||
| aa747dcc85 | |||
| 56c3406f25 | |||
| 7cdab010b3 | |||
| 53f26b0be8 | |||
| 3614a2bcd4 | |||
| 06f0e6517e | |||
| 1a6f926181 | |||
| b0712a5bf0 | |||
| 1c69c0a157 | |||
|
|
748f092795 | ||
|
|
cfc14643d2 | ||
|
|
b0f38619ba | ||
| 549b69c234 | |||
|
|
5376d23f8b | ||
|
|
ecf3191fcc | ||
|
|
3baa67360c | ||
|
|
b7bd7a1c9c | ||
|
|
6fe2e79bff | ||
|
|
19e6cc6c93 | ||
|
|
50839718e2 | ||
| 68a92bb297 | |||
|
|
41d720c1d0 | ||
| 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
|
||||
===========================================
|
||||
|
||||
## 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
|
||||
- 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)
|
||||
|
||||
22
README.md
22
README.md
@@ -1,9 +1,8 @@
|
||||
# OneFinity CNC Controller Firmware (A-axis fork)
|
||||
# OneFinity CNC Controller Firmware (W-axis fork)
|
||||
|
||||
This is the community-fork firmware (V09 UI, FA6, cold-boot work,
|
||||
macOS dev tooling) with a virtual A axis driven by an auxcnc ESP32
|
||||
over USB serial. See [docs/AUX_A_AXIS.md](docs/AUX_A_AXIS.md) for the
|
||||
design and config.
|
||||
This is the OneFinity / Buildbotics bbctrl firmware with a virtual W
|
||||
axis driven by an auxcnc ESP32 over USB serial. See
|
||||
[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for the design and config.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -17,7 +16,7 @@ src/svelte-components/ Newer Svelte UI for dialogs and settings
|
||||
src/pug/ Pug templates compiled into build/http/index.html
|
||||
src/resources/ Static assets and config templates
|
||||
scripts/ Install / update / RPi build helpers
|
||||
docs/ Architecture, dev setup, A-axis docs
|
||||
docs/ Architecture, dev setup, W-axis docs
|
||||
```
|
||||
|
||||
## Build & flash (quick path, macOS or Linux)
|
||||
@@ -101,8 +100,7 @@ bbctrl restarts, then the new UI).
|
||||
|
||||
```bash
|
||||
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 A axis is enabled
|
||||
curl -s http://onefinity.local/api/aux/status # if W axis is enabled
|
||||
```
|
||||
|
||||
## Build & flash (full path, Debian/Linux)
|
||||
@@ -111,12 +109,12 @@ For AVR + GPlan rebuilds, see [docs/development.md](docs/development.md).
|
||||
That path uses qemu + chroot to cross-compile gplan for ARM and needs
|
||||
the `gcc-avr` / `avr-libc` toolchain.
|
||||
|
||||
## A axis (auxcnc)
|
||||
## W axis (auxcnc)
|
||||
|
||||
This fork adds a virtual A axis. See
|
||||
[docs/AUX_A_AXIS.md](docs/AUX_A_AXIS.md) for:
|
||||
This fork adds a virtual W axis. See
|
||||
[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for:
|
||||
|
||||
- G-code surface (`G28 A0`, `G1 A25`, etc.)
|
||||
- G-code surface (`G28 W0`, `G1 W25`, etc.)
|
||||
- The G-code preprocessor and hook architecture
|
||||
- aux.json keys
|
||||
- REST API (`/api/aux/*`)
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
# A axis (auxcnc) integration
|
||||
# W axis (auxcnc) integration
|
||||
|
||||
> **Note:** This document describes the original out-of-band W-axis
|
||||
> architecture (gcode preprocessor rewriting W tokens into HOOK
|
||||
> messages dispatched between blocks). The current implementation
|
||||
> integrates the auxcnc-driven stepper as a *virtual A axis* through
|
||||
> gplan via a synthetic motor (`bbctrl/ExternalAxis.py`), so A is
|
||||
> blended with XYZ in the same S-curve plan and the gcode surface
|
||||
> below applies as plain `A` words.
|
||||
>
|
||||
> The HOOK pipeline still exists for ATC pneumatic atoms (M100 EJECT,
|
||||
> M102 RELEASE, M103 CLAMP) - see `bbctrl/AuxPreprocessor.py`. Macros
|
||||
> compose drop/grab tool sequences from those atoms.
|
||||
|
||||
This adds a virtual `A` axis to the bbctrl controller, driven by the
|
||||
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
|
||||
@@ -22,15 +10,15 @@ a small REST API for jogging / homing from the UI.
|
||||
|
||||
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: A moves run
|
||||
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 `A` words.
|
||||
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 A word continues to drive XYZ.
|
||||
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.
|
||||
@@ -38,25 +26,25 @@ Pipeline:
|
||||
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 A finishes.
|
||||
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 `A` words are rewritten the same way at the
|
||||
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 A0 ; home A axis
|
||||
G1 A25 F300 ; move A to 25 mm absolute
|
||||
G1 X100 W12.5 ; mixed: A moves first, then XYZ (configurable)
|
||||
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 A-2.5 ; relative A move
|
||||
G1 W-2.5 ; relative W move
|
||||
G90
|
||||
G92 A0 ; set current A as zero (G92-style)
|
||||
G92 W0 ; set current W as zero (G92-style)
|
||||
```
|
||||
|
||||
Rules:
|
||||
@@ -117,18 +105,18 @@ limit switch when the axis isn't homed yet).
|
||||
**Control view**
|
||||
|
||||
- A jog row appears under the XYZ jog grid when `aux_enabled` is true,
|
||||
with three buttons: `A-`, `A+`, and a wide `Home W`. There is
|
||||
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 A axis row with position, status (OFFLINE /
|
||||
- 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 A axis on/off requires
|
||||
`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
|
||||
@@ -147,19 +135,19 @@ These are pushed via `state.set` and visible in the websocket stream:
|
||||
## Edge cases
|
||||
|
||||
- **ESP reboots mid-job**: `[boot] auxcnc v=N` banner -> `aux_homed`
|
||||
cleared, message added: "A axis controller restarted - re-home
|
||||
before use". Subsequent A moves still run; if you want a hard fail
|
||||
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 A move completes. For an immediate stop hit
|
||||
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 A moves are
|
||||
- **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.
|
||||
|
||||
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.
|
||||
@@ -9,8 +9,8 @@
|
||||
# * The full V09 chrome (header tabs, settings rail, jog grid, DRO
|
||||
# skeleton, status strip).
|
||||
# * A "DISCONNECTED" overlay because there's no controller backend.
|
||||
# * The A axis row in jog/DRO is hidden (correct: it appears only when
|
||||
# the controller reports `aux_enabled = true`). To exercise the A
|
||||
# * 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
|
||||
|
||||
@@ -232,17 +232,6 @@ module.exports = {
|
||||
const toolpath = await api.get(`path/${file}`);
|
||||
this.toolpath_progress = toolpath.progress;
|
||||
|
||||
// Planner failure (e.g. AuxPreprocessor Z-A coupling
|
||||
// rejection). Close the dialog and surface the message
|
||||
// instead of polling the same broken plan forever.
|
||||
if (toolpath.error) {
|
||||
this.showGcodeMessage = false;
|
||||
this.toolpath_progress = 0;
|
||||
console.error("Plan failed:", toolpath.error);
|
||||
alert("Could not plan G-code:\n\n" + toolpath.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") {
|
||||
this.showGcodeMessage = false;
|
||||
|
||||
@@ -259,11 +248,7 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// api.get throws on non-2xx; log and break the loop so the
|
||||
// dialog doesn't stay up forever.
|
||||
console.error(error);
|
||||
this.showGcodeMessage = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -303,75 +303,6 @@ class AuxAxis(object):
|
||||
return
|
||||
self._do_steps(int(steps), ignore_limits=True)
|
||||
|
||||
# ----------------------------------------------- continuous-rate jog
|
||||
#
|
||||
# Hold-to-jog support for the gamepad pendant. JOG / JOGSTOP on
|
||||
# the ESP give a smooth ramp-up, cruise-until-released, ramp-down
|
||||
# profile - much better than streaming small STEPS chunks.
|
||||
#
|
||||
# `jog_start` returns immediately after the ESP acknowledges with
|
||||
# `[jog] started ...`. The terminal `[jog] done count=<n>
|
||||
# pos=<p>` arrives later; our reader picks it up and resyncs
|
||||
# _pos_steps via the same path as STEPS.
|
||||
def jog_start(self, direction, max_rate_sps=None,
|
||||
accel_sps2=None, ignore_limits=False,
|
||||
target_steps=None):
|
||||
"""Begin a continuous-rate jog. `direction` is +1 or -1.
|
||||
Returns once the ESP has accepted the JOG command.
|
||||
|
||||
target_steps (optional): a signed step-counter value. The
|
||||
ESP picks the deceleration start point so the motor ramps
|
||||
smoothly from the current cruise rate to step_start_rate
|
||||
and stops AT this counter value. Used to enforce host-side
|
||||
soft limits without overshoot. The target must be on the
|
||||
side of the current g_pos that matches `direction`; the
|
||||
ESP rejects a wrong-side target with reason=softlimit."""
|
||||
self._require_present()
|
||||
if direction not in (-1, +1):
|
||||
raise AuxAxisError('jog_start direction must be +/-1')
|
||||
sign = '+' if direction > 0 else '-'
|
||||
rate = (int(max_rate_sps) if max_rate_sps is not None
|
||||
else int(self._cfg['step_max_sps']))
|
||||
accel = (int(accel_sps2) if accel_sps2 is not None
|
||||
else int(self._cfg['step_accel_sps2']))
|
||||
if rate < 1: rate = 1
|
||||
if accel < 1: accel = 1
|
||||
cmd = 'JOG dir=%s maxrate=%d accel=%d safe=%d' % (
|
||||
sign, rate, accel, 0 if ignore_limits else 1)
|
||||
if target_steps is not None:
|
||||
cmd += ' target=%d' % int(target_steps)
|
||||
# Capture both the immediate ack AND the eventual terminal
|
||||
# line in a single _rpc call would block; instead fire the
|
||||
# ack-only RPC here and let _on_line handle the terminal
|
||||
# `[jog] done` async (it falls through to the info log path,
|
||||
# but we hook _on_line to update _pos_steps).
|
||||
line = self._rpc(cmd, topic='jog', timeout=2.0)
|
||||
if line.startswith('error'):
|
||||
raise AuxAxisError('JOG rejected: %s' % line)
|
||||
if not line.startswith('started'):
|
||||
# Could be "done count=0 pos=..." if a near-instant abort
|
||||
# raced; treat as completed.
|
||||
self._pos_steps = self._parse_kv_int(
|
||||
line, 'pos', self._pos_steps)
|
||||
self._publish_state()
|
||||
# else: cruising, terminal [jog] reply will arrive later.
|
||||
|
||||
def jog_stop(self):
|
||||
"""Request the running JOG to ramp down to a stop. Returns
|
||||
immediately; the terminal `[jog] done` arrives async and is
|
||||
picked up by `_on_line` to resync _pos_steps.
|
||||
|
||||
Like abort(), this does NOT take the RPC lock - JOGSTOP is
|
||||
the on-release path of a hold-to-jog UI and must not block
|
||||
on whatever else is in flight."""
|
||||
if not self._present:
|
||||
return
|
||||
try:
|
||||
self.log.info('aux >> JOGSTOP')
|
||||
self._send_raw('JOGSTOP')
|
||||
except Exception as e:
|
||||
self.log.warning('JOGSTOP send failed: %s' % e)
|
||||
|
||||
def abort(self):
|
||||
"""Cancel any running ESP motion immediately."""
|
||||
if not self._present:
|
||||
@@ -382,23 +313,38 @@ class AuxAxis(object):
|
||||
except Exception as e:
|
||||
self.log.warning('ABORT send failed: %s' % e)
|
||||
|
||||
# ---------------------------------------------------------- ATC atoms
|
||||
# ---------------------------------------------------------- ATC commands
|
||||
#
|
||||
# The auxcnc firmware drives an AMB 1050 FME-W DI tool changer via
|
||||
# two pneumatic valves on relays 1-2:
|
||||
# V1 (clamp, 3/2 valve) - relay 2: ON = collet open, OFF = vent + spring closes
|
||||
# V2 (ejector) - relay 1: ON = ejector cylinder extends
|
||||
#
|
||||
# The host exposes three composable atoms - RELEASE, CLAMP, EJECT -
|
||||
# and composes drop/grab sequences from G-code macros that call
|
||||
# them in order. (Older firmware exposed monolithic DROPTOOL /
|
||||
# GRABTOOL verbs; protocol v3 dropped them in favour of these
|
||||
# atoms so callers can interleave Z moves between ejector pulses.)
|
||||
# three pneumatic valves on relays 1-3. The ESP runs the timed
|
||||
# sequences itself; the host just kicks them off and waits for the
|
||||
# terminal reply.
|
||||
|
||||
def atc_droptool(self, timeout=30.0):
|
||||
"""Eject the current tool. Opens the collet (V1), oscillates the
|
||||
ejector (V2), then re-clamps with a bleed cycle. Blocks until
|
||||
the ESP reports done. Raises on failure."""
|
||||
self._require_present()
|
||||
line = self._rpc('DROPTOOL', topic='droptool', timeout=timeout)
|
||||
if line.startswith('done'):
|
||||
return
|
||||
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||
raise AuxAxisError('DROPTOOL failed: %s' % reason)
|
||||
|
||||
def atc_grabtool(self, timeout=30.0):
|
||||
"""Pick up a tool that's already been seated by the operator.
|
||||
Opens V1 (releases the collet), waits for the operator to insert
|
||||
the holder, then re-clamps with a bleed cycle. Blocks."""
|
||||
self._require_present()
|
||||
line = self._rpc('GRABTOOL', topic='grabtool', timeout=timeout)
|
||||
if line.startswith('done'):
|
||||
return
|
||||
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||
raise AuxAxisError('GRABTOOL failed: %s' % reason)
|
||||
|
||||
def atc_release(self, timeout=5.0):
|
||||
"""Open the collet (V1 on). Instant. Idempotent. Pairs with
|
||||
atc_clamp() to bracket a sequence of host-side moves and/or
|
||||
ejector pulses with the collet held open."""
|
||||
"""Manually open the collet (release-only, no clamp). Use
|
||||
atc_clamp() afterwards once the new holder is in place."""
|
||||
self._require_present()
|
||||
line = self._rpc('RELEASE', topic='release', timeout=timeout)
|
||||
if line.startswith('done'):
|
||||
@@ -407,8 +353,8 @@ class AuxAxis(object):
|
||||
raise AuxAxisError('RELEASE failed: %s' % reason)
|
||||
|
||||
def atc_clamp(self, timeout=10.0):
|
||||
"""Close the collet: V1 off, then dwell for the line to vent
|
||||
and the spring to re-engage. Idempotent."""
|
||||
"""Manually clamp the collet (run a full bleed cycle). Pairs
|
||||
with atc_release() for two-step manual tool changes."""
|
||||
self._require_present()
|
||||
line = self._rpc('CLAMP', topic='clamp', timeout=timeout)
|
||||
if line.startswith('done'):
|
||||
@@ -416,29 +362,6 @@ class AuxAxis(object):
|
||||
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||
raise AuxAxisError('CLAMP failed: %s' % reason)
|
||||
|
||||
def atc_eject(self, pulse_ms=None, dwell_ms=None, timeout=10.0):
|
||||
"""One ejector wiggle: V2 on for pulse_ms, then off for
|
||||
dwell_ms. The collet (V1) is left in whatever state the caller
|
||||
set it to via atc_release/atc_clamp - typically RELEASE first
|
||||
so the holder can actually drop.
|
||||
|
||||
Repeatedly calling atc_eject gives the wiggle that the old
|
||||
monolithic DROPTOOL did internally, but as discrete blocking
|
||||
calls so a macro can interleave Z moves between pulses.
|
||||
|
||||
pulse_ms / dwell_ms default to the ESP-side defaults
|
||||
(currently 500 / 500). Pass explicit values to override."""
|
||||
self._require_present()
|
||||
parts = ['EJECT']
|
||||
if pulse_ms is not None: parts.append('pulse=%d' % int(pulse_ms))
|
||||
if dwell_ms is not None: parts.append('dwell=%d' % int(dwell_ms))
|
||||
cmd = ' '.join(parts)
|
||||
line = self._rpc(cmd, topic='eject', timeout=timeout)
|
||||
if line.startswith('done'):
|
||||
return
|
||||
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||
raise AuxAxisError('EJECT failed: %s' % reason)
|
||||
|
||||
def close(self):
|
||||
self._stop.set()
|
||||
try:
|
||||
@@ -692,22 +615,7 @@ class AuxAxis(object):
|
||||
self._pending_replies.append(body)
|
||||
self._pending_cv.notify_all()
|
||||
return
|
||||
# Async informational line.
|
||||
#
|
||||
# The terminal [jog] done|aborted line for a continuous
|
||||
# JOG arrives long after the JOG _rpc returned (the JOG
|
||||
# _rpc only waits for the immediate `[jog] started`
|
||||
# ack). Use this async path to keep _pos_steps in sync
|
||||
# so subsequent moves compute the correct delta.
|
||||
if topic == 'jog' and ('pos=' in body):
|
||||
try:
|
||||
self._pos_steps = self._parse_kv_int(
|
||||
body, 'pos', self._pos_steps)
|
||||
if 'reason=limit' in body:
|
||||
self._homed = False
|
||||
self._publish_state()
|
||||
except Exception:
|
||||
pass
|
||||
# Async informational line; just log.
|
||||
self.log.info('aux: %s' % line)
|
||||
else:
|
||||
self.log.info('aux: %s' % line)
|
||||
|
||||
@@ -11,28 +11,20 @@
|
||||
# so gplan handles W motion natively. The preprocessor no longer
|
||||
# touches W tokens. ATC pneumatics still go through the hook
|
||||
# channel because they're events, not motion.
|
||||
# v3: ATC primitives split into atoms. The composite DROPTOOL /
|
||||
# GRABTOOL hooks are gone; macros now compose tool changes from
|
||||
# RELEASE / CLAMP / EJECT.
|
||||
#
|
||||
# What this still does
|
||||
# --------------------
|
||||
# Maps three user-defined M-codes onto pneumatic-tool-changer atoms:
|
||||
# Maps four user-defined M-codes onto pneumatic-tool-changer events:
|
||||
#
|
||||
# M100 EJECT -> (MSG,HOOK:eject:) one V2 ejector pulse
|
||||
# M102 RELEASE -> (MSG,HOOK:release:) open collet (V1 on)
|
||||
# M103 CLAMP -> (MSG,HOOK:clamp:) close collet (V1 off + vent)
|
||||
#
|
||||
# M101 (formerly GRABTOOL) is intentionally unmapped - it's now a
|
||||
# pure host-side macro composed from RELEASE / dwell / CLAMP. If a
|
||||
# legacy file still emits M101 the preprocessor leaves it alone and
|
||||
# the planner ignores it (M101 is in the user-defined range, so it
|
||||
# won't error - it just won't do anything).
|
||||
# M100 DROPTOOL -> (MSG,HOOK:droptool:)
|
||||
# M101 GRABTOOL -> (MSG,HOOK:grabtool:)
|
||||
# M102 RELEASE -> (MSG,HOOK:release:)
|
||||
# M103 CLAMP -> (MSG,HOOK:clamp:)
|
||||
#
|
||||
# M100-M103 are in LinuxCNC/Buildbotics' user-defined range, so the
|
||||
# planner won't error if the codes leak through unrewritten - it just
|
||||
# won't *do* anything. We strip the recognized ones out and emit the
|
||||
# matching hook line in their place.
|
||||
# won't *do* anything. We strip them out and emit the matching hook
|
||||
# line in their place.
|
||||
#
|
||||
# The preprocessor is intentionally conservative: anything it doesn't
|
||||
# understand is left alone.
|
||||
@@ -46,46 +38,12 @@ import tempfile
|
||||
|
||||
|
||||
# Strip line comments so we don't get fooled by "(M100 not really)".
|
||||
# Note this is a simple regex and doesn't handle nested parentheses
|
||||
# - which actually occur in real macro headers like
|
||||
# `(Composed from atoms: M102 = RELEASE (V1 on), M103 = CLAMP)`.
|
||||
# Use _strip_comments() below for a parser that does handle them.
|
||||
_PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
|
||||
|
||||
|
||||
def _strip_comments(line):
|
||||
"""Return `line` with paren comments and the trailing semicolon
|
||||
comment removed. Handles arbitrarily nested parentheses (RS274
|
||||
technically forbids them but real-world gcode comments often
|
||||
contain prose with parens, e.g. `(M102 = RELEASE (V1 on))`).
|
||||
|
||||
Returns just the executable code, with the original whitespace
|
||||
preserved between tokens."""
|
||||
out = []
|
||||
depth = 0
|
||||
i = 0
|
||||
n = len(line)
|
||||
while i < n:
|
||||
c = line[i]
|
||||
if c == ';' and depth == 0:
|
||||
break
|
||||
if c == '(':
|
||||
depth += 1
|
||||
i += 1
|
||||
continue
|
||||
if c == ')':
|
||||
if depth > 0: depth -= 1
|
||||
i += 1
|
||||
continue
|
||||
if depth == 0:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
# ATC pneumatics M-codes mapped onto hook events. M101 is
|
||||
# deliberately unassigned (see header).
|
||||
# ATC pneumatics M-codes mapped onto hook events.
|
||||
_ATC_M_CODES = {
|
||||
100: 'eject',
|
||||
100: 'droptool',
|
||||
101: 'grabtool',
|
||||
102: 'release',
|
||||
103: 'clamp',
|
||||
}
|
||||
@@ -169,7 +127,8 @@ class AuxPreprocessor(object):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
for line in f:
|
||||
code = _strip_comments(line)
|
||||
code = _PAREN_COMMENT_RE.sub('', line)
|
||||
code = code.split(';', 1)[0]
|
||||
if _ATC_M_RE.search(code):
|
||||
return True
|
||||
if couple_active:
|
||||
@@ -360,7 +319,8 @@ class AuxPreprocessor(object):
|
||||
line = raw.rstrip('\n')
|
||||
|
||||
# Comment-only or blank lines pass through verbatim.
|
||||
code = _strip_comments(line)
|
||||
code = _PAREN_COMMENT_RE.sub('', line)
|
||||
code = code.split(';', 1)[0]
|
||||
if not code.strip():
|
||||
fout.write(raw)
|
||||
continue
|
||||
@@ -379,14 +339,10 @@ class AuxPreprocessor(object):
|
||||
if self._maybe_inject_a_down(code, fout):
|
||||
rewrote_any = True
|
||||
|
||||
# ATC M-codes (M100/M102/M103). Match against the
|
||||
# comment-stripped `code` so prose mentions like
|
||||
# `(M102 = RELEASE)` inside a comment don't spuriously
|
||||
# fire hooks. Each match emits a (MSG,HOOK:<event>:)
|
||||
# line; the M-code is stripped from the executable
|
||||
# residual but the original line's comments are kept
|
||||
# for log readability.
|
||||
atc_matches = list(_ATC_M_RE.finditer(code))
|
||||
# ATC M-codes (M100-M103). Each ATC M-code on the line
|
||||
# is replaced with its (MSG,HOOK:<event>:) line and
|
||||
# stripped from the residual.
|
||||
atc_matches = list(_ATC_M_RE.finditer(line))
|
||||
if atc_matches:
|
||||
rewrote_any = True
|
||||
for m in atc_matches:
|
||||
@@ -394,49 +350,19 @@ class AuxPreprocessor(object):
|
||||
except ValueError: continue
|
||||
event = _ATC_M_CODES.get(num)
|
||||
if event:
|
||||
# We need two things here that aren't
|
||||
# naturally provided by the (MSG,...)
|
||||
# transport:
|
||||
#
|
||||
# (1) Synchronization. (MSG,HOOK:...) is
|
||||
# fire-and-forget from gplan's view -
|
||||
# gplan emits the message and keeps
|
||||
# streaming subsequent blocks (Z
|
||||
# moves, the next eject, etc.) to the
|
||||
# AVR. Meanwhile the hook handler
|
||||
# runs the actual ESP RPC in a
|
||||
# thread, and Z lifts while V2 is
|
||||
# still wiggling. To make M-codes
|
||||
# behave like proper blocking gcode,
|
||||
# we precede each HOOK with M0
|
||||
# (program pause). The Hooks layer
|
||||
# registers the atom as block_unpause
|
||||
# + auto_resume, so:
|
||||
# M0 -> machine pauses
|
||||
# (MSG,HOOK:event:) fires hook
|
||||
# hook thread runs ESP RPC
|
||||
# hook completes, auto-unpauses
|
||||
# next block streams
|
||||
# End result: M100/M102/M103 block
|
||||
# until the ESP says done, just like
|
||||
# a G-code dwell.
|
||||
#
|
||||
# (2) Block separation. gplan collapses
|
||||
# consecutive comment-only lines
|
||||
# into a single block, so back-to-
|
||||
# back HOOK lines used to drop all
|
||||
# but the last. M0 is its own block
|
||||
# so this falls out automatically -
|
||||
# the (MSG,...) attaches cleanly to
|
||||
# each M0.
|
||||
fout.write('M0 (MSG,HOOK:%s:)\n' % event)
|
||||
code_stripped = _ATC_M_RE.sub('', code).strip()
|
||||
if code_stripped:
|
||||
# Mixed line: keep the residual executable
|
||||
# gcode. Drop the comments to keep the
|
||||
# rewritten file tidy (the original line's
|
||||
# text already appears once as the input).
|
||||
fout.write(code_stripped + '\n')
|
||||
fout.write('(MSG,HOOK:%s:)\n' % event)
|
||||
line = _ATC_M_RE.sub('', line)
|
||||
code = _PAREN_COMMENT_RE.sub('', line)
|
||||
code = code.split(';', 1)[0]
|
||||
if not code.strip():
|
||||
# Nothing meaningful left; preserve any trailing
|
||||
# comment text but skip empty lines.
|
||||
rest = line.rstrip()
|
||||
if rest:
|
||||
fout.write(rest + '\n')
|
||||
continue
|
||||
# Other gcode remains on the line - emit it.
|
||||
fout.write(line + '\n')
|
||||
continue
|
||||
|
||||
# No rewrite needed.
|
||||
@@ -445,26 +371,16 @@ class AuxPreprocessor(object):
|
||||
return rewrote_any
|
||||
|
||||
|
||||
def preprocess_to_tempfile(src_path, log=None, coupling=None):
|
||||
"""Run the preprocessor on `src_path` and return the path to a
|
||||
rewritten temp file (or None if no rewriting was needed). Caller
|
||||
owns the temp file and must os.unlink() it when done.
|
||||
def preprocess_file(src_path, log=None, coupling=None, **_unused):
|
||||
"""Convenience: rewrite src_path in place if it contains ATC
|
||||
M-codes or needs Z-A coupling injection. Returns True if the
|
||||
file was rewritten.
|
||||
|
||||
The original source file is never modified - this is the
|
||||
intentional design: the macro / job file the operator authored
|
||||
is what they see in the macro editor and the file viewer; the
|
||||
rewriting happens only on the in-memory copy that gplan loads.
|
||||
|
||||
Why we rewrite at all: gplan (the camotics planner) treats the
|
||||
user-defined M-codes M100/M102/M103 as no-ops. The only callback
|
||||
channel it exposes during a running program is the (MSG,...)
|
||||
message stream, so the only way for the host to react to those
|
||||
M-codes mid-program is to substitute (MSG,HOOK:<event>:) lines
|
||||
in their place. This rewriting is an implementation detail the
|
||||
operator should never have to know about - hence the tempfile.
|
||||
"""
|
||||
`coupling` is an optional dict (see AuxPreprocessor.__init__).
|
||||
Extra keyword args are accepted for backwards compat (the old
|
||||
w_first arg is no longer used)."""
|
||||
if not AuxPreprocessor.file_uses_aux(src_path, coupling=coupling):
|
||||
return None
|
||||
return False
|
||||
pre = AuxPreprocessor(log=log, coupling=coupling)
|
||||
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',
|
||||
dir=os.path.dirname(src_path) or None)
|
||||
@@ -472,36 +388,13 @@ def preprocess_to_tempfile(src_path, log=None, coupling=None):
|
||||
try:
|
||||
rewrote = pre.process(src_path, tmp)
|
||||
if rewrote:
|
||||
return tmp
|
||||
shutil.move(tmp, src_path)
|
||||
return True
|
||||
os.unlink(tmp)
|
||||
return None
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def preprocess_file(src_path, log=None, coupling=None, **_unused):
|
||||
"""DEPRECATED in-place version of the preprocessor. Kept for
|
||||
callers that still rewrite their input on disk (chiefly the
|
||||
upload path, where mutating the file is fine because there's no
|
||||
operator-authored source to preserve).
|
||||
|
||||
Returns True if the file was rewritten, False otherwise.
|
||||
|
||||
For new callers prefer preprocess_to_tempfile() which never
|
||||
touches the source."""
|
||||
tmp = preprocess_to_tempfile(src_path, log=log, coupling=coupling)
|
||||
if tmp is None:
|
||||
return False
|
||||
try:
|
||||
shutil.move(tmp, src_path)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
return True
|
||||
|
||||
@@ -166,46 +166,31 @@ class Ctrl(object):
|
||||
else:
|
||||
self.aux.home()
|
||||
|
||||
def _hook_droptool(ctx): self.aux.atc_droptool()
|
||||
def _hook_grabtool(ctx): self.aux.atc_grabtool()
|
||||
def _hook_release(ctx): self.aux.atc_release()
|
||||
def _hook_clamp(ctx): self.aux.atc_clamp()
|
||||
|
||||
def _hook_eject(ctx):
|
||||
# ctx['data'] is the payload after HOOK:eject:. Allow
|
||||
# operators to override pulse / dwell from gcode via
|
||||
# (MSG,HOOK:eject:pulse=400 dwell=300). Empty data ->
|
||||
# ESP defaults.
|
||||
data = (ctx.get('data') or '').strip()
|
||||
kw = {}
|
||||
for tok in data.split():
|
||||
if '=' not in tok: continue
|
||||
k, v = tok.split('=', 1)
|
||||
k = k.strip().lower()
|
||||
if k in ('pulse', 'pulse_ms'):
|
||||
try: kw['pulse_ms'] = int(v)
|
||||
except ValueError: pass
|
||||
elif k in ('dwell', 'dwell_ms'):
|
||||
try: kw['dwell_ms'] = int(v)
|
||||
except ValueError: pass
|
||||
self.aux.atc_eject(**kw)
|
||||
|
||||
# Legacy alias for older gcode that used aux_home.
|
||||
self.hooks.register_internal('aux_home', _hook_aux_home,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=180)
|
||||
|
||||
# ATC pneumatic atoms. block_unpause + auto_resume so a
|
||||
# program using M100/M102/M103 pauses at the right point and
|
||||
# resumes once each atom finishes. Macros compose drop/grab
|
||||
# sequences from these primitives.
|
||||
# ATC pneumatics. block_unpause + auto_resume so a program
|
||||
# using M100/M101/M102/M103 pauses at the right point and
|
||||
# resumes once the sequence is done.
|
||||
self.hooks.register_internal('droptool', _hook_droptool,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=60)
|
||||
self.hooks.register_internal('grabtool', _hook_grabtool,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=60)
|
||||
self.hooks.register_internal('release', _hook_release,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=10)
|
||||
self.hooks.register_internal('clamp', _hook_clamp,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=15)
|
||||
self.hooks.register_internal('eject', _hook_eject,
|
||||
block_unpause=True, auto_resume=True,
|
||||
timeout=15)
|
||||
log.info('Aux hooks registered')
|
||||
|
||||
|
||||
|
||||
@@ -107,27 +107,15 @@ class FileHandler(bbctrl.APIHandler):
|
||||
# auxcnc stepper is exposed as a virtual A axis (see
|
||||
# ExternalAxis).
|
||||
try:
|
||||
from bbctrl.AuxPreprocessor import (
|
||||
preprocess_file, AuxPreprocessorError)
|
||||
from bbctrl.AuxPreprocessor import preprocess_file
|
||||
log = self.get_log('AuxPreprocessor')
|
||||
ext = getattr(self.get_ctrl(), 'ext_axis', None)
|
||||
coupling = (ext.coupling_for_preprocessor()
|
||||
if ext is not None else None)
|
||||
try:
|
||||
if preprocess_file(filename.decode('utf8'),
|
||||
log=log, coupling=coupling):
|
||||
log.info('Rewrote upload (ATC / Z-A coupling) in %s'
|
||||
% self.uploadFilename)
|
||||
except AuxPreprocessorError as e:
|
||||
# Surface coupling-violation errors to the operator
|
||||
# via the message stream so the upload doesn't go
|
||||
# silently un-rewritten and then trip the runtime
|
||||
# check (which can hang the planner dialog).
|
||||
log.warning('Aux preprocess refused upload: %s' % e)
|
||||
try:
|
||||
self.get_ctrl().state.add_message(
|
||||
'Z-A coupling: ' + str(e))
|
||||
except Exception: pass
|
||||
if preprocess_file(filename.decode('utf8'),
|
||||
log=log, coupling=coupling):
|
||||
log.info('Rewrote upload (ATC / Z-A coupling) in %s'
|
||||
% self.uploadFilename)
|
||||
except Exception:
|
||||
self.get_log('AuxPreprocessor').exception(
|
||||
'Aux preprocess failed; uploading unchanged')
|
||||
|
||||
@@ -25,21 +25,10 @@
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import inevent
|
||||
from inevent.Constants import *
|
||||
|
||||
|
||||
# Set to True (or BBCTRL_AJOG_DRYRUN=1 in env) to log press/release
|
||||
# events and would-be ESP commands without actually sending JOG /
|
||||
# JOGSTOP. Useful for debugging the gamepad event path without
|
||||
# touching the gantry. Defaults to live actuation.
|
||||
A_DRY_RUN = os.environ.get('BBCTRL_AJOG_DRYRUN', '') == '1'
|
||||
|
||||
|
||||
# Listen for input events
|
||||
class Jog(inevent.JogHandler):
|
||||
def __init__(self, ctrl):
|
||||
@@ -62,23 +51,12 @@ class Jog(inevent.JogHandler):
|
||||
"dir": [1, -1, -1, 1],
|
||||
"arrows": [ABS_HAT0X, ABS_HAT0Y],
|
||||
"speed": [0x133, 0x130, 0x131, 0x134],
|
||||
"lock": [0x136], # L1 = horiz-lock; RB/RT now A axis
|
||||
# Right back controls drive the A axis while held.
|
||||
# Verified on Xbox 360 pad (Vendor=045e Product=028e):
|
||||
# RB (upper-right bumper) -> BTN_TR (0x137) digital -> A+
|
||||
# RT (lower-right trigger) -> ABS_RZ analog 0..255 -> A-
|
||||
# Some pads expose RT as BTN_TR2 (0x139) instead -- that
|
||||
# works too via a_neg_btn.
|
||||
"a_pos_btn": 0x137,
|
||||
"a_neg_btn": 0x139,
|
||||
"a_neg_abs": ABS_RZ,
|
||||
"a_abs_thresh": 32, # 0..255 trigger press threshold
|
||||
"lock": [0x136, 0x137],
|
||||
}
|
||||
}
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
self.a_button = 0 # -1, 0, +1 from RB / RT hold state
|
||||
self.v = [0.0] * 4
|
||||
self.lastV = self.v
|
||||
self.callback()
|
||||
@@ -86,276 +64,6 @@ class Jog(inevent.JogHandler):
|
||||
self.processor = inevent.InEvent(ctrl.ioloop, self, types = ['js'])
|
||||
|
||||
|
||||
# -------- A-axis (external, ESP-driven) hold-to-jog ---------------
|
||||
#
|
||||
# The Mach jog path only knows about AVR axes; the A axis is
|
||||
# handled by ExternalAxis on the auxcnc ESP, which has a proper
|
||||
# JOG / JOGSTOP protocol added for hold-to-jog: ramp up on press,
|
||||
# cruise while held, ramp down on release.
|
||||
#
|
||||
# Speed buttons (X/A/B/Y) scale the cruise rate (1/128, 1/32,
|
||||
# 1/4, 1.0x of the configured step_max_sps).
|
||||
def _a_speed_scale(self):
|
||||
if self.speed == 1: return 1.0 / 128.0
|
||||
if self.speed == 2: return 1.0 / 32.0
|
||||
if self.speed == 3: return 1.0 / 4.0
|
||||
return 1.0
|
||||
|
||||
def _a_stop(self):
|
||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||
ext_state = ('present' if (ext is not None and ext.enabled)
|
||||
else 'unavailable')
|
||||
if A_DRY_RUN:
|
||||
self.log.info('AJOG DRYRUN _a_stop ext=%s (would send JOGSTOP)',
|
||||
ext_state)
|
||||
return
|
||||
if ext is None or not ext.enabled:
|
||||
return
|
||||
try:
|
||||
ext.aux.jog_stop()
|
||||
except Exception as e:
|
||||
self.log.warning('A-axis jog_stop failed: %s', e)
|
||||
|
||||
def _a_soft_limit_target_steps(self, aux, direction):
|
||||
"""Return a step-counter target for the configured soft
|
||||
limit (`min_mm` / `max_mm`) on the `direction` side of the
|
||||
current position, or None when no limit applies (axis
|
||||
unhomed or limits not configured)."""
|
||||
try:
|
||||
if not bool(aux._homed):
|
||||
return None
|
||||
cfg = aux._cfg
|
||||
lo_mm = float(cfg.get('min_mm', 0.0))
|
||||
hi_mm = float(cfg.get('max_mm', 0.0))
|
||||
if hi_mm <= lo_mm:
|
||||
return None
|
||||
lo_steps = aux._mm_to_steps(lo_mm)
|
||||
hi_steps = aux._mm_to_steps(hi_mm)
|
||||
# _mm_to_steps applies dir_sign; sort so we know which
|
||||
# is "more positive in g_pos".
|
||||
top_steps = max(lo_steps, hi_steps)
|
||||
bottom_steps = min(lo_steps, hi_steps)
|
||||
return top_steps if direction > 0 else bottom_steps
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _a_coupling_target_steps(self, ext, direction):
|
||||
"""Return a step-counter target that prevents the Z-A
|
||||
coupling rule (a - z <= K) from being violated by this jog.
|
||||
Returns None when coupling is disabled or doesn't constrain
|
||||
motion in `direction`.
|
||||
|
||||
The constraint is on machine-mm: the rule limits how far A
|
||||
may go *up* (toward larger machine A) for the current Z. So
|
||||
only the +A jog direction can ever violate it; -A jogs are
|
||||
unconstrained by coupling and we return None for them.
|
||||
|
||||
Note: 'direction' here refers to the gamepad axis sign, not
|
||||
machine-mm. dir_sign in aux config maps gamepad+ to
|
||||
machine+ steps. We translate via the existing
|
||||
ext._a_machine_now / aux._mm_to_steps so the result is in
|
||||
the same g_pos space as _a_soft_limit_target_steps."""
|
||||
try:
|
||||
if not ext.couple_z_enabled:
|
||||
return None
|
||||
if not bool(ext.aux._homed):
|
||||
return None
|
||||
K = ext.couple_K
|
||||
if K is None:
|
||||
return None
|
||||
z_now = ext._z_machine_now()
|
||||
if z_now is None:
|
||||
return None
|
||||
# Max permitted A in machine-mm: a_max = z_now + K.
|
||||
a_max_mm = float(z_now) + float(K)
|
||||
a_max_steps = ext.aux._mm_to_steps(a_max_mm)
|
||||
# The coupling only caps the *upper* side (more-positive
|
||||
# machine A). With dir_sign=+1 that's g_pos+; with
|
||||
# dir_sign=-1 it's g_pos-. Jogs in the opposite gamepad
|
||||
# direction don't approach the coupling bound, return
|
||||
# None so the soft-limit target alone applies.
|
||||
dir_sign = 1 if int(ext.aux._cfg.get('dir_sign', 1)) >= 0 else -1
|
||||
# Gamepad+ moves toward larger machine-mm when dir_sign>0.
|
||||
machine_dir = direction * dir_sign
|
||||
if machine_dir <= 0:
|
||||
return None
|
||||
return a_max_steps
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _a_combined_target_steps(self, ext, direction):
|
||||
"""Pick the more restrictive of soft-limit and coupling
|
||||
targets. Returns (target_steps, source_label) where
|
||||
target_steps is None when neither rule applies."""
|
||||
soft = self._a_soft_limit_target_steps(ext.aux, direction)
|
||||
couple = self._a_coupling_target_steps(ext, direction)
|
||||
if soft is None and couple is None:
|
||||
return None, 'none'
|
||||
if soft is None: return couple, 'coupling'
|
||||
if couple is None: return soft, 'softlimit'
|
||||
# Both present: pick whichever is reached first when moving
|
||||
# in `direction` from the current g_pos.
|
||||
try:
|
||||
cur = int(ext.aux._pos_steps)
|
||||
except Exception:
|
||||
cur = 0
|
||||
if direction > 0:
|
||||
return ((soft, 'softlimit') if soft <= couple
|
||||
else (couple, 'coupling'))
|
||||
else:
|
||||
return ((soft, 'softlimit') if soft >= couple
|
||||
else (couple, 'coupling'))
|
||||
|
||||
def _a_start(self, direction):
|
||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||
ext_state = ('present' if (ext is not None and ext.enabled)
|
||||
else 'unavailable')
|
||||
scale = self._a_speed_scale()
|
||||
target_steps = None
|
||||
target_src = 'none'
|
||||
cur_steps = None
|
||||
if ext is not None and ext.enabled:
|
||||
target_steps, target_src = self._a_combined_target_steps(
|
||||
ext, direction)
|
||||
try: cur_steps = int(ext.aux._pos_steps)
|
||||
except Exception: cur_steps = None
|
||||
if A_DRY_RUN:
|
||||
try:
|
||||
step_max = (int(ext.aux._cfg['step_max_sps'])
|
||||
if ext is not None and ext.enabled else -1)
|
||||
accel = (int(ext.aux._cfg['step_accel_sps2'])
|
||||
if ext is not None and ext.enabled else -1)
|
||||
except Exception:
|
||||
step_max, accel = -1, -1
|
||||
self.log.info(
|
||||
'AJOG DRYRUN _a_start dir=%+d ext=%s speed=%d scale=%.4f '
|
||||
'step_max=%d accel=%d cur_steps=%s target_steps=%s '
|
||||
'target_src=%s (would send JOG)',
|
||||
direction, ext_state, self.speed, scale, step_max, accel,
|
||||
cur_steps, target_steps, target_src)
|
||||
return
|
||||
if ext is None or not ext.enabled or direction == 0:
|
||||
return
|
||||
try:
|
||||
aux = ext.aux
|
||||
max_rate = max(1, int(int(aux._cfg['step_max_sps']) * scale))
|
||||
accel = int(aux._cfg['step_accel_sps2'])
|
||||
# If the axis is already at-or-past the more-restrictive
|
||||
# boundary (soft limit OR Z-A coupling) in the requested
|
||||
# direction, refuse the jog rather than sending a
|
||||
# wrong-side target the ESP would reject.
|
||||
if target_steps is not None and cur_steps is not None:
|
||||
at_limit = ((direction > 0 and cur_steps >= target_steps)
|
||||
or (direction < 0 and cur_steps <= target_steps))
|
||||
if at_limit:
|
||||
self.log.info(
|
||||
'A-axis jog refused: at %s limit '
|
||||
'(cur=%d target=%d dir=%+d)',
|
||||
target_src, cur_steps, target_steps, direction)
|
||||
return
|
||||
# ignore_limits=True (safe=0) when the axis is unhomed:
|
||||
# pendant jog is allowed before homing for setup. When
|
||||
# homed, soft limits AND Z-A coupling are enforced via
|
||||
# target_steps and the ESP's hardware-limit abort still
|
||||
# applies unconditionally (movingTowardLimit in
|
||||
# jogTask).
|
||||
ignore = not bool(aux._homed)
|
||||
aux.jog_start(direction,
|
||||
max_rate_sps=max_rate,
|
||||
accel_sps2=accel,
|
||||
ignore_limits=ignore,
|
||||
target_steps=target_steps)
|
||||
if target_steps is not None:
|
||||
self.log.info(
|
||||
'A-axis jog_start dir=%+d cur=%d target=%d (%s)',
|
||||
direction, cur_steps, target_steps, target_src)
|
||||
except Exception as e:
|
||||
self.log.warning('A-axis jog_start failed: %s', e)
|
||||
|
||||
def _a_apply(self, new_dir, old_dir):
|
||||
if new_dir == old_dir:
|
||||
return
|
||||
# On any state change we stop the current jog and (if the
|
||||
# new direction is non-zero) start a fresh one. JOG / JOGSTOP
|
||||
# are non-blocking on the host side.
|
||||
if old_dir != 0:
|
||||
self._a_stop()
|
||||
if new_dir != 0:
|
||||
self._a_start(new_dir)
|
||||
|
||||
def _a_resync_pos(self):
|
||||
"""Pull the ESP step counter back into ExternalAxis after a
|
||||
JOG ends, so subsequent gplan-driven A motion computes the
|
||||
right delta. Called opportunistically on state changes; the
|
||||
AuxAxis reader also updates _pos_steps from the terminal
|
||||
[jog] done line."""
|
||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||
if ext is None or not ext.enabled:
|
||||
return
|
||||
try:
|
||||
ext._pos_mm = ext.aux.position_mm
|
||||
self.ctrl.state.set(ext.axis_letter + 'p', ext._pos_mm)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def event(self, event, state, dev_name):
|
||||
cfg = self.get_config(dev_name)
|
||||
old = self.a_button
|
||||
|
||||
# DEBUG: log EVERY incoming gamepad event so we can see
|
||||
# exactly what the pendant is producing on press/release.
|
||||
# Skip noisy stick / report-syn events to keep the journal
|
||||
# readable but log all KEY events and any ABS event whose
|
||||
# code matches one we care about.
|
||||
try:
|
||||
tname = ev_type_name.get(event.type, '?')
|
||||
except Exception:
|
||||
tname = '?'
|
||||
if event.type == EV_KEY:
|
||||
self.log.info(
|
||||
'AJOG EV dev=%r type=%s(%d) code=0x%x val=%d '
|
||||
'cfg.a_pos_btn=0x%x cfg.a_neg_btn=0x%x',
|
||||
dev_name, tname, event.type, event.code, event.value,
|
||||
cfg.get('a_pos_btn', 0), cfg.get('a_neg_btn', 0))
|
||||
elif event.type == EV_ABS and event.code in (
|
||||
cfg.get('a_neg_abs', -1),
|
||||
cfg.get('a_pos_abs', -1)):
|
||||
self.log.info(
|
||||
'AJOG EV dev=%r type=%s(%d) code=0x%x val=%d (trigger ABS)',
|
||||
dev_name, tname, event.type, event.code, event.value)
|
||||
|
||||
if event.type == EV_KEY:
|
||||
if event.code == cfg.get('a_pos_btn'):
|
||||
if event.value: self.a_button = 1
|
||||
elif self.a_button == 1: self.a_button = 0
|
||||
elif event.code == cfg.get('a_neg_btn'):
|
||||
if event.value: self.a_button = -1
|
||||
elif self.a_button == -1: self.a_button = 0
|
||||
|
||||
elif event.type == EV_ABS:
|
||||
thresh = cfg.get('a_abs_thresh', 32)
|
||||
if event.code == cfg.get('a_neg_abs'):
|
||||
if event.value >= thresh: self.a_button = -1
|
||||
elif self.a_button == -1: self.a_button = 0
|
||||
|
||||
if self.a_button != old:
|
||||
self.log.info(
|
||||
'AJOG STATE %+d -> %+d (t=%.3f dry_run=%s)',
|
||||
old, self.a_button, time.monotonic(), A_DRY_RUN)
|
||||
self._a_apply(self.a_button, old)
|
||||
# On every release pull a fresh position mirror in case
|
||||
# the user does a gplan-driven A move next. The terminal
|
||||
# [jog] done line itself already updates aux._pos_steps;
|
||||
# this propagates that into ExternalAxis._pos_mm.
|
||||
if self.a_button == 0 and not A_DRY_RUN:
|
||||
# Wait briefly so the [jog] done line has time to
|
||||
# arrive before we read aux.position_mm.
|
||||
self.ctrl.ioloop.call_later(0.2, self._a_resync_pos)
|
||||
|
||||
super().event(event, state, dev_name)
|
||||
|
||||
|
||||
def up(self): self.ctrl.lcd.page_up()
|
||||
def down(self): self.ctrl.lcd.page_down()
|
||||
def left(self): self.ctrl.lcd.page_left()
|
||||
@@ -382,7 +90,4 @@ class Jog(inevent.JogHandler):
|
||||
if self.speed == 2: scale = 1.0 / 32.0
|
||||
if self.speed == 3: scale = 1.0 / 4.0
|
||||
|
||||
# axes[3] is left untouched by RB/RT -- the A axis is the
|
||||
# ESP-driven external axis on this branch and is jogged via
|
||||
# discrete relative moves through ExternalAxis (see _a_pump).
|
||||
self.v = [x * scale for x in self.axes]
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from collections import deque
|
||||
@@ -77,10 +76,6 @@ class Planner():
|
||||
self.planner = None
|
||||
self._position_dirty = False
|
||||
self.where = ''
|
||||
# Tracks the rewritten temp file (if any) returned by the
|
||||
# AuxPreprocessor for the currently-loaded program. We delete
|
||||
# it on the next load() so it doesn't pile up under /tmp.
|
||||
self._aux_tempfile = None
|
||||
|
||||
ctrl.state.add_listener(self._update)
|
||||
|
||||
@@ -512,57 +507,28 @@ class Planner():
|
||||
|
||||
def load(self, path):
|
||||
self.where = path
|
||||
src_path = self.ctrl.get_path('upload', path)
|
||||
self.log.info('GCode:' + src_path)
|
||||
|
||||
# Clean up any leftover temp file from a previous load.
|
||||
self._cleanup_aux_tempfile()
|
||||
|
||||
# Rewrite ATC M-codes (M100/M102/M103) and inject Z-A
|
||||
# coupling moves before gplan sees them. The rewriting goes
|
||||
# to a temp file -- the operator's macro / job source is
|
||||
# never modified. This matters because:
|
||||
#
|
||||
# 1. The macro editor reads back the source. If we
|
||||
# rewrote in place, the operator would open `drop.nc`
|
||||
# and see (MSG,HOOK:...) blobs instead of the M-code
|
||||
# sequence they wrote.
|
||||
# 2. Re-running a rewritten file would re-rewrite it; any
|
||||
# bug in the regex (e.g. with paren comments) would
|
||||
# compound on every load.
|
||||
#
|
||||
# Why we rewrite at all: gplan treats M100..M103 as no-ops
|
||||
# by spec and exposes no callback for user M-codes. Its only
|
||||
# in-band channel back to Python during a running program is
|
||||
# the (MSG,...) message stream, so we substitute hook
|
||||
# messages for the M-codes purely as transport.
|
||||
load_path = src_path
|
||||
path = self.ctrl.get_path('upload', path)
|
||||
self.log.info('GCode:' + path)
|
||||
# Rewrite ATC M-codes (M100..M103) before gplan sees them.
|
||||
# preprocess_file is a no-op when no rewriting is needed and
|
||||
# idempotent when run twice on the same file, so this is
|
||||
# safe on every load. W tokens are no longer rewritten - the
|
||||
# auxcnc stepper is now exposed as a virtual A axis and gcode
|
||||
# should use A directly.
|
||||
try:
|
||||
from bbctrl.AuxPreprocessor import preprocess_to_tempfile
|
||||
from bbctrl.AuxPreprocessor import preprocess_file
|
||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||
coupling = (ext.coupling_for_preprocessor()
|
||||
if ext is not None else None)
|
||||
tmp = preprocess_to_tempfile(
|
||||
src_path, log=self.log, coupling=coupling)
|
||||
if tmp is not None:
|
||||
self._aux_tempfile = tmp
|
||||
load_path = tmp
|
||||
self.log.info(
|
||||
'Rewrote (ATC / Z-A coupling) for gplan: %s -> %s'
|
||||
% (src_path, tmp))
|
||||
if preprocess_file(path, log=self.log, coupling=coupling):
|
||||
self.log.info('Rewrote (ATC / Z-A coupling) in %s' % path)
|
||||
except Exception:
|
||||
self.log.exception('Aux preprocess at load failed; '
|
||||
'attempting to load file unchanged')
|
||||
self._sync_position()
|
||||
self.planner.load(load_path, self.get_config(False, True))
|
||||
self.planner.load(path, self.get_config(False, True))
|
||||
self.reset_times()
|
||||
|
||||
def _cleanup_aux_tempfile(self):
|
||||
if self._aux_tempfile and os.path.exists(self._aux_tempfile):
|
||||
try: os.unlink(self._aux_tempfile)
|
||||
except OSError: pass
|
||||
self._aux_tempfile = None
|
||||
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
|
||||
@@ -74,7 +74,6 @@ class Plan(object):
|
||||
self.progress = 0
|
||||
self.cancel = False
|
||||
self.pid = None
|
||||
self.error = None
|
||||
|
||||
root = ctrl.get_path()
|
||||
self.gcode = '%s/upload/%s' % (root, filename)
|
||||
@@ -203,16 +202,8 @@ class Plan(object):
|
||||
if not self._exists(): yield self._exec()
|
||||
self.future.set_result(self._read())
|
||||
|
||||
except Exception as e:
|
||||
# Record the error and ALWAYS resolve the future, otherwise
|
||||
# PathHandler.get keeps timing out at 1s forever and the UI
|
||||
# gets stuck on the "Processing New File" dialog.
|
||||
self.preplanner.log.exception(
|
||||
"Failed to plan file: " + str(e))
|
||||
self.error = str(e) or 'Plan failed'
|
||||
self.progress = 1
|
||||
if not self.future.done():
|
||||
self.future.set_result(None)
|
||||
except:
|
||||
self.preplanner.log.exception("Failed to load file - doesn't appear to be GCode.")
|
||||
|
||||
|
||||
class Preplanner(object):
|
||||
@@ -277,6 +268,3 @@ class Preplanner(object):
|
||||
|
||||
def get_plan_progress(self, filename):
|
||||
return self.plans[filename].progress if filename in self.plans else 0
|
||||
|
||||
def get_plan_error(self, filename):
|
||||
return self.plans[filename].error if filename in self.plans else None
|
||||
|
||||
@@ -411,22 +411,11 @@ class PathHandler(bbctrl.APIHandler):
|
||||
|
||||
except gen.TimeoutError:
|
||||
progress = preplanner.get_plan_progress(filename)
|
||||
err = preplanner.get_plan_error(filename)
|
||||
resp = dict(progress = progress)
|
||||
if err: resp['error'] = err
|
||||
self.write_json(resp)
|
||||
self.write_json(dict(progress = progress))
|
||||
return
|
||||
|
||||
try:
|
||||
# Plan finished but produced no data (planner subprocess
|
||||
# failed, e.g. AuxPreprocessor coupling rejection at
|
||||
# planner-load time). Surface the error so the UI can
|
||||
# close the "Processing New File" dialog instead of
|
||||
# polling forever.
|
||||
if data is None:
|
||||
err = preplanner.get_plan_error(filename) or 'Plan failed'
|
||||
self.write_json(dict(progress = 1, error = err))
|
||||
return
|
||||
if data is None: return
|
||||
meta, positions, speeds = data
|
||||
|
||||
if dataType == '/positions': data = positions
|
||||
|
||||
Reference in New Issue
Block a user