plotdeck 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotdeck-1.0.0/PKG-INFO +227 -0
- plotdeck-1.0.0/README.md +206 -0
- plotdeck-1.0.0/pyproject.toml +66 -0
- plotdeck-1.0.0/setup.cfg +4 -0
- plotdeck-1.0.0/src/plotdeck/__init__.py +4 -0
- plotdeck-1.0.0/src/plotdeck/__main__.py +6 -0
- plotdeck-1.0.0/src/plotdeck/app.py +55 -0
- plotdeck-1.0.0/src/plotdeck/core/__init__.py +4 -0
- plotdeck-1.0.0/src/plotdeck/core/controller.py +561 -0
- plotdeck-1.0.0/src/plotdeck/core/gcode_parser.py +404 -0
- plotdeck-1.0.0/src/plotdeck/core/grbl_protocol.py +278 -0
- plotdeck-1.0.0/src/plotdeck/core/status.py +148 -0
- plotdeck-1.0.0/src/plotdeck/core/streamer.py +328 -0
- plotdeck-1.0.0/src/plotdeck/core/transport.py +375 -0
- plotdeck-1.0.0/src/plotdeck/ui/__init__.py +1 -0
- plotdeck-1.0.0/src/plotdeck/ui/connection_panel.py +139 -0
- plotdeck-1.0.0/src/plotdeck/ui/console.py +171 -0
- plotdeck-1.0.0/src/plotdeck/ui/dialogs.py +317 -0
- plotdeck-1.0.0/src/plotdeck/ui/editor.py +544 -0
- plotdeck-1.0.0/src/plotdeck/ui/job_panel.py +130 -0
- plotdeck-1.0.0/src/plotdeck/ui/jog_panel.py +222 -0
- plotdeck-1.0.0/src/plotdeck/ui/main_window.py +1116 -0
- plotdeck-1.0.0/src/plotdeck/ui/manual.py +156 -0
- plotdeck-1.0.0/src/plotdeck/ui/status_panel.py +244 -0
- plotdeck-1.0.0/src/plotdeck/ui/theme.py +329 -0
- plotdeck-1.0.0/src/plotdeck/ui/visualizer.py +374 -0
- plotdeck-1.0.0/src/plotdeck/utils/__init__.py +1 -0
- plotdeck-1.0.0/src/plotdeck/utils/logging_setup.py +42 -0
- plotdeck-1.0.0/src/plotdeck/utils/settings.py +86 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/PKG-INFO +227 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/SOURCES.txt +38 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/dependency_links.txt +1 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/entry_points.txt +5 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/requires.txt +15 -0
- plotdeck-1.0.0/src/plotdeck.egg-info/top_level.txt +1 -0
- plotdeck-1.0.0/tests/test_controller.py +218 -0
- plotdeck-1.0.0/tests/test_gcode_parser.py +132 -0
- plotdeck-1.0.0/tests/test_status.py +69 -0
- plotdeck-1.0.0/tests/test_streamer.py +186 -0
- plotdeck-1.0.0/tests/test_ui_smoke.py +178 -0
plotdeck-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotdeck
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: G-code sender and editor for GRBL-based CNC routers
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: PySide6>=6.6
|
|
9
|
+
Requires-Dist: pyserial>=3.5
|
|
10
|
+
Requires-Dist: pyqtgraph>=0.13
|
|
11
|
+
Requires-Dist: numpy>=1.24
|
|
12
|
+
Provides-Extra: 3d
|
|
13
|
+
Requires-Dist: PyOpenGL>=3.1; extra == "3d"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-qt>=4.4; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
18
|
+
Requires-Dist: black>=24; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
20
|
+
Requires-Dist: pyinstaller>=6; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# PlotDeck
|
|
23
|
+
|
|
24
|
+
A G-code sender and editor for small CNC routers running **GRBL 1.1** on an
|
|
25
|
+
Arduino Uno. macOS desktop app, Python + PySide6.
|
|
26
|
+
|
|
27
|
+
PlotDeck connects over USB serial, lets you edit and preview a G-code file,
|
|
28
|
+
streams it with GRBL's character-counting flow control, and gives you manual
|
|
29
|
+
control (jog, home, zero, overrides) with live position and toolpath feedback.
|
|
30
|
+
|
|
31
|
+
**New here? Read [USER_MANUAL.md](USER_MANUAL.md), or open it inside the app
|
|
32
|
+
from the Manual toolbar button or Help → User Manual (F1).**
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Connection** — USB port discovery, 115200 baud default, GRBL banner
|
|
39
|
+
detection (the Uno resets when the port opens), 5 Hz status polling, and a
|
|
40
|
+
**Demo** mode that runs a simulated machine with no hardware.
|
|
41
|
+
- **Streaming** — 127-byte character-counting sender, pause/resume, safe stop
|
|
42
|
+
(feed hold + soft reset), start from a line, and GRBL check mode (`$C`) dry
|
|
43
|
+
runs.
|
|
44
|
+
- **Editor** — line numbers, G/M/axis/feed syntax colours, current-line and
|
|
45
|
+
executed-line markers, error markers, find/replace, go-to-line, toggle
|
|
46
|
+
comment, strip comments, renumber, and offset/scale/rotate transforms.
|
|
47
|
+
- **Visualizer** — 2D toolpath with executed (green) and remaining (dashed)
|
|
48
|
+
paths, live tool position, scroll/button zoom, pan, fit-to-view, bounding
|
|
49
|
+
box and time estimate. Optional 3D view with orbit and zoom when PyOpenGL
|
|
50
|
+
is installed.
|
|
51
|
+
- **Machine control** — jog pad with step chips and keyboard control, work
|
|
52
|
+
zeroing per axis, G54–G59 selection, homing, unlock, jog cancel, feed /
|
|
53
|
+
rapid / spindle overrides, spindle on/off, and a Z-probe wizard.
|
|
54
|
+
- **Safety workflows** — pre-flight summary (time, bounds, depth, tools,
|
|
55
|
+
warnings), `M6` interception with a tool-change pause, `error:N` pauses the
|
|
56
|
+
job instead of blindly continuing, alarm dialog with unlock/home recovery,
|
|
57
|
+
and automatic job-interrupted reporting on disconnect.
|
|
58
|
+
- **Settings** — editable `$$` table with descriptions, plus persistence of
|
|
59
|
+
recent files, port, theme and window layout via QSettings.
|
|
60
|
+
- **Help** — the full user manual is bundled and rendered in-app from the
|
|
61
|
+
**Manual** toolbar button or **Help → User Manual** (F1), with search and links.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- macOS (only platform tested), Python 3.10 or newer
|
|
66
|
+
- Arduino Uno with GRBL 1.1 and a CNC shield (for real hardware)
|
|
67
|
+
- Python packages: PySide6, pyserial, pyqtgraph, numpy
|
|
68
|
+
- Optional: PyOpenGL (3D view), PyInstaller (packaging)
|
|
69
|
+
|
|
70
|
+
## Setup
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bash scripts/setup_env.sh # creates .venv and installs .[dev,3d]
|
|
74
|
+
source .venv/bin/activate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or manually:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python3 -m venv .venv
|
|
81
|
+
source .venv/bin/activate
|
|
82
|
+
python -m pip install -e ".[dev,3d]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Run
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -m plotdeck
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pick a `/dev/cu.usbmodem*` port (or `/dev/cu.usbserial*` for CH340 boards) and
|
|
92
|
+
press **Connect**. Click **Demo** to explore the interface without hardware.
|
|
93
|
+
|
|
94
|
+
## Typical workflow
|
|
95
|
+
|
|
96
|
+
1. Connect and wait for the state badge to read **IDLE**.
|
|
97
|
+
2. Home the machine, jog to the workpiece origin, and zero X/Y (and Z, or use
|
|
98
|
+
**Machine → Probe Z…** with a touch plate).
|
|
99
|
+
3. Open a G-code file (⌘O or drag and drop). Check the toolpath preview.
|
|
100
|
+
4. Press **Run**. The pre-flight dialog shows the time estimate, bounding box,
|
|
101
|
+
tools and warnings; **Dry run** uses GRBL check mode.
|
|
102
|
+
5. Watch progress in the Job panel; the editor follows the executing line.
|
|
103
|
+
**Pause** sends feed hold, **Stop** (⌘.) sends feed hold then soft reset.
|
|
104
|
+
6. If GRBL returns `error:N`, the job pauses and asks whether to skip the line
|
|
105
|
+
or abort. Insert tool changes when the M6 dialog appears.
|
|
106
|
+
|
|
107
|
+
> **Tool changes:** PlotDeck pauses and prompts for `M6`, but it does not
|
|
108
|
+
> automatically stop a manually switched spindle or retract Z. Turn the spindle
|
|
109
|
+
> off and raise Z before reaching in, then re-probe Z if the tool length
|
|
110
|
+
> changed. Only an automatic spindle relay wired to GRBL can be stopped by
|
|
111
|
+
> GRBL itself.
|
|
112
|
+
|
|
113
|
+
## Keyboard shortcuts
|
|
114
|
+
|
|
115
|
+
| Action | Shortcut |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| Open / Save | ⌘O / ⌘S |
|
|
118
|
+
| Find / Go to line | ⌘F / ⌘L |
|
|
119
|
+
| Run / Run from cursor | ⌘R / ⌘⇧R |
|
|
120
|
+
| Pause / Resume | toolbar (feed hold / cycle start) |
|
|
121
|
+
| Stop | ⌘. |
|
|
122
|
+
| Soft reset | ⌘⇧. |
|
|
123
|
+
| Toggle comment | ⌘/ |
|
|
124
|
+
| Connect / Disconnect | ⌘K |
|
|
125
|
+
| Fit toolpath | ⌘0 (or `F` with the preview focused) |
|
|
126
|
+
| Jog | arrows (X/Y), PgUp/PgDn (Z), `[` `]` step, Space cancel |
|
|
127
|
+
|
|
128
|
+
Jog keys only act while the jog panel has focus. The console accepts commands
|
|
129
|
+
when idle; during a job line commands are refused (real-time commands such as
|
|
130
|
+
`?` or `!` still work from a serial terminal, but the app controls them).
|
|
131
|
+
|
|
132
|
+
## Tests
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
source .venv/bin/activate
|
|
136
|
+
QT_QPA_PLATFORM=offscreen pytest -q
|
|
137
|
+
ruff check src tests
|
|
138
|
+
black --check src tests
|
|
139
|
+
mypy
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The suite drives the core against an in-process GRBL simulator
|
|
143
|
+
(`tests/fake_grbl.py`) that enforces the 127-byte buffer, and includes headless
|
|
144
|
+
UI smoke tests (`pytest-qt`). CI runs the same checks on macOS.
|
|
145
|
+
|
|
146
|
+
## Packaging
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
bash packaging/build_app.sh
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The built `dist/PlotDeck.app` was launched headlessly during development to
|
|
153
|
+
confirm the bundle starts (PySide6, pyqtgraph and the Qt platform plugins are
|
|
154
|
+
collected; PyOpenGL is included when installed).
|
|
155
|
+
|
|
156
|
+
This produces `dist/PlotDeck.app` via `packaging/plotdeck.spec`, and a `.dmg`
|
|
157
|
+
when `create-dmg` is installed. For distribution outside your own Mac, sign and
|
|
158
|
+
notarize with your Apple Developer identity:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
codesign --deep --options runtime --sign "Developer ID Application: …" dist/PlotDeck.app
|
|
162
|
+
xcrun notarytool submit … --wait
|
|
163
|
+
xcrun stapler staple dist/PlotDeck.app
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Architecture
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
src/plotdeck/
|
|
170
|
+
├── core/ # no Qt imports; unit-testable headless
|
|
171
|
+
│ ├── transport.py # Transport ABC, SerialTransport thread, MockTransport
|
|
172
|
+
│ ├── grbl_protocol.py # real-time bytes, response classifier, error tables
|
|
173
|
+
│ ├── status.py # <...> status reports → MachineStatus
|
|
174
|
+
│ ├── gcode_parser.py # cleaning, tokenizing, toolpath + time estimate
|
|
175
|
+
│ ├── streamer.py # character-counting job state machine
|
|
176
|
+
│ └── controller.py # MachineController orchestration
|
|
177
|
+
├── ui/ # PySide6 widgets
|
|
178
|
+
│ ├── main_window.py # layout, menus, shortcuts, state-driven enablement
|
|
179
|
+
│ ├── editor.py # editor, gutter, highlighter, find bar
|
|
180
|
+
│ ├── console.py # colour-coded console + history
|
|
181
|
+
│ ├── status_panel.py # DRO, state badge, override sliders
|
|
182
|
+
│ ├── jog_panel.py # jog pad, zeroing, WCS
|
|
183
|
+
│ ├── visualizer.py # 2D/3D toolpath preview
|
|
184
|
+
│ ├── job_panel.py # progress and timing
|
|
185
|
+
│ ├── dialogs.py # pre-flight, tool change, probe, settings
|
|
186
|
+
│ └── theme.py # colour tokens and QSS
|
|
187
|
+
└── utils/ # settings (QSettings) and logging
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The UI never touches the serial port. Controller callbacks are marshalled to
|
|
191
|
+
the main thread through a small `QObject` signal bridge. The controller and
|
|
192
|
+
streamer are plain Python, so tests and CLI use do not need a Qt event loop.
|
|
193
|
+
|
|
194
|
+
## GRBL notes
|
|
195
|
+
|
|
196
|
+
- Buffer size is 127 usable bytes; lines are counted as `len(line) + 1`.
|
|
197
|
+
- Real-time bytes (`?`, `!`, `~`, `0x18`, `0x85`, override bytes) bypass the
|
|
198
|
+
queue and never produce `ok`.
|
|
199
|
+
- Manual commands are acknowledged separately from the job stream, so a `$C`
|
|
200
|
+
or `$H` ok cannot corrupt buffer accounting.
|
|
201
|
+
- `M6` is never forwarded (GRBL would return `error:20`); the streamer pauses,
|
|
202
|
+
the UI prompts, then the job resumes after the tool change.
|
|
203
|
+
- On soft reset the streamer clears its queue and pending bytes and the app
|
|
204
|
+
re-syncs from the next status report.
|
|
205
|
+
|
|
206
|
+
## Limitations / not implemented
|
|
207
|
+
|
|
208
|
+
- Only GRBL 1.1 is supported; Marlin/FluidNC are out of scope.
|
|
209
|
+
- No CAM: PlotDeck sends G-code produced elsewhere.
|
|
210
|
+
- No auto-reconnect, macros editor, tool/material library, or height-map
|
|
211
|
+
leveling (post-1.0 in the plan).
|
|
212
|
+
- The 3D view requires `PyOpenGL` (`pip install -e ".[3d]"`); without it the
|
|
213
|
+
3D button reports that the view is unavailable.
|
|
214
|
+
- Clicking a toolpath segment does not jump to its line yet.
|
|
215
|
+
- Find/replace is literal text, not regular expressions; hover documentation
|
|
216
|
+
and G/M-code autocomplete are not implemented.
|
|
217
|
+
- Continuous (hold-to-jog) motion and saved macros are not implemented.
|
|
218
|
+
- The app icon is optional; drop `packaging/icon.icns` in place before building
|
|
219
|
+
if you want one.
|
|
220
|
+
|
|
221
|
+
## Safety
|
|
222
|
+
|
|
223
|
+
This software controls a machine that can injure you. Wear eye protection,
|
|
224
|
+
secure the workpiece, know where the power switch is, and keep a hand near it
|
|
225
|
+
for the first cut. A software stop is **not** an emergency stop — wire a
|
|
226
|
+
hardware e-stop that cuts motor and spindle power. Test with the tool
|
|
227
|
+
disconnected or raised before cutting material.
|
plotdeck-1.0.0/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# PlotDeck
|
|
2
|
+
|
|
3
|
+
A G-code sender and editor for small CNC routers running **GRBL 1.1** on an
|
|
4
|
+
Arduino Uno. macOS desktop app, Python + PySide6.
|
|
5
|
+
|
|
6
|
+
PlotDeck connects over USB serial, lets you edit and preview a G-code file,
|
|
7
|
+
streams it with GRBL's character-counting flow control, and gives you manual
|
|
8
|
+
control (jog, home, zero, overrides) with live position and toolpath feedback.
|
|
9
|
+
|
|
10
|
+
**New here? Read [USER_MANUAL.md](USER_MANUAL.md), or open it inside the app
|
|
11
|
+
from the Manual toolbar button or Help → User Manual (F1).**
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Connection** — USB port discovery, 115200 baud default, GRBL banner
|
|
18
|
+
detection (the Uno resets when the port opens), 5 Hz status polling, and a
|
|
19
|
+
**Demo** mode that runs a simulated machine with no hardware.
|
|
20
|
+
- **Streaming** — 127-byte character-counting sender, pause/resume, safe stop
|
|
21
|
+
(feed hold + soft reset), start from a line, and GRBL check mode (`$C`) dry
|
|
22
|
+
runs.
|
|
23
|
+
- **Editor** — line numbers, G/M/axis/feed syntax colours, current-line and
|
|
24
|
+
executed-line markers, error markers, find/replace, go-to-line, toggle
|
|
25
|
+
comment, strip comments, renumber, and offset/scale/rotate transforms.
|
|
26
|
+
- **Visualizer** — 2D toolpath with executed (green) and remaining (dashed)
|
|
27
|
+
paths, live tool position, scroll/button zoom, pan, fit-to-view, bounding
|
|
28
|
+
box and time estimate. Optional 3D view with orbit and zoom when PyOpenGL
|
|
29
|
+
is installed.
|
|
30
|
+
- **Machine control** — jog pad with step chips and keyboard control, work
|
|
31
|
+
zeroing per axis, G54–G59 selection, homing, unlock, jog cancel, feed /
|
|
32
|
+
rapid / spindle overrides, spindle on/off, and a Z-probe wizard.
|
|
33
|
+
- **Safety workflows** — pre-flight summary (time, bounds, depth, tools,
|
|
34
|
+
warnings), `M6` interception with a tool-change pause, `error:N` pauses the
|
|
35
|
+
job instead of blindly continuing, alarm dialog with unlock/home recovery,
|
|
36
|
+
and automatic job-interrupted reporting on disconnect.
|
|
37
|
+
- **Settings** — editable `$$` table with descriptions, plus persistence of
|
|
38
|
+
recent files, port, theme and window layout via QSettings.
|
|
39
|
+
- **Help** — the full user manual is bundled and rendered in-app from the
|
|
40
|
+
**Manual** toolbar button or **Help → User Manual** (F1), with search and links.
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- macOS (only platform tested), Python 3.10 or newer
|
|
45
|
+
- Arduino Uno with GRBL 1.1 and a CNC shield (for real hardware)
|
|
46
|
+
- Python packages: PySide6, pyserial, pyqtgraph, numpy
|
|
47
|
+
- Optional: PyOpenGL (3D view), PyInstaller (packaging)
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bash scripts/setup_env.sh # creates .venv and installs .[dev,3d]
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or manually:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python3 -m venv .venv
|
|
60
|
+
source .venv/bin/activate
|
|
61
|
+
python -m pip install -e ".[dev,3d]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Run
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m plotdeck
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pick a `/dev/cu.usbmodem*` port (or `/dev/cu.usbserial*` for CH340 boards) and
|
|
71
|
+
press **Connect**. Click **Demo** to explore the interface without hardware.
|
|
72
|
+
|
|
73
|
+
## Typical workflow
|
|
74
|
+
|
|
75
|
+
1. Connect and wait for the state badge to read **IDLE**.
|
|
76
|
+
2. Home the machine, jog to the workpiece origin, and zero X/Y (and Z, or use
|
|
77
|
+
**Machine → Probe Z…** with a touch plate).
|
|
78
|
+
3. Open a G-code file (⌘O or drag and drop). Check the toolpath preview.
|
|
79
|
+
4. Press **Run**. The pre-flight dialog shows the time estimate, bounding box,
|
|
80
|
+
tools and warnings; **Dry run** uses GRBL check mode.
|
|
81
|
+
5. Watch progress in the Job panel; the editor follows the executing line.
|
|
82
|
+
**Pause** sends feed hold, **Stop** (⌘.) sends feed hold then soft reset.
|
|
83
|
+
6. If GRBL returns `error:N`, the job pauses and asks whether to skip the line
|
|
84
|
+
or abort. Insert tool changes when the M6 dialog appears.
|
|
85
|
+
|
|
86
|
+
> **Tool changes:** PlotDeck pauses and prompts for `M6`, but it does not
|
|
87
|
+
> automatically stop a manually switched spindle or retract Z. Turn the spindle
|
|
88
|
+
> off and raise Z before reaching in, then re-probe Z if the tool length
|
|
89
|
+
> changed. Only an automatic spindle relay wired to GRBL can be stopped by
|
|
90
|
+
> GRBL itself.
|
|
91
|
+
|
|
92
|
+
## Keyboard shortcuts
|
|
93
|
+
|
|
94
|
+
| Action | Shortcut |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| Open / Save | ⌘O / ⌘S |
|
|
97
|
+
| Find / Go to line | ⌘F / ⌘L |
|
|
98
|
+
| Run / Run from cursor | ⌘R / ⌘⇧R |
|
|
99
|
+
| Pause / Resume | toolbar (feed hold / cycle start) |
|
|
100
|
+
| Stop | ⌘. |
|
|
101
|
+
| Soft reset | ⌘⇧. |
|
|
102
|
+
| Toggle comment | ⌘/ |
|
|
103
|
+
| Connect / Disconnect | ⌘K |
|
|
104
|
+
| Fit toolpath | ⌘0 (or `F` with the preview focused) |
|
|
105
|
+
| Jog | arrows (X/Y), PgUp/PgDn (Z), `[` `]` step, Space cancel |
|
|
106
|
+
|
|
107
|
+
Jog keys only act while the jog panel has focus. The console accepts commands
|
|
108
|
+
when idle; during a job line commands are refused (real-time commands such as
|
|
109
|
+
`?` or `!` still work from a serial terminal, but the app controls them).
|
|
110
|
+
|
|
111
|
+
## Tests
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
source .venv/bin/activate
|
|
115
|
+
QT_QPA_PLATFORM=offscreen pytest -q
|
|
116
|
+
ruff check src tests
|
|
117
|
+
black --check src tests
|
|
118
|
+
mypy
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The suite drives the core against an in-process GRBL simulator
|
|
122
|
+
(`tests/fake_grbl.py`) that enforces the 127-byte buffer, and includes headless
|
|
123
|
+
UI smoke tests (`pytest-qt`). CI runs the same checks on macOS.
|
|
124
|
+
|
|
125
|
+
## Packaging
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bash packaging/build_app.sh
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The built `dist/PlotDeck.app` was launched headlessly during development to
|
|
132
|
+
confirm the bundle starts (PySide6, pyqtgraph and the Qt platform plugins are
|
|
133
|
+
collected; PyOpenGL is included when installed).
|
|
134
|
+
|
|
135
|
+
This produces `dist/PlotDeck.app` via `packaging/plotdeck.spec`, and a `.dmg`
|
|
136
|
+
when `create-dmg` is installed. For distribution outside your own Mac, sign and
|
|
137
|
+
notarize with your Apple Developer identity:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
codesign --deep --options runtime --sign "Developer ID Application: …" dist/PlotDeck.app
|
|
141
|
+
xcrun notarytool submit … --wait
|
|
142
|
+
xcrun stapler staple dist/PlotDeck.app
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Architecture
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
src/plotdeck/
|
|
149
|
+
├── core/ # no Qt imports; unit-testable headless
|
|
150
|
+
│ ├── transport.py # Transport ABC, SerialTransport thread, MockTransport
|
|
151
|
+
│ ├── grbl_protocol.py # real-time bytes, response classifier, error tables
|
|
152
|
+
│ ├── status.py # <...> status reports → MachineStatus
|
|
153
|
+
│ ├── gcode_parser.py # cleaning, tokenizing, toolpath + time estimate
|
|
154
|
+
│ ├── streamer.py # character-counting job state machine
|
|
155
|
+
│ └── controller.py # MachineController orchestration
|
|
156
|
+
├── ui/ # PySide6 widgets
|
|
157
|
+
│ ├── main_window.py # layout, menus, shortcuts, state-driven enablement
|
|
158
|
+
│ ├── editor.py # editor, gutter, highlighter, find bar
|
|
159
|
+
│ ├── console.py # colour-coded console + history
|
|
160
|
+
│ ├── status_panel.py # DRO, state badge, override sliders
|
|
161
|
+
│ ├── jog_panel.py # jog pad, zeroing, WCS
|
|
162
|
+
│ ├── visualizer.py # 2D/3D toolpath preview
|
|
163
|
+
│ ├── job_panel.py # progress and timing
|
|
164
|
+
│ ├── dialogs.py # pre-flight, tool change, probe, settings
|
|
165
|
+
│ └── theme.py # colour tokens and QSS
|
|
166
|
+
└── utils/ # settings (QSettings) and logging
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The UI never touches the serial port. Controller callbacks are marshalled to
|
|
170
|
+
the main thread through a small `QObject` signal bridge. The controller and
|
|
171
|
+
streamer are plain Python, so tests and CLI use do not need a Qt event loop.
|
|
172
|
+
|
|
173
|
+
## GRBL notes
|
|
174
|
+
|
|
175
|
+
- Buffer size is 127 usable bytes; lines are counted as `len(line) + 1`.
|
|
176
|
+
- Real-time bytes (`?`, `!`, `~`, `0x18`, `0x85`, override bytes) bypass the
|
|
177
|
+
queue and never produce `ok`.
|
|
178
|
+
- Manual commands are acknowledged separately from the job stream, so a `$C`
|
|
179
|
+
or `$H` ok cannot corrupt buffer accounting.
|
|
180
|
+
- `M6` is never forwarded (GRBL would return `error:20`); the streamer pauses,
|
|
181
|
+
the UI prompts, then the job resumes after the tool change.
|
|
182
|
+
- On soft reset the streamer clears its queue and pending bytes and the app
|
|
183
|
+
re-syncs from the next status report.
|
|
184
|
+
|
|
185
|
+
## Limitations / not implemented
|
|
186
|
+
|
|
187
|
+
- Only GRBL 1.1 is supported; Marlin/FluidNC are out of scope.
|
|
188
|
+
- No CAM: PlotDeck sends G-code produced elsewhere.
|
|
189
|
+
- No auto-reconnect, macros editor, tool/material library, or height-map
|
|
190
|
+
leveling (post-1.0 in the plan).
|
|
191
|
+
- The 3D view requires `PyOpenGL` (`pip install -e ".[3d]"`); without it the
|
|
192
|
+
3D button reports that the view is unavailable.
|
|
193
|
+
- Clicking a toolpath segment does not jump to its line yet.
|
|
194
|
+
- Find/replace is literal text, not regular expressions; hover documentation
|
|
195
|
+
and G/M-code autocomplete are not implemented.
|
|
196
|
+
- Continuous (hold-to-jog) motion and saved macros are not implemented.
|
|
197
|
+
- The app icon is optional; drop `packaging/icon.icns` in place before building
|
|
198
|
+
if you want one.
|
|
199
|
+
|
|
200
|
+
## Safety
|
|
201
|
+
|
|
202
|
+
This software controls a machine that can injure you. Wear eye protection,
|
|
203
|
+
secure the workpiece, know where the power switch is, and keep a hand near it
|
|
204
|
+
for the first cut. A software stop is **not** an emergency stop — wire a
|
|
205
|
+
hardware e-stop that cuts motor and spindle power. Test with the tool
|
|
206
|
+
disconnected or raised before cutting material.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plotdeck"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "G-code sender and editor for GRBL-based CNC routers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"PySide6>=6.6",
|
|
14
|
+
"pyserial>=3.5",
|
|
15
|
+
"pyqtgraph>=0.13",
|
|
16
|
+
"numpy>=1.24",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
3d = ["PyOpenGL>=3.1"]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"pytest-qt>=4.4",
|
|
24
|
+
"ruff>=0.5",
|
|
25
|
+
"black>=24",
|
|
26
|
+
"mypy>=1.10",
|
|
27
|
+
"pyinstaller>=6",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
plotdeck = "plotdeck.app:main"
|
|
32
|
+
|
|
33
|
+
[project.gui-scripts]
|
|
34
|
+
plotdeck-gui = "plotdeck.app:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
plotdeck = ["ui/resources/*"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
pythonpath = ["src"]
|
|
45
|
+
qt_api = "pyside6"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
src = ["src", "tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
54
|
+
ignore = ["E501"]
|
|
55
|
+
|
|
56
|
+
[tool.black]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = ["py310"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.10"
|
|
62
|
+
ignore_missing_imports = true
|
|
63
|
+
check_untyped_defs = true
|
|
64
|
+
no_implicit_optional = true
|
|
65
|
+
warn_unused_ignores = true
|
|
66
|
+
files = ["src"]
|
plotdeck-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""QApplication bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from PySide6.QtWidgets import QApplication, QMessageBox
|
|
10
|
+
|
|
11
|
+
from . import APP_NAME, __version__
|
|
12
|
+
from .ui.main_window import MainWindow
|
|
13
|
+
from .utils.logging_setup import setup_logging
|
|
14
|
+
from .utils.settings import AppSettings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _install_exception_hook(logger: logging.Logger) -> None:
|
|
18
|
+
def hook(exc_type, exc_value, exc_tb): # noqa: ANN001
|
|
19
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
20
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
21
|
+
return
|
|
22
|
+
trace = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
23
|
+
logger.error("Unhandled exception:\n%s", trace)
|
|
24
|
+
app = QApplication.instance()
|
|
25
|
+
if app is not None:
|
|
26
|
+
QMessageBox.critical(
|
|
27
|
+
None,
|
|
28
|
+
"PlotDeck crashed",
|
|
29
|
+
"An unexpected error occurred. The traceback was written to "
|
|
30
|
+
"~/Library/Logs/PlotDeck/plotdeck.log.\n\n"
|
|
31
|
+
f"{exc_type.__name__}: {exc_value}",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
sys.excepthook = hook
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> int:
|
|
38
|
+
logger = setup_logging()
|
|
39
|
+
logger.info("Starting %s %s", APP_NAME, __version__)
|
|
40
|
+
_install_exception_hook(logger)
|
|
41
|
+
|
|
42
|
+
app = QApplication(argv if argv is not None else sys.argv)
|
|
43
|
+
app.setApplicationName(APP_NAME)
|
|
44
|
+
app.setApplicationDisplayName(APP_NAME)
|
|
45
|
+
app.setOrganizationName("PlotDeck")
|
|
46
|
+
app.setApplicationVersion(__version__)
|
|
47
|
+
|
|
48
|
+
settings = AppSettings()
|
|
49
|
+
window = MainWindow(app_settings=settings)
|
|
50
|
+
window.show()
|
|
51
|
+
return app.exec()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|