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.
Files changed (40) hide show
  1. plotdeck-1.0.0/PKG-INFO +227 -0
  2. plotdeck-1.0.0/README.md +206 -0
  3. plotdeck-1.0.0/pyproject.toml +66 -0
  4. plotdeck-1.0.0/setup.cfg +4 -0
  5. plotdeck-1.0.0/src/plotdeck/__init__.py +4 -0
  6. plotdeck-1.0.0/src/plotdeck/__main__.py +6 -0
  7. plotdeck-1.0.0/src/plotdeck/app.py +55 -0
  8. plotdeck-1.0.0/src/plotdeck/core/__init__.py +4 -0
  9. plotdeck-1.0.0/src/plotdeck/core/controller.py +561 -0
  10. plotdeck-1.0.0/src/plotdeck/core/gcode_parser.py +404 -0
  11. plotdeck-1.0.0/src/plotdeck/core/grbl_protocol.py +278 -0
  12. plotdeck-1.0.0/src/plotdeck/core/status.py +148 -0
  13. plotdeck-1.0.0/src/plotdeck/core/streamer.py +328 -0
  14. plotdeck-1.0.0/src/plotdeck/core/transport.py +375 -0
  15. plotdeck-1.0.0/src/plotdeck/ui/__init__.py +1 -0
  16. plotdeck-1.0.0/src/plotdeck/ui/connection_panel.py +139 -0
  17. plotdeck-1.0.0/src/plotdeck/ui/console.py +171 -0
  18. plotdeck-1.0.0/src/plotdeck/ui/dialogs.py +317 -0
  19. plotdeck-1.0.0/src/plotdeck/ui/editor.py +544 -0
  20. plotdeck-1.0.0/src/plotdeck/ui/job_panel.py +130 -0
  21. plotdeck-1.0.0/src/plotdeck/ui/jog_panel.py +222 -0
  22. plotdeck-1.0.0/src/plotdeck/ui/main_window.py +1116 -0
  23. plotdeck-1.0.0/src/plotdeck/ui/manual.py +156 -0
  24. plotdeck-1.0.0/src/plotdeck/ui/status_panel.py +244 -0
  25. plotdeck-1.0.0/src/plotdeck/ui/theme.py +329 -0
  26. plotdeck-1.0.0/src/plotdeck/ui/visualizer.py +374 -0
  27. plotdeck-1.0.0/src/plotdeck/utils/__init__.py +1 -0
  28. plotdeck-1.0.0/src/plotdeck/utils/logging_setup.py +42 -0
  29. plotdeck-1.0.0/src/plotdeck/utils/settings.py +86 -0
  30. plotdeck-1.0.0/src/plotdeck.egg-info/PKG-INFO +227 -0
  31. plotdeck-1.0.0/src/plotdeck.egg-info/SOURCES.txt +38 -0
  32. plotdeck-1.0.0/src/plotdeck.egg-info/dependency_links.txt +1 -0
  33. plotdeck-1.0.0/src/plotdeck.egg-info/entry_points.txt +5 -0
  34. plotdeck-1.0.0/src/plotdeck.egg-info/requires.txt +15 -0
  35. plotdeck-1.0.0/src/plotdeck.egg-info/top_level.txt +1 -0
  36. plotdeck-1.0.0/tests/test_controller.py +218 -0
  37. plotdeck-1.0.0/tests/test_gcode_parser.py +132 -0
  38. plotdeck-1.0.0/tests/test_status.py +69 -0
  39. plotdeck-1.0.0/tests/test_streamer.py +186 -0
  40. plotdeck-1.0.0/tests/test_ui_smoke.py +178 -0
@@ -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
+ ![PlotDeck screenshot](images/PlotDeck.png)
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.
@@ -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
+ ![PlotDeck screenshot](images/PlotDeck.png)
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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """PlotDeck - a G-code sender and editor for GRBL-controlled CNC routers."""
2
+
3
+ __version__ = "1.0.0"
4
+ APP_NAME = "PlotDeck"
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m plotdeck``."""
2
+
3
+ from .app import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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())
@@ -0,0 +1,4 @@
1
+ """Core layer: transport, protocol, streaming and controller logic.
2
+
3
+ Nothing in this package imports Qt, so it can be tested headless.
4
+ """