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.
Files changed (43) hide show
  1. flashforge/__init__.py +188 -0
  2. flashforge/api/__init__.py +17 -0
  3. flashforge/api/constants/__init__.py +10 -0
  4. flashforge/api/constants/commands.py +10 -0
  5. flashforge/api/constants/endpoints.py +11 -0
  6. flashforge/api/controls/__init__.py +16 -0
  7. flashforge/api/controls/control.py +357 -0
  8. flashforge/api/controls/files.py +171 -0
  9. flashforge/api/controls/info.py +292 -0
  10. flashforge/api/controls/job_control.py +561 -0
  11. flashforge/api/controls/temp_control.py +89 -0
  12. flashforge/api/filament/__init__.py +7 -0
  13. flashforge/api/filament/filament.py +39 -0
  14. flashforge/api/misc/__init__.py +8 -0
  15. flashforge/api/misc/scientific_notation.py +28 -0
  16. flashforge/api/misc/temperature.py +42 -0
  17. flashforge/api/network/__init__.py +10 -0
  18. flashforge/api/network/fnet_code.py +15 -0
  19. flashforge/api/network/utils.py +55 -0
  20. flashforge/client.py +381 -0
  21. flashforge/discovery/__init__.py +12 -0
  22. flashforge/discovery/discovery.py +388 -0
  23. flashforge/models/__init__.py +50 -0
  24. flashforge/models/machine_info.py +247 -0
  25. flashforge/models/responses.py +123 -0
  26. flashforge/tcp/__init__.py +41 -0
  27. flashforge/tcp/ff_client.py +587 -0
  28. flashforge/tcp/gcode/__init__.py +11 -0
  29. flashforge/tcp/gcode/gcode_controller.py +296 -0
  30. flashforge/tcp/gcode/gcodes.py +93 -0
  31. flashforge/tcp/parsers/__init__.py +27 -0
  32. flashforge/tcp/parsers/endstop_status.py +273 -0
  33. flashforge/tcp/parsers/location_info.py +68 -0
  34. flashforge/tcp/parsers/print_status.py +182 -0
  35. flashforge/tcp/parsers/printer_info.py +154 -0
  36. flashforge/tcp/parsers/temp_info.py +217 -0
  37. flashforge/tcp/parsers/thumbnail_info.py +242 -0
  38. flashforge/tcp/tcp_client.py +448 -0
  39. flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
  40. flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
  41. flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
  42. flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
  43. 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
@@ -0,0 +1,11 @@
1
+ """
2
+ G-code related modules for FlashForge printer communication.
3
+ """
4
+
5
+ from .gcode_controller import GCodeController
6
+ from .gcodes import GCodes
7
+
8
+ __all__ = [
9
+ 'GCodes',
10
+ 'GCodeController',
11
+ ]