flashforge-python-api 1.0.0__py3-none-any.whl
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.
- flashforge/__init__.py +188 -0
- flashforge/api/__init__.py +17 -0
- flashforge/api/constants/__init__.py +10 -0
- flashforge/api/constants/commands.py +10 -0
- flashforge/api/constants/endpoints.py +11 -0
- flashforge/api/controls/__init__.py +16 -0
- flashforge/api/controls/control.py +357 -0
- flashforge/api/controls/files.py +171 -0
- flashforge/api/controls/info.py +292 -0
- flashforge/api/controls/job_control.py +561 -0
- flashforge/api/controls/temp_control.py +89 -0
- flashforge/api/filament/__init__.py +7 -0
- flashforge/api/filament/filament.py +39 -0
- flashforge/api/misc/__init__.py +8 -0
- flashforge/api/misc/scientific_notation.py +28 -0
- flashforge/api/misc/temperature.py +42 -0
- flashforge/api/network/__init__.py +10 -0
- flashforge/api/network/fnet_code.py +15 -0
- flashforge/api/network/utils.py +55 -0
- flashforge/client.py +381 -0
- flashforge/discovery/__init__.py +12 -0
- flashforge/discovery/discovery.py +388 -0
- flashforge/models/__init__.py +50 -0
- flashforge/models/machine_info.py +247 -0
- flashforge/models/responses.py +123 -0
- flashforge/tcp/__init__.py +41 -0
- flashforge/tcp/ff_client.py +587 -0
- flashforge/tcp/gcode/__init__.py +11 -0
- flashforge/tcp/gcode/gcode_controller.py +296 -0
- flashforge/tcp/gcode/gcodes.py +93 -0
- flashforge/tcp/parsers/__init__.py +27 -0
- flashforge/tcp/parsers/endstop_status.py +273 -0
- flashforge/tcp/parsers/location_info.py +68 -0
- flashforge/tcp/parsers/print_status.py +182 -0
- flashforge/tcp/parsers/printer_info.py +154 -0
- flashforge/tcp/parsers/temp_info.py +217 -0
- flashforge/tcp/parsers/thumbnail_info.py +242 -0
- flashforge/tcp/tcp_client.py +448 -0
- flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
- flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
- flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
- flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
- flashforge_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G-code controller for FlashForge 3D printers.
|
|
3
|
+
|
|
4
|
+
This module provides high-level G-code command methods that build upon
|
|
5
|
+
the basic TCP communication provided by FlashForgeTcpClient.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .gcodes import GCodes
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..ff_client import FlashForgeClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GCodeController:
|
|
21
|
+
"""
|
|
22
|
+
Controller for sending specific G-code commands to FlashForge printers.
|
|
23
|
+
|
|
24
|
+
This class provides convenient methods for common printer operations
|
|
25
|
+
like LED control, job management, movement, and temperature control.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: 'FlashForgeClient') -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize the G-code controller.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
client: The FlashForgeClient instance for sending commands
|
|
34
|
+
"""
|
|
35
|
+
self.client = client
|
|
36
|
+
|
|
37
|
+
async def led_on(self) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Turn the printer's LED lights on.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if the command was successful, False otherwise
|
|
43
|
+
"""
|
|
44
|
+
return await self.client.send_cmd_ok(GCodes.CMD_LED_ON)
|
|
45
|
+
|
|
46
|
+
async def led_off(self) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Turn the printer's LED lights off.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if the command was successful, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
return await self.client.send_cmd_ok(GCodes.CMD_LED_OFF)
|
|
54
|
+
|
|
55
|
+
async def pause_job(self) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Pause the current print job.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if the command was successful, False otherwise
|
|
61
|
+
"""
|
|
62
|
+
return await self.client.send_cmd_ok(GCodes.CMD_PAUSE_PRINT)
|
|
63
|
+
|
|
64
|
+
async def resume_job(self) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Resume a paused print job.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the command was successful, False otherwise
|
|
70
|
+
"""
|
|
71
|
+
return await self.client.send_cmd_ok(GCodes.CMD_RESUME_PRINT)
|
|
72
|
+
|
|
73
|
+
async def stop_job(self) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Stop the current print job.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the command was successful, False otherwise
|
|
79
|
+
"""
|
|
80
|
+
return await self.client.send_cmd_ok(GCodes.CMD_STOP_PRINT)
|
|
81
|
+
|
|
82
|
+
async def start_job(self, filename: str) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Start a print job from a file stored on the printer.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
filename: The name of the file to print (typically without path)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if the command was successful, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
cmd = GCodes.CMD_START_PRINT.replace("%%filename%%", filename)
|
|
93
|
+
return await self.client.send_cmd_ok(cmd)
|
|
94
|
+
|
|
95
|
+
async def home(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Home all axes (X, Y, Z) of the printer.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if the command was successful, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
return await self.client.send_cmd_ok(GCodes.CMD_HOME_AXES)
|
|
103
|
+
|
|
104
|
+
async def rapid_home(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Perform a rapid homing of all axes.
|
|
107
|
+
|
|
108
|
+
Note: This uses the same command as regular homing in the current implementation.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if the command was successful, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
# In the TypeScript implementation, this is the same as home()
|
|
114
|
+
return await self.home()
|
|
115
|
+
|
|
116
|
+
async def move(self, x: float, y: float, z: float, feedrate: int) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Move the extruder to a specified X, Y, Z position.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
x: The target X coordinate
|
|
122
|
+
y: The target Y coordinate
|
|
123
|
+
z: The target Z coordinate
|
|
124
|
+
feedrate: The feedrate for the movement in mm/min
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the command was successful, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
cmd = f"~G1 X{x} Y{y} Z{z} F{feedrate}"
|
|
130
|
+
return await self.client.send_cmd_ok(cmd)
|
|
131
|
+
|
|
132
|
+
async def move_extruder(self, x: float, y: float, feedrate: int) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Move the extruder to a specified X, Y position.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
x: The target X coordinate
|
|
138
|
+
y: The target Y coordinate
|
|
139
|
+
feedrate: The feedrate for the movement in mm/min
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if the command was successful, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
cmd = f"~G1 X{x} Y{y} F{feedrate}"
|
|
145
|
+
return await self.client.send_cmd_ok(cmd)
|
|
146
|
+
|
|
147
|
+
async def extrude(self, length: float, feedrate: int = 450) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Command the extruder to extrude a specific length of filament.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
length: The length of filament to extrude in millimeters
|
|
153
|
+
feedrate: The feedrate for extrusion in mm/min (default: 450)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if the command was successful, False otherwise
|
|
157
|
+
"""
|
|
158
|
+
cmd = f"~G1 E{length} F{feedrate}"
|
|
159
|
+
return await self.client.send_cmd_ok(cmd)
|
|
160
|
+
|
|
161
|
+
async def set_extruder_temp(self, temp: int, wait_for: bool = False) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Set the target temperature for the extruder.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
temp: The target temperature in Celsius
|
|
167
|
+
wait_for: If True, wait until the target temperature is reached
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if the command was successful, False otherwise
|
|
171
|
+
"""
|
|
172
|
+
if wait_for:
|
|
173
|
+
cmd = f"{GCodes.WAIT_FOR_HOTEND_TEMP} S{temp}"
|
|
174
|
+
ok = await self.client.send_cmd_ok(cmd)
|
|
175
|
+
if not ok:
|
|
176
|
+
return False
|
|
177
|
+
# Wait for temperature to be reached
|
|
178
|
+
return await self.wait_for_extruder_temp(temp)
|
|
179
|
+
else:
|
|
180
|
+
cmd = f"~M104 S{temp}"
|
|
181
|
+
return await self.client.send_cmd_ok(cmd)
|
|
182
|
+
|
|
183
|
+
async def set_bed_temp(self, temp: int, wait_for: bool = False) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Set the target temperature for the print bed.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
temp: The target temperature in Celsius
|
|
189
|
+
wait_for: If True, wait until the target temperature is reached
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if the command was successful, False otherwise
|
|
193
|
+
"""
|
|
194
|
+
if wait_for:
|
|
195
|
+
cmd = f"{GCodes.WAIT_FOR_BED_TEMP} S{temp}"
|
|
196
|
+
ok = await self.client.send_cmd_ok(cmd)
|
|
197
|
+
if not ok:
|
|
198
|
+
return False
|
|
199
|
+
# Wait for temperature to be reached
|
|
200
|
+
return await self.wait_for_bed_temp(temp)
|
|
201
|
+
else:
|
|
202
|
+
cmd = f"~M140 S{temp}"
|
|
203
|
+
return await self.client.send_cmd_ok(cmd)
|
|
204
|
+
|
|
205
|
+
async def cancel_extruder_temp(self) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Cancel extruder heating and set its target temperature to 0.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if the command was successful, False otherwise
|
|
211
|
+
"""
|
|
212
|
+
cmd = "~M104 S0"
|
|
213
|
+
ok = await self.client.send_cmd_ok(cmd)
|
|
214
|
+
return ok
|
|
215
|
+
|
|
216
|
+
async def cancel_bed_temp(self, wait_for_cool: bool = False) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Cancel print bed heating and set its target temperature to 0.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
wait_for_cool: If True, wait for the bed to cool down after canceling
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if the command was successful, False otherwise
|
|
225
|
+
"""
|
|
226
|
+
cmd = "~M140 S0"
|
|
227
|
+
ok = await self.client.send_cmd_ok(cmd)
|
|
228
|
+
if not ok:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
if wait_for_cool:
|
|
232
|
+
# Wait for bed to cool down to a reasonable temperature
|
|
233
|
+
return await self.wait_for_bed_temp(40, cooling=True)
|
|
234
|
+
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
async def wait_for_bed_temp(self, target_temp: int, cooling: bool = False, timeout: int = 600) -> bool:
|
|
238
|
+
"""
|
|
239
|
+
Wait for the bed temperature to reach the target.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
target_temp: The target temperature to wait for
|
|
243
|
+
cooling: If True, wait for temperature to drop below target; otherwise wait to reach target
|
|
244
|
+
timeout: Maximum time to wait in seconds (default: 600)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if target temperature was reached, False on timeout or error
|
|
248
|
+
"""
|
|
249
|
+
start_time = asyncio.get_event_loop().time()
|
|
250
|
+
timeout_time = start_time + timeout
|
|
251
|
+
|
|
252
|
+
while asyncio.get_event_loop().time() < timeout_time:
|
|
253
|
+
temp_info = await self.client.get_temp_info()
|
|
254
|
+
if temp_info:
|
|
255
|
+
bed_temp = temp_info.get_bed_temp()
|
|
256
|
+
if bed_temp:
|
|
257
|
+
current_temp = bed_temp.get_current()
|
|
258
|
+
if cooling:
|
|
259
|
+
if current_temp <= target_temp:
|
|
260
|
+
return True
|
|
261
|
+
else:
|
|
262
|
+
if abs(current_temp - target_temp) <= 2: # Within 2°C tolerance
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
await asyncio.sleep(5) # Check every 5 seconds
|
|
266
|
+
|
|
267
|
+
logger.warning(f"Timeout waiting for bed temperature {'cooling to' if cooling else 'heating to'} {target_temp}°C")
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
async def wait_for_extruder_temp(self, target_temp: int, timeout: int = 600) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Wait for the extruder temperature to reach the target.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
target_temp: The target temperature to wait for
|
|
276
|
+
timeout: Maximum time to wait in seconds (default: 600)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if target temperature was reached, False on timeout or error
|
|
280
|
+
"""
|
|
281
|
+
start_time = asyncio.get_event_loop().time()
|
|
282
|
+
timeout_time = start_time + timeout
|
|
283
|
+
|
|
284
|
+
while asyncio.get_event_loop().time() < timeout_time:
|
|
285
|
+
temp_info = await self.client.get_temp_info()
|
|
286
|
+
if temp_info:
|
|
287
|
+
extruder_temp = temp_info.get_extruder_temp()
|
|
288
|
+
if extruder_temp:
|
|
289
|
+
current_temp = extruder_temp.get_current()
|
|
290
|
+
if abs(current_temp - target_temp) <= 2: # Within 2°C tolerance
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
await asyncio.sleep(5) # Check every 5 seconds
|
|
294
|
+
|
|
295
|
+
logger.warning(f"Timeout waiting for extruder temperature to reach {target_temp}°C")
|
|
296
|
+
return False
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G-code and M-code command constants for FlashForge 3D printers.
|
|
3
|
+
|
|
4
|
+
This module defines the standard commands used for TCP communication with FlashForge printers.
|
|
5
|
+
The '~' prefix is characteristic of FlashForge's TCP command protocol.
|
|
6
|
+
Placeholders like '%%filename%%' are intended to be replaced with actual values before sending.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GCodes:
|
|
13
|
+
"""
|
|
14
|
+
Collection of G-code and M-code command strings for FlashForge 3D printers.
|
|
15
|
+
|
|
16
|
+
All commands use the FlashForge TCP protocol with '~' prefix.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Authentication and session control
|
|
20
|
+
CMD_LOGIN: Final[str] = "~M601 S1"
|
|
21
|
+
"""Command to initiate a control session with the printer (login)."""
|
|
22
|
+
|
|
23
|
+
CMD_LOGOUT: Final[str] = "~M602"
|
|
24
|
+
"""Command to terminate a control session with the printer (logout)."""
|
|
25
|
+
|
|
26
|
+
# Emergency control
|
|
27
|
+
CMD_EMERGENCY_STOP: Final[str] = "~M112"
|
|
28
|
+
"""Command for an emergency stop of all printer activity."""
|
|
29
|
+
|
|
30
|
+
# Status queries
|
|
31
|
+
CMD_PRINT_STATUS: Final[str] = "~M27"
|
|
32
|
+
"""Command to request the current print job status."""
|
|
33
|
+
|
|
34
|
+
CMD_ENDSTOP_INFO: Final[str] = "~M119"
|
|
35
|
+
"""Command to request the status of the printer's endstops."""
|
|
36
|
+
|
|
37
|
+
CMD_INFO_STATUS: Final[str] = "~M115"
|
|
38
|
+
"""Command to request general printer information, including firmware version."""
|
|
39
|
+
|
|
40
|
+
CMD_INFO_XYZAB: Final[str] = "~M114"
|
|
41
|
+
"""Command to request the current X, Y, Z, A, B coordinates of the print head."""
|
|
42
|
+
|
|
43
|
+
CMD_TEMP: Final[str] = "~M105"
|
|
44
|
+
"""Command to request current temperatures (extruder, bed)."""
|
|
45
|
+
|
|
46
|
+
# LED control
|
|
47
|
+
CMD_LED_ON: Final[str] = "~M146 r255 g255 b255 F0"
|
|
48
|
+
"""Command to turn the printer's LED lights on (full white)."""
|
|
49
|
+
|
|
50
|
+
CMD_LED_OFF: Final[str] = "~M146 r0 g0 b0 F0"
|
|
51
|
+
"""Command to turn the printer's LED lights off."""
|
|
52
|
+
|
|
53
|
+
# Filament runout sensor
|
|
54
|
+
CMD_RUNOUT_SENSOR_ON: Final[str] = "~M405"
|
|
55
|
+
"""Command to enable the filament runout sensor."""
|
|
56
|
+
|
|
57
|
+
CMD_RUNOUT_SENSOR_OFF: Final[str] = "~M406"
|
|
58
|
+
"""Command to disable the filament runout sensor."""
|
|
59
|
+
|
|
60
|
+
# File operations
|
|
61
|
+
CMD_LIST_LOCAL_FILES: Final[str] = "~M661"
|
|
62
|
+
"""Command to list files stored locally on the printer."""
|
|
63
|
+
|
|
64
|
+
CMD_GET_THUMBNAIL: Final[str] = "~M662"
|
|
65
|
+
"""Command to retrieve a thumbnail image for a specified G-code file."""
|
|
66
|
+
|
|
67
|
+
# Camera control
|
|
68
|
+
TAKE_PICTURE: Final[str] = "~M240"
|
|
69
|
+
"""Command to instruct the printer to take a picture with its camera, if equipped."""
|
|
70
|
+
|
|
71
|
+
# Movement and homing
|
|
72
|
+
CMD_HOME_AXES: Final[str] = "~G28"
|
|
73
|
+
"""Command to home all printer axes (X, Y, Z)."""
|
|
74
|
+
|
|
75
|
+
# Print job control
|
|
76
|
+
CMD_START_PRINT: Final[str] = "~M23 0:/user/%%filename%%"
|
|
77
|
+
"""Command to select a file for printing. %%filename%% should be replaced with actual file path."""
|
|
78
|
+
|
|
79
|
+
CMD_PAUSE_PRINT: Final[str] = "~M25"
|
|
80
|
+
"""Command to pause the current print job."""
|
|
81
|
+
|
|
82
|
+
CMD_RESUME_PRINT: Final[str] = "~M24"
|
|
83
|
+
"""Command to resume a paused print job."""
|
|
84
|
+
|
|
85
|
+
CMD_STOP_PRINT: Final[str] = "~M26"
|
|
86
|
+
"""Command to stop/cancel the current print job."""
|
|
87
|
+
|
|
88
|
+
# Temperature control with waiting
|
|
89
|
+
WAIT_FOR_HOTEND_TEMP: Final[str] = "~M109"
|
|
90
|
+
"""Command to set extruder temperature and wait until it's reached. Requires S[temperature] parameter."""
|
|
91
|
+
|
|
92
|
+
WAIT_FOR_BED_TEMP: Final[str] = "~M190"
|
|
93
|
+
"""Command to set bed temperature and wait until it's reached. Requires S[temperature] or R[temperature] parameter."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response parsers for FlashForge printer TCP communication.
|
|
3
|
+
|
|
4
|
+
This module contains classes for parsing various response types from FlashForge printers,
|
|
5
|
+
including printer information, temperature data, location data, and more.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .endstop_status import Endstop, EndstopStatus, MachineStatus, MoveMode, Status
|
|
9
|
+
from .location_info import LocationInfo
|
|
10
|
+
from .print_status import PrintStatus
|
|
11
|
+
from .printer_info import PrinterInfo
|
|
12
|
+
from .temp_info import TempData, TempInfo
|
|
13
|
+
from .thumbnail_info import ThumbnailInfo
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'PrinterInfo',
|
|
17
|
+
'TempInfo',
|
|
18
|
+
'TempData',
|
|
19
|
+
'LocationInfo',
|
|
20
|
+
'EndstopStatus',
|
|
21
|
+
'MachineStatus',
|
|
22
|
+
'MoveMode',
|
|
23
|
+
'Status',
|
|
24
|
+
'Endstop',
|
|
25
|
+
'PrintStatus',
|
|
26
|
+
'ThumbnailInfo',
|
|
27
|
+
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Endstop Status Parser
|
|
3
|
+
|
|
4
|
+
Parses endstop and machine status information from M119 command responses.
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MachineStatus(Enum):
|
|
12
|
+
"""Enumerates the possible operational statuses of the machine."""
|
|
13
|
+
BUILDING_FROM_SD = "BUILDING_FROM_SD"
|
|
14
|
+
BUILDING_COMPLETED = "BUILDING_COMPLETED"
|
|
15
|
+
PAUSED = "PAUSED"
|
|
16
|
+
READY = "READY"
|
|
17
|
+
BUSY = "BUSY"
|
|
18
|
+
DEFAULT = "DEFAULT"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MoveMode(Enum):
|
|
22
|
+
"""Enumerates the possible movement modes of the printer."""
|
|
23
|
+
MOVING = "MOVING"
|
|
24
|
+
PAUSED = "PAUSED"
|
|
25
|
+
READY = "READY"
|
|
26
|
+
WAIT_ON_TOOL = "WAIT_ON_TOOL"
|
|
27
|
+
HOMING = "HOMING"
|
|
28
|
+
DEFAULT = "DEFAULT"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Status:
|
|
32
|
+
"""
|
|
33
|
+
Represents additional status flags parsed from a status line.
|
|
34
|
+
The meaning of S, L, J, F flags can be specific to printer firmware or model.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, data: str):
|
|
38
|
+
"""
|
|
39
|
+
Creates an instance of Status by parsing a string line.
|
|
40
|
+
It uses regular expressions to find key-value pairs like "S:0".
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
data: The string line containing status flags (e.g., "Status S:0 L:0 J:0 F:0")
|
|
44
|
+
"""
|
|
45
|
+
self.s: int = self._get_value(data, "S")
|
|
46
|
+
self.l: int = self._get_value(data, "L")
|
|
47
|
+
self.j: int = self._get_value(data, "J")
|
|
48
|
+
self.f: int = self._get_value(data, "F")
|
|
49
|
+
|
|
50
|
+
def _get_value(self, input_str: str, key: str) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Helper function to extract a numeric value associated with a key from a string.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
input_str: The string to search within
|
|
56
|
+
key: The key whose numeric value is to be extracted
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The parsed integer value, or -1 if the key is not found or parsing fails
|
|
60
|
+
"""
|
|
61
|
+
pattern = rf"{key}:(\d+)"
|
|
62
|
+
match = re.search(pattern, input_str)
|
|
63
|
+
if match and match.group(1):
|
|
64
|
+
return int(match.group(1))
|
|
65
|
+
return -1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Endstop:
|
|
69
|
+
"""
|
|
70
|
+
Represents the state of the printer's endstops.
|
|
71
|
+
Typically, a value of 0 means not triggered, and 1 means triggered.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, data: str):
|
|
75
|
+
"""
|
|
76
|
+
Creates an instance of Endstop by parsing a string line.
|
|
77
|
+
It uses regular expressions to find key-value pairs like "X-max:0".
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data: The string line containing endstop states (e.g., "Endstop X-max:0 Y-max:0 Z-min:1")
|
|
81
|
+
"""
|
|
82
|
+
self.x_max: int = self._get_value(data, "X-max")
|
|
83
|
+
self.y_max: int = self._get_value(data, "Y-max")
|
|
84
|
+
self.z_min: int = self._get_value(data, "Z-min")
|
|
85
|
+
|
|
86
|
+
def _get_value(self, input_str: str, key: str) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Helper function to extract a numeric value associated with a key from a string.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
input_str: The string to search within
|
|
92
|
+
key: The key whose numeric value is to be extracted
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The parsed integer value, or -1 if the key is not found or parsing fails
|
|
96
|
+
"""
|
|
97
|
+
pattern = rf"{key}:(\d+)"
|
|
98
|
+
match = re.search(pattern, input_str)
|
|
99
|
+
if match and match.group(1):
|
|
100
|
+
return int(match.group(1))
|
|
101
|
+
return -1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class EndstopStatus:
|
|
105
|
+
"""
|
|
106
|
+
Represents the status of the printer's endstops and various other machine states.
|
|
107
|
+
|
|
108
|
+
This information is typically parsed from the response of an M119 command or a similar
|
|
109
|
+
consolidated status report from the printer. It includes endstop states, machine operational status,
|
|
110
|
+
movement mode, LED status, and the currently loaded file.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self):
|
|
114
|
+
"""Initialize a new EndstopStatus instance."""
|
|
115
|
+
self.endstop: Optional[Endstop] = None
|
|
116
|
+
self.machine_status: MachineStatus = MachineStatus.DEFAULT
|
|
117
|
+
self.move_mode: MoveMode = MoveMode.DEFAULT
|
|
118
|
+
self.status: Optional[Status] = None
|
|
119
|
+
self.led_enabled: bool = False
|
|
120
|
+
self.current_file: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
def from_replay(self, replay: str) -> Optional['EndstopStatus']:
|
|
123
|
+
"""
|
|
124
|
+
Parses a raw string replay (typically from an M119 or similar status command)
|
|
125
|
+
to populate the properties of this EndstopStatus instance.
|
|
126
|
+
|
|
127
|
+
The replay is expected to be a multi-line string where each line provides specific information:
|
|
128
|
+
- Line 1 (data[0]): Usually a command echo or header, ignored
|
|
129
|
+
- Line 2 (data[1]): Parsed into the endstop object
|
|
130
|
+
- Line 3 (data[2]): Parsed to determine machine_status by checking for keywords
|
|
131
|
+
- Line 4 (data[3]): Parsed to determine move_mode by checking for keywords
|
|
132
|
+
- Line 5 (data[4]): Parsed into the status object
|
|
133
|
+
- Line 6 (data[5]): Parsed to determine led_enabled (1 for true, 0 for false)
|
|
134
|
+
- Line 7 (data[6]): Parsed to get current_file, or None if empty
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
replay: The raw multi-line string response from the printer
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The populated EndstopStatus instance, or None if parsing fails
|
|
141
|
+
"""
|
|
142
|
+
if not replay:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
data = replay.split('\n')
|
|
147
|
+
|
|
148
|
+
# Validate that we have enough lines for a valid response
|
|
149
|
+
# A valid M119 response should have at least 2 lines (command echo + endstop data)
|
|
150
|
+
if len(data) < 2:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
# Check if the data looks like a valid M119 response
|
|
154
|
+
# Should contain "Endstop" in the second line
|
|
155
|
+
if len(data) > 1 and "Endstop" not in data[1]:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Parse endstop data (line 1)
|
|
159
|
+
if len(data) > 1:
|
|
160
|
+
self.endstop = Endstop(data[1])
|
|
161
|
+
|
|
162
|
+
# Parse machine status (line 2)
|
|
163
|
+
if len(data) > 2:
|
|
164
|
+
machine_status = data[2].replace("MachineStatus: ", "").strip()
|
|
165
|
+
if "BUILDING_FROM_SD" in machine_status:
|
|
166
|
+
self.machine_status = MachineStatus.BUILDING_FROM_SD
|
|
167
|
+
elif "BUILDING_COMPLETED" in machine_status:
|
|
168
|
+
self.machine_status = MachineStatus.BUILDING_COMPLETED
|
|
169
|
+
elif "PAUSED" in machine_status:
|
|
170
|
+
self.machine_status = MachineStatus.PAUSED
|
|
171
|
+
elif "READY" in machine_status:
|
|
172
|
+
self.machine_status = MachineStatus.READY
|
|
173
|
+
elif "BUSY" in machine_status:
|
|
174
|
+
self.machine_status = MachineStatus.BUSY
|
|
175
|
+
else:
|
|
176
|
+
print(f"EndstopStatus: Encountered unknown MachineStatus: {machine_status}")
|
|
177
|
+
self.machine_status = MachineStatus.DEFAULT
|
|
178
|
+
|
|
179
|
+
# Parse move mode (line 3)
|
|
180
|
+
if len(data) > 3:
|
|
181
|
+
move_mode = data[3].replace("MoveMode: ", "").strip()
|
|
182
|
+
if "MOVING" in move_mode:
|
|
183
|
+
self.move_mode = MoveMode.MOVING
|
|
184
|
+
elif "PAUSED" in move_mode:
|
|
185
|
+
self.move_mode = MoveMode.PAUSED
|
|
186
|
+
elif "READY" in move_mode:
|
|
187
|
+
self.move_mode = MoveMode.READY
|
|
188
|
+
elif "WAIT_ON_TOOL" in move_mode:
|
|
189
|
+
self.move_mode = MoveMode.WAIT_ON_TOOL
|
|
190
|
+
elif "HOMING" in move_mode:
|
|
191
|
+
self.move_mode = MoveMode.HOMING
|
|
192
|
+
else:
|
|
193
|
+
print(f"EndstopStatus: Encountered unknown MoveMode: {move_mode}")
|
|
194
|
+
self.move_mode = MoveMode.DEFAULT
|
|
195
|
+
|
|
196
|
+
# Parse status flags (line 4)
|
|
197
|
+
if len(data) > 4:
|
|
198
|
+
self.status = Status(data[4])
|
|
199
|
+
|
|
200
|
+
# Parse LED status (line 5)
|
|
201
|
+
if len(data) > 5:
|
|
202
|
+
led_str = data[5].replace("LED: ", "").strip()
|
|
203
|
+
try:
|
|
204
|
+
self.led_enabled = int(led_str) == 1
|
|
205
|
+
except ValueError:
|
|
206
|
+
self.led_enabled = False
|
|
207
|
+
|
|
208
|
+
# Parse current file (line 6)
|
|
209
|
+
if len(data) > 6:
|
|
210
|
+
current_file = data[6].replace("CurrentFile: ", "").strip()
|
|
211
|
+
self.current_file = current_file if current_file else None
|
|
212
|
+
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
print("Unable to create EndstopStatus instance from replay")
|
|
217
|
+
print(f"Replay: {replay}")
|
|
218
|
+
print(f"Error: {e}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def is_print_complete(self) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Checks if the machine status indicates that a print has been completed.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if machine_status is BUILDING_COMPLETED, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
return self.machine_status == MachineStatus.BUILDING_COMPLETED
|
|
229
|
+
|
|
230
|
+
def is_printing(self) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Checks if the machine status indicates that a print is currently in progress from SD.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if machine_status is BUILDING_FROM_SD, False otherwise
|
|
236
|
+
"""
|
|
237
|
+
return self.machine_status == MachineStatus.BUILDING_FROM_SD
|
|
238
|
+
|
|
239
|
+
def is_ready(self) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Checks if the printer is in a ready state (both move mode and machine status are READY).
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if the printer is ready, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
return (self.move_mode == MoveMode.READY and
|
|
247
|
+
self.machine_status == MachineStatus.READY)
|
|
248
|
+
|
|
249
|
+
def is_paused(self) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Checks if the printer is currently paused (either machine status or move mode is PAUSED).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if the printer is paused, False otherwise
|
|
255
|
+
"""
|
|
256
|
+
return (self.machine_status == MachineStatus.PAUSED or
|
|
257
|
+
self.move_mode == MoveMode.PAUSED)
|
|
258
|
+
|
|
259
|
+
def __str__(self) -> str:
|
|
260
|
+
"""String representation of the endstop status."""
|
|
261
|
+
return (f"EndstopStatus(machine={self.machine_status.value}, "
|
|
262
|
+
f"move={self.move_mode.value}, led={self.led_enabled}, "
|
|
263
|
+
f"file='{self.current_file}')")
|
|
264
|
+
|
|
265
|
+
def __repr__(self) -> str:
|
|
266
|
+
"""Detailed string representation for debugging."""
|
|
267
|
+
return (f"EndstopStatus("
|
|
268
|
+
f"endstop={self.endstop}, "
|
|
269
|
+
f"machine_status={self.machine_status}, "
|
|
270
|
+
f"move_mode={self.move_mode}, "
|
|
271
|
+
f"status={self.status}, "
|
|
272
|
+
f"led_enabled={self.led_enabled}, "
|
|
273
|
+
f"current_file='{self.current_file}')")
|