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,587 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level client for FlashForge 3D printers via TCP/IP.
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive interface for interacting with FlashForge printers,
|
|
5
|
+
building upon the basic TCP communication provided by FlashForgeTcpClient.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .gcode import GCodeController, GCodes
|
|
13
|
+
from .parsers import EndstopStatus, LocationInfo, PrinterInfo, PrintStatus, TempInfo, ThumbnailInfo
|
|
14
|
+
from .tcp_client import FlashForgeTcpClient
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlashForgeClient(FlashForgeTcpClient):
|
|
20
|
+
"""
|
|
21
|
+
High-level client for interacting with FlashForge 3D printers via TCP/IP.
|
|
22
|
+
|
|
23
|
+
This class implements specific G-code commands and workflows for printer control,
|
|
24
|
+
such as initialization, LED control, job management, homing, temperature settings,
|
|
25
|
+
filament operations, and retrieving various printer statuses.
|
|
26
|
+
|
|
27
|
+
It uses a "legacy" API approach primarily based on sending G-code/M-code commands
|
|
28
|
+
and parsing text-based responses.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, hostname: str) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Create an instance of FlashForgeClient.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
hostname: The IP address or hostname of the FlashForge printer
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(hostname)
|
|
39
|
+
self._control = GCodeController(self)
|
|
40
|
+
self._is_5m_pro = False
|
|
41
|
+
"""Flag indicating if the connected printer is a 5M Pro model, which may have specific features."""
|
|
42
|
+
|
|
43
|
+
def get_ip(self) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Get the IP address or hostname of the connected printer.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The printer's hostname or IP address
|
|
49
|
+
"""
|
|
50
|
+
return self.hostname
|
|
51
|
+
|
|
52
|
+
def gcode(self) -> GCodeController:
|
|
53
|
+
"""
|
|
54
|
+
Get the GCodeController instance associated with this client.
|
|
55
|
+
|
|
56
|
+
Provides access to specific G-code command methods.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The GCodeController instance
|
|
60
|
+
"""
|
|
61
|
+
return self._control
|
|
62
|
+
|
|
63
|
+
async def init_control(self) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Initialize the control connection with the printer.
|
|
66
|
+
|
|
67
|
+
This typically involves sending a login command, retrieving printer info,
|
|
68
|
+
and starting a keep-alive mechanism. Retries on failure.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if control is successfully initialized, False otherwise
|
|
72
|
+
"""
|
|
73
|
+
logger.info("(Legacy API) InitControl()")
|
|
74
|
+
tries = 0
|
|
75
|
+
while tries <= 3:
|
|
76
|
+
result = await self.send_raw_cmd(GCodes.CMD_LOGIN)
|
|
77
|
+
if result and "Control failed." not in result and "ok" in result:
|
|
78
|
+
await asyncio.sleep(0.1)
|
|
79
|
+
info = await self.get_printer_info()
|
|
80
|
+
if not info:
|
|
81
|
+
logger.info("(Legacy API) Failed to get printer info, aborting.")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
logger.info(f"(Legacy API) connected to: {info.type_name}")
|
|
85
|
+
logger.info(f"(Legacy API) Firmware version: {info.firmware_version}")
|
|
86
|
+
|
|
87
|
+
if "5M" in info.type_name and "Pro" in info.type_name:
|
|
88
|
+
self._is_5m_pro = True
|
|
89
|
+
|
|
90
|
+
await self.start_keep_alive()
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
tries += 1
|
|
94
|
+
# Ensures no errors from previous connections that were improperly closed
|
|
95
|
+
await self.send_raw_cmd(GCodes.CMD_LOGOUT)
|
|
96
|
+
await asyncio.sleep(0.5 * tries)
|
|
97
|
+
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
async def led_on(self) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Turn the printer's LED lights on.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if the command is successful, False otherwise
|
|
106
|
+
"""
|
|
107
|
+
return await self._control.led_on()
|
|
108
|
+
|
|
109
|
+
async def led_off(self) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Turn the printer's LED lights off.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if the command is successful, False otherwise
|
|
115
|
+
"""
|
|
116
|
+
return await self._control.led_off()
|
|
117
|
+
|
|
118
|
+
async def pause_job(self) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Pause the current print job.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if the command is successful, False otherwise
|
|
124
|
+
"""
|
|
125
|
+
return await self._control.pause_job()
|
|
126
|
+
|
|
127
|
+
async def resume_job(self) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Resume a paused print job.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if the command is successful, False otherwise
|
|
133
|
+
"""
|
|
134
|
+
return await self._control.resume_job()
|
|
135
|
+
|
|
136
|
+
async def stop_job(self) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Stop the current print job.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if the command is successful, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
return await self._control.stop_job()
|
|
144
|
+
|
|
145
|
+
async def start_job(self, name: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Start a print job from a file stored on the printer.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: The name of the file to print (typically without path)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if the command is successful, False otherwise
|
|
154
|
+
"""
|
|
155
|
+
return await self._control.start_job(name)
|
|
156
|
+
|
|
157
|
+
async def home_axes(self) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Home all axes (X, Y, Z) of the printer.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if the command is successful, False otherwise
|
|
163
|
+
"""
|
|
164
|
+
return await self._control.home()
|
|
165
|
+
|
|
166
|
+
async def rapid_home(self) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Perform a rapid homing of all axes.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if the command is successful, False otherwise
|
|
172
|
+
"""
|
|
173
|
+
return await self._control.rapid_home()
|
|
174
|
+
|
|
175
|
+
async def turn_runout_sensor_on(self) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Turn on the filament runout sensor.
|
|
178
|
+
|
|
179
|
+
This functionality is only available on specific printer models (e.g., 5M Pro).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if the command is successful and applicable, False otherwise
|
|
183
|
+
"""
|
|
184
|
+
if self._is_5m_pro:
|
|
185
|
+
return await self.send_cmd_ok(GCodes.CMD_RUNOUT_SENSOR_ON)
|
|
186
|
+
|
|
187
|
+
logger.info("Filament runout sensor not equipped on this printer.")
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
async def turn_runout_sensor_off(self) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Turn off the filament runout sensor.
|
|
193
|
+
|
|
194
|
+
This functionality is only available on specific printer models (e.g., 5M Pro).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if the command is successful and applicable, False otherwise
|
|
198
|
+
"""
|
|
199
|
+
if self._is_5m_pro:
|
|
200
|
+
return await self.send_cmd_ok(GCodes.CMD_RUNOUT_SENSOR_OFF)
|
|
201
|
+
|
|
202
|
+
logger.info("Filament runout sensor not equipped on this printer.")
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
async def set_extruder_temp(self, temp: int, wait_for: bool = False) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Set the target temperature for the extruder.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
temp: The target temperature in Celsius
|
|
211
|
+
wait_for: If True, the method will wait until the target temperature is reached
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if the command is successful, False otherwise
|
|
215
|
+
"""
|
|
216
|
+
return await self._control.set_extruder_temp(temp, wait_for)
|
|
217
|
+
|
|
218
|
+
async def cancel_extruder_temp(self) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Cancel extruder heating and set its target temperature to 0.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if the command is successful, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
return await self._control.cancel_extruder_temp()
|
|
226
|
+
|
|
227
|
+
async def set_bed_temp(self, temp: int, wait_for: bool = False) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Set the target temperature for the print bed.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
temp: The target temperature in Celsius
|
|
233
|
+
wait_for: If True, the method will wait until the target temperature is reached
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if the command is successful, False otherwise
|
|
237
|
+
"""
|
|
238
|
+
return await self._control.set_bed_temp(temp, wait_for)
|
|
239
|
+
|
|
240
|
+
async def cancel_bed_temp(self, wait_for_cool: bool = False) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Cancel print bed heating and set its target temperature to 0.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
wait_for_cool: If True, waits for the bed to cool down after canceling
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if the command is successful, False otherwise
|
|
249
|
+
"""
|
|
250
|
+
return await self._control.cancel_bed_temp(wait_for_cool)
|
|
251
|
+
|
|
252
|
+
async def extrude(self, length: float, feedrate: int = 450) -> bool:
|
|
253
|
+
"""
|
|
254
|
+
Command the extruder to extrude a specific length of filament.
|
|
255
|
+
|
|
256
|
+
Uses G1 E[length] F[feedrate] command.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
length: The length of filament to extrude in millimeters
|
|
260
|
+
feedrate: The feedrate for extrusion in mm/min (default: 450)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True if the command is successful, False otherwise
|
|
264
|
+
"""
|
|
265
|
+
return await self.send_cmd_ok(f"~G1 E{length} F{feedrate}")
|
|
266
|
+
|
|
267
|
+
async def move_extruder(self, x: float, y: float, feedrate: int) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Move the extruder to a specified X, Y position.
|
|
270
|
+
|
|
271
|
+
Uses G1 X[x] Y[y] F[feedrate] command.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
x: The target X coordinate
|
|
275
|
+
y: The target Y coordinate
|
|
276
|
+
feedrate: The feedrate for the movement in mm/min
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if the command is successful, False otherwise
|
|
280
|
+
"""
|
|
281
|
+
return await self.send_cmd_ok(f"~G1 X{x} Y{y} F{feedrate}")
|
|
282
|
+
|
|
283
|
+
async def move(self, x: float, y: float, z: float, feedrate: int) -> bool:
|
|
284
|
+
"""
|
|
285
|
+
Move the extruder to a specified X, Y, Z position.
|
|
286
|
+
|
|
287
|
+
Uses G1 X[x] Y[y] Z[z] F[feedrate] command.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
x: The target X coordinate
|
|
291
|
+
y: The target Y coordinate
|
|
292
|
+
z: The target Z coordinate
|
|
293
|
+
feedrate: The feedrate for the movement in mm/min
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
True if the command is successful, False otherwise
|
|
297
|
+
"""
|
|
298
|
+
return await self.send_cmd_ok(f"~G1 X{x} Y{y} Z{z} F{feedrate}")
|
|
299
|
+
|
|
300
|
+
async def send_cmd_ok(self, cmd: str) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Send a G-code/M-code command to the printer and check for an "ok" response.
|
|
303
|
+
|
|
304
|
+
Expects the printer's reply to include "Received." and "ok" to be considered successful.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
cmd: The command string to send (e.g., "~M115")
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
True if the command is acknowledged with "ok", False otherwise or on error
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
reply = await self.send_command_async(cmd)
|
|
314
|
+
if reply and "Received." in reply and "ok" in reply:
|
|
315
|
+
return True
|
|
316
|
+
except Exception as ex:
|
|
317
|
+
logger.error(f"SendCmdOk exception sending cmd: {cmd} : {ex}")
|
|
318
|
+
return False
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
async def send_raw_cmd(self, cmd: str) -> str:
|
|
322
|
+
"""
|
|
323
|
+
Send a raw command string to the printer and return the raw response.
|
|
324
|
+
|
|
325
|
+
Handles a special case for "M661" (list files), which is processed differently.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
cmd: The raw command string to send
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The printer's raw string response, or an empty string on failure.
|
|
332
|
+
For "M661", it returns a newline-separated list of files.
|
|
333
|
+
"""
|
|
334
|
+
if "M661" not in cmd:
|
|
335
|
+
result = await self.send_command_async(cmd)
|
|
336
|
+
return result or ''
|
|
337
|
+
|
|
338
|
+
file_list = await self.get_file_list_async()
|
|
339
|
+
return "\n".join(file_list)
|
|
340
|
+
|
|
341
|
+
async def get_printer_info(self) -> Optional[PrinterInfo]:
|
|
342
|
+
"""
|
|
343
|
+
Retrieve general printer information (model, firmware, etc.).
|
|
344
|
+
|
|
345
|
+
Sends CMD_INFO_STATUS and parses the response into a PrinterInfo object.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
A PrinterInfo object, or None if retrieval fails
|
|
349
|
+
"""
|
|
350
|
+
response = await self.send_command_async(GCodes.CMD_INFO_STATUS)
|
|
351
|
+
if response:
|
|
352
|
+
return PrinterInfo().from_replay(response)
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
async def get_temp_info(self) -> Optional[TempInfo]:
|
|
356
|
+
"""
|
|
357
|
+
Retrieve current temperature information (extruder, bed).
|
|
358
|
+
|
|
359
|
+
Sends CMD_TEMP and parses the response into a TempInfo object.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
A TempInfo object, or None if retrieval fails
|
|
363
|
+
"""
|
|
364
|
+
response = await self.send_command_async(GCodes.CMD_TEMP)
|
|
365
|
+
if response:
|
|
366
|
+
return TempInfo().from_replay(response)
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
async def get_location_info(self) -> Optional[LocationInfo]:
|
|
370
|
+
"""
|
|
371
|
+
Retrieve the current XYZ coordinates of the print head.
|
|
372
|
+
|
|
373
|
+
Sends CMD_INFO_XYZAB and parses the response into a LocationInfo object.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
A LocationInfo object, or None if retrieval fails
|
|
377
|
+
"""
|
|
378
|
+
response = await self.send_command_async(GCodes.CMD_INFO_XYZAB)
|
|
379
|
+
if response:
|
|
380
|
+
return LocationInfo().from_replay(response)
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
async def _get_nozzle_temp(self) -> float:
|
|
384
|
+
"""
|
|
385
|
+
Retrieve the current temperature of the nozzle (extruder).
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
The current nozzle temperature in Celsius, or 0 if unavailable
|
|
389
|
+
"""
|
|
390
|
+
temps = await self.get_temp_info()
|
|
391
|
+
if temps and temps.get_extruder_temp():
|
|
392
|
+
return temps.get_extruder_temp().get_current()
|
|
393
|
+
return 0.0
|
|
394
|
+
|
|
395
|
+
async def get_endstop_status(self) -> Optional[EndstopStatus]:
|
|
396
|
+
"""
|
|
397
|
+
Retrieve the current endstop status and machine state information.
|
|
398
|
+
|
|
399
|
+
Sends M119 command and parses the response into an EndstopStatus object.
|
|
400
|
+
This includes endstop states, machine status, move mode, LED status, and current file.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
An EndstopStatus object, or None if retrieval fails
|
|
404
|
+
"""
|
|
405
|
+
response = await self.send_command_async(GCodes.CMD_INFO_STATUS) # M119
|
|
406
|
+
if response:
|
|
407
|
+
return EndstopStatus().from_replay(response)
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
async def get_print_status(self) -> Optional[PrintStatus]:
|
|
411
|
+
"""
|
|
412
|
+
Retrieve the current print status and progress information.
|
|
413
|
+
|
|
414
|
+
Sends M27 command and parses the response into a PrintStatus object.
|
|
415
|
+
This includes SD card byte progress and layer progress.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
A PrintStatus object, or None if retrieval fails
|
|
419
|
+
"""
|
|
420
|
+
response = await self.send_command_async("~M27") # Print progress
|
|
421
|
+
if response:
|
|
422
|
+
return PrintStatus().from_replay(response)
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
async def get_thumbnail(self, file_name: str) -> Optional[ThumbnailInfo]:
|
|
426
|
+
"""
|
|
427
|
+
Retrieve the thumbnail image for a specific file.
|
|
428
|
+
|
|
429
|
+
Sends M662 command and parses the response to extract PNG thumbnail data.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
file_name: The name of the file to get the thumbnail for
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A ThumbnailInfo object containing the PNG image data, or None if retrieval fails
|
|
436
|
+
"""
|
|
437
|
+
# M662 command to get thumbnail
|
|
438
|
+
cmd = f"~M662 {file_name}"
|
|
439
|
+
response = await self.send_command_async(cmd)
|
|
440
|
+
if response:
|
|
441
|
+
return ThumbnailInfo().from_replay(response, file_name)
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
async def check_machine_state(self) -> str:
|
|
445
|
+
"""
|
|
446
|
+
Get a simplified machine state string for quick status checking.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
A string indicating the machine state ("printing", "ready", "paused", "complete", "unknown")
|
|
450
|
+
"""
|
|
451
|
+
endstop_status = await self.get_endstop_status()
|
|
452
|
+
if not endstop_status:
|
|
453
|
+
return "unknown"
|
|
454
|
+
|
|
455
|
+
if endstop_status.is_printing():
|
|
456
|
+
return "printing"
|
|
457
|
+
elif endstop_status.is_print_complete():
|
|
458
|
+
return "complete"
|
|
459
|
+
elif endstop_status.is_paused():
|
|
460
|
+
return "paused"
|
|
461
|
+
elif endstop_status.is_ready():
|
|
462
|
+
return "ready"
|
|
463
|
+
else:
|
|
464
|
+
return "unknown"
|
|
465
|
+
|
|
466
|
+
async def get_current_print_file(self) -> Optional[str]:
|
|
467
|
+
"""
|
|
468
|
+
Get the name of the currently loaded/printing file.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
The filename of the current print job, or None if no file is loaded
|
|
472
|
+
"""
|
|
473
|
+
endstop_status = await self.get_endstop_status()
|
|
474
|
+
if endstop_status:
|
|
475
|
+
return endstop_status.current_file
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
async def is_printer_ready(self) -> bool:
|
|
479
|
+
"""
|
|
480
|
+
Check if the printer is ready for new commands.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
True if the printer is in a ready state, False otherwise
|
|
484
|
+
"""
|
|
485
|
+
endstop_status = await self.get_endstop_status()
|
|
486
|
+
if endstop_status:
|
|
487
|
+
return endstop_status.is_ready()
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
async def get_print_progress(self) -> tuple[int, int, int]:
|
|
491
|
+
"""
|
|
492
|
+
Get comprehensive print progress information.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
A tuple of (layer_percent, sd_percent, current_layer) where:
|
|
496
|
+
- layer_percent: Progress based on layers (0-100)
|
|
497
|
+
- sd_percent: Progress based on SD card bytes (0-100)
|
|
498
|
+
- current_layer: Current layer number
|
|
499
|
+
Returns (0, 0, 0) if progress information is unavailable
|
|
500
|
+
"""
|
|
501
|
+
print_status = await self.get_print_status()
|
|
502
|
+
if not print_status:
|
|
503
|
+
return (0, 0, 0)
|
|
504
|
+
|
|
505
|
+
layer_percent = print_status.get_print_percent()
|
|
506
|
+
sd_percent = print_status.get_sd_percent()
|
|
507
|
+
|
|
508
|
+
# Handle NaN values
|
|
509
|
+
if layer_percent != layer_percent: # NaN check
|
|
510
|
+
layer_percent = 0
|
|
511
|
+
if sd_percent != sd_percent: # NaN check
|
|
512
|
+
sd_percent = 0
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
current_layer = int(print_status.layer_current) if print_status.layer_current else 0
|
|
516
|
+
except ValueError:
|
|
517
|
+
current_layer = 0
|
|
518
|
+
|
|
519
|
+
return (int(layer_percent), int(sd_percent), current_layer)
|
|
520
|
+
|
|
521
|
+
async def wait_for_part_cool(self, target_temp: float = 50.0, timeout_seconds: int = 1800) -> bool:
|
|
522
|
+
"""
|
|
523
|
+
Wait for printer components (extruder and bed) to cool down to a safe temperature.
|
|
524
|
+
|
|
525
|
+
This method cancels heating for both extruder and bed, then waits for them to cool down.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
target_temp: The target temperature to wait for (default: 50°C)
|
|
529
|
+
timeout_seconds: Maximum time to wait in seconds (default: 30 minutes)
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
True if components cooled to target temperature, False if timeout or error
|
|
533
|
+
"""
|
|
534
|
+
logger.info(f"Starting cooling process - waiting for parts to cool to {target_temp}°C")
|
|
535
|
+
|
|
536
|
+
# Cancel heating for both extruder and bed
|
|
537
|
+
extruder_cancel_ok = await self.cancel_extruder_temp()
|
|
538
|
+
bed_cancel_ok = await self.cancel_bed_temp()
|
|
539
|
+
|
|
540
|
+
if not (extruder_cancel_ok and bed_cancel_ok):
|
|
541
|
+
logger.error("Failed to cancel heating - cannot wait for cooling")
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
start_time = asyncio.get_event_loop().time()
|
|
545
|
+
timeout_time = start_time + timeout_seconds
|
|
546
|
+
|
|
547
|
+
extruder_cooled = False
|
|
548
|
+
bed_cooled = False
|
|
549
|
+
|
|
550
|
+
while asyncio.get_event_loop().time() < timeout_time:
|
|
551
|
+
# Check current temperatures
|
|
552
|
+
temp_info = await self.get_temp_info()
|
|
553
|
+
if not temp_info:
|
|
554
|
+
logger.warning("Could not get temperature info during cooling")
|
|
555
|
+
await asyncio.sleep(10)
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
# Check extruder temperature
|
|
559
|
+
if not extruder_cooled:
|
|
560
|
+
extruder_temp = temp_info.get_extruder_temp()
|
|
561
|
+
if extruder_temp:
|
|
562
|
+
current_extruder_temp = extruder_temp.get_current()
|
|
563
|
+
if current_extruder_temp <= target_temp:
|
|
564
|
+
extruder_cooled = True
|
|
565
|
+
logger.info(f"Extruder cooled to {current_extruder_temp}°C")
|
|
566
|
+
|
|
567
|
+
# Check bed temperature
|
|
568
|
+
if not bed_cooled:
|
|
569
|
+
bed_temp = temp_info.get_bed_temp()
|
|
570
|
+
if bed_temp:
|
|
571
|
+
current_bed_temp = bed_temp.get_current()
|
|
572
|
+
if current_bed_temp <= target_temp:
|
|
573
|
+
bed_cooled = True
|
|
574
|
+
logger.info(f"Bed cooled to {current_bed_temp}°C")
|
|
575
|
+
|
|
576
|
+
# Check if both have cooled
|
|
577
|
+
if extruder_cooled and bed_cooled:
|
|
578
|
+
logger.info("All components have cooled to target temperature")
|
|
579
|
+
return True
|
|
580
|
+
|
|
581
|
+
# Wait before next check
|
|
582
|
+
await asyncio.sleep(10) # Check every 10 seconds
|
|
583
|
+
|
|
584
|
+
# Timeout reached
|
|
585
|
+
logger.warning(f"Timeout after {timeout_seconds} seconds waiting for cooling")
|
|
586
|
+
logger.warning(f"Extruder cooled: {extruder_cooled}, Bed cooled: {bed_cooled}")
|
|
587
|
+
return False
|