flashforge-python-api 1.0.2__py3-none-any.whl → 1.2.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 +70 -29
  2. flashforge/api/__init__.py +1 -0
  3. flashforge/api/constants/__init__.py +1 -0
  4. flashforge/api/constants/commands.py +1 -0
  5. flashforge/api/constants/endpoints.py +7 -0
  6. flashforge/api/controls/__init__.py +1 -0
  7. flashforge/api/controls/control.py +64 -61
  8. flashforge/api/controls/files.py +28 -26
  9. flashforge/api/controls/info.py +84 -85
  10. flashforge/api/controls/job_control.py +120 -82
  11. flashforge/api/controls/temp_control.py +14 -11
  12. flashforge/api/misc/__init__.py +1 -1
  13. flashforge/api/network/__init__.py +2 -1
  14. flashforge/api/network/utils.py +7 -8
  15. flashforge/client.py +185 -64
  16. flashforge/discovery/__init__.py +30 -3
  17. flashforge/discovery/discovery.py +641 -308
  18. flashforge/models/__init__.py +11 -10
  19. flashforge/models/machine_info.py +225 -100
  20. flashforge/models/responses.py +103 -43
  21. flashforge/tcp/__init__.py +30 -17
  22. flashforge/tcp/a3_client.py +408 -0
  23. flashforge/tcp/a4_client.py +300 -0
  24. flashforge/tcp/ff_client.py +93 -90
  25. flashforge/tcp/gcode/__init__.py +4 -2
  26. flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
  27. flashforge/tcp/gcode/gcode_controller.py +48 -36
  28. flashforge/tcp/gcode/gcodes.py +7 -1
  29. flashforge/tcp/parsers/__init__.py +11 -11
  30. flashforge/tcp/parsers/endstop_status.py +103 -132
  31. flashforge/tcp/parsers/location_info.py +26 -13
  32. flashforge/tcp/parsers/print_status.py +49 -56
  33. flashforge/tcp/parsers/printer_info.py +38 -48
  34. flashforge/tcp/parsers/temp_info.py +46 -47
  35. flashforge/tcp/parsers/thumbnail_info.py +65 -40
  36. flashforge/tcp/tcp_client.py +296 -293
  37. flashforge_python_api-1.2.0.dist-info/METADATA +133 -0
  38. flashforge_python_api-1.2.0.dist-info/RECORD +46 -0
  39. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/WHEEL +1 -1
  40. flashforge_python_api-1.0.2.dist-info/METADATA +0 -284
  41. flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
  42. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
  43. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ X, Y, Z coordinates of the printer's print head.
6
6
  """
7
7
 
8
8
  import logging
9
+ import re
9
10
  from typing import Optional
10
11
 
11
12
  logger = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ logger = logging.getLogger(__name__)
14
15
  class LocationInfo:
15
16
  """
16
17
  Represents the current X, Y, and Z coordinates of the printer's print head.
17
-
18
+
18
19
  This information is typically parsed from the response of an M114 G-code command,
19
20
  which reports the current position.
20
21
  """
@@ -30,29 +31,41 @@ class LocationInfo:
30
31
  self.z: str = ""
31
32
  """The current Z-axis coordinate as a string (e.g., "5.25")."""
32
33
 
33
- def from_replay(self, replay: str) -> Optional['LocationInfo']:
34
+ def from_replay(self, replay: str) -> Optional["LocationInfo"]:
34
35
  """
35
36
  Parse a raw string replay from M114 command to populate coordinate info.
36
-
37
+
37
38
  The parsing logic assumes the replay is a multi-line string where the second line
38
39
  (data[1]) contains the coordinate data in a format like "X:10.00 Y:20.50 Z:5.25 ...".
39
40
  It splits this line by spaces and then extracts the values for X, Y, and Z by
40
41
  removing the prefixes "X:", "Y:", and "Z:".
41
-
42
+
42
43
  Args:
43
44
  replay: The raw multi-line string response from the printer
44
-
45
+
45
46
  Returns:
46
47
  The populated LocationInfo instance, or None if parsing fails
47
48
  """
48
49
  try:
49
- data = replay.split('\n')
50
- # The first line (data[0]) is often the command echo (e.g., "ok M114") or similar,
51
- # actual coordinate data is expected on the second line.
52
- loc_data = data[1].split(' ')
53
- self.x = loc_data[0].replace("X:", "").strip()
54
- self.y = loc_data[1].replace("Y:", "").strip()
55
- self.z = loc_data[2].replace("Z:", "").strip()
50
+ lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
51
+ coordinate_line = next(
52
+ (line for line in lines if "X:" in line and "Y:" in line and "Z:" in line),
53
+ "",
54
+ )
55
+ if not coordinate_line:
56
+ logger.error("LocationInfo replay has bad/null data")
57
+ return None
58
+
59
+ x_match = re.search(r"X:\s*([^\s]+)", coordinate_line)
60
+ y_match = re.search(r"Y:\s*([^\s]+)", coordinate_line)
61
+ z_match = re.search(r"Z:\s*([^\s]+)", coordinate_line)
62
+ if not x_match or not y_match or not z_match:
63
+ logger.error("LocationInfo replay has bad/null data")
64
+ return None
65
+
66
+ self.x = x_match.group(1)
67
+ self.y = y_match.group(1)
68
+ self.z = z_match.group(1)
56
69
  return self
57
70
  except Exception:
58
71
  logger.error("LocationInfo replay has bad/null data")
@@ -61,7 +74,7 @@ class LocationInfo:
61
74
  def __str__(self) -> str:
62
75
  """
63
76
  Return a string representation of the location information.
64
-
77
+
65
78
  Returns:
66
79
  A string in the format "X: [X_value] Y: [Y_value] Z: [Z_value]"
67
80
  """
@@ -3,39 +3,40 @@ FlashForge Python API - Print Status Parser
3
3
 
4
4
  Parses print progress information from M27 command responses.
5
5
  """
6
+
6
7
  from typing import Optional
7
8
 
8
9
 
9
10
  class PrintStatus:
10
11
  """
11
12
  Represents the status of an ongoing print job, including SD card byte progress and layer progress.
12
-
13
+
13
14
  This information is typically parsed from the response of an M27 G-code command,
14
15
  which reports the print progress from the SD card.
15
16
  """
16
17
 
17
- def __init__(self):
18
+ def __init__(self) -> None:
18
19
  """Initialize a new PrintStatus instance."""
19
20
  self.sd_current: str = ""
20
21
  self.sd_total: str = ""
21
22
  self.layer_current: str = ""
22
23
  self.layer_total: str = ""
23
24
 
24
- def from_replay(self, replay: str) -> Optional['PrintStatus']:
25
+ def from_replay(self, replay: str) -> Optional["PrintStatus"]:
25
26
  """
26
27
  Parses a raw string replay (typically from an M27 command) to populate
27
28
  the print status properties of this instance.
28
-
29
+
29
30
  The parsing logic expects a multi-line string:
30
31
  - Line 1 (data[0]): Usually a command echo, ignored
31
32
  - Line 2 (data[1]): Contains SD card progress, e.g., "SD printing byte 12345/67890"
32
33
  It extracts the current and total bytes
33
34
  - Line 3 (data[2]): Contains layer progress, e.g., "Layer: 10/250"
34
35
  It extracts the current and total layers
35
-
36
+
36
37
  Args:
37
38
  replay: The raw multi-line string response from the printer
38
-
39
+
39
40
  Returns:
40
41
  The populated PrintStatus instance, or None if parsing fails
41
42
  """
@@ -43,44 +44,31 @@ class PrintStatus:
43
44
  return None
44
45
 
45
46
  try:
46
- data = replay.split('\n')
47
-
48
- # Parse SD progress (line 1)
49
- if len(data) > 1:
50
- # Example: "SD printing byte 12345/67890"
51
- sd_progress = data[1].replace("SD printing byte ", "").strip()
52
- sd_progress_data = sd_progress.split('/')
53
-
54
- if len(sd_progress_data) >= 2:
55
- self.sd_current = sd_progress_data[0].strip()
56
- self.sd_total = sd_progress_data[1].strip()
57
- else:
58
- print("PrintStatus: Invalid SD progress format")
59
- return None
60
-
61
- # Parse layer progress (line 2)
62
- if len(data) > 2:
63
- try:
64
- # Example: "Layer: 10/250"
65
- layer_progress = data[2].replace("Layer: ", "").strip()
66
- except Exception:
67
- print("PrintStatus: Bad layer progress")
68
- print(f"Raw printer replay: {replay}")
69
- return None
70
-
71
- try:
72
- lp_data = layer_progress.split('/')
47
+ self.sd_current = ""
48
+ self.sd_total = ""
49
+ self.layer_current = ""
50
+ self.layer_total = ""
51
+ lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
52
+
53
+ for line in lines:
54
+ if "SD printing byte " in line:
55
+ sd_progress = line.split("SD printing byte ", 1)[1].strip().strip('"')
56
+ sd_progress_data = sd_progress.split("/")
57
+ if len(sd_progress_data) >= 2:
58
+ self.sd_current = sd_progress_data[0].strip()
59
+ self.sd_total = sd_progress_data[1].strip()
60
+ elif line.startswith("Layer:"):
61
+ layer_progress = line.replace("Layer:", "", 1).strip()
62
+ lp_data = layer_progress.split("/")
73
63
  if len(lp_data) >= 2:
74
64
  self.layer_current = lp_data[0].strip()
75
65
  self.layer_total = lp_data[1].strip()
76
66
  else:
77
- print("PrintStatus: Invalid layer progress format")
78
- print(f"layerProgress: {layer_progress}")
79
67
  return None
80
- except Exception:
81
- print("PrintStatus: Bad layer progress parsing")
82
- print(f"layerProgress: {layer_progress}")
83
- return None
68
+
69
+ if not self.sd_current or not self.sd_total:
70
+ print("PrintStatus: Invalid SD progress format")
71
+ return None
84
72
 
85
73
  return self
86
74
 
@@ -92,7 +80,7 @@ class PrintStatus:
92
80
  """
93
81
  Calculates the print progress percentage based on the current and total layers.
94
82
  The result is clamped between 0 and 100.
95
-
83
+
96
84
  Returns:
97
85
  The print progress percentage (0-100), rounded to the nearest integer.
98
86
  Returns NaN if layer information is not available or invalid.
@@ -102,18 +90,18 @@ class PrintStatus:
102
90
  total_layers = int(self.layer_total)
103
91
 
104
92
  if total_layers == 0:
105
- return float('nan')
93
+ return float("nan")
106
94
 
107
95
  perc = (current_layer / total_layers) * 100
108
96
  return round(min(100, max(0, perc))) # Clamp between 0 and 100
109
97
 
110
98
  except (ValueError, TypeError):
111
- return float('nan')
99
+ return self.get_sd_percent()
112
100
 
113
101
  def get_layer_progress(self) -> str:
114
102
  """
115
103
  Gets the layer progress as a string.
116
-
104
+
117
105
  Returns:
118
106
  A string in the format "currentLayer/totalLayers"
119
107
  """
@@ -122,7 +110,7 @@ class PrintStatus:
122
110
  def get_sd_progress(self) -> str:
123
111
  """
124
112
  Gets the SD card byte progress as a string.
125
-
113
+
126
114
  Returns:
127
115
  A string in the format "currentBytes/totalBytes"
128
116
  """
@@ -131,7 +119,7 @@ class PrintStatus:
131
119
  def get_sd_percent(self) -> float:
132
120
  """
133
121
  Calculates the SD card progress percentage based on current and total bytes.
134
-
122
+
135
123
  Returns:
136
124
  The SD progress percentage (0-100), or NaN if data is invalid
137
125
  """
@@ -140,18 +128,18 @@ class PrintStatus:
140
128
  total_bytes = int(self.sd_total)
141
129
 
142
130
  if total_bytes == 0:
143
- return float('nan')
131
+ return float("nan")
144
132
 
145
133
  perc = (current_bytes / total_bytes) * 100
146
134
  return round(min(100, max(0, perc))) # Clamp between 0 and 100
147
135
 
148
136
  except (ValueError, TypeError):
149
- return float('nan')
137
+ return float("nan")
150
138
 
151
139
  def is_complete(self) -> bool:
152
140
  """
153
141
  Checks if the print is complete based on layer progress.
154
-
142
+
155
143
  Returns:
156
144
  True if current layer equals total layers, False otherwise
157
145
  """
@@ -160,7 +148,8 @@ class PrintStatus:
160
148
  total = int(self.layer_total)
161
149
  return current >= total and total > 0
162
150
  except (ValueError, TypeError):
163
- return False
151
+ sd_percent = self.get_sd_percent()
152
+ return sd_percent == 100 if sd_percent == sd_percent else False
164
153
 
165
154
  def __str__(self) -> str:
166
155
  """String representation of the print status."""
@@ -170,13 +159,17 @@ class PrintStatus:
170
159
  layer_str = "nan%" if layer_perc != layer_perc else f"{layer_perc}%" # Check for NaN
171
160
  sd_str = "nan%" if sd_perc != sd_perc else f"{sd_perc}%" # Check for NaN
172
161
 
173
- return (f"PrintStatus(layer={self.get_layer_progress()} [{layer_str}], "
174
- f"sd={self.get_sd_progress()} [{sd_str}])")
162
+ return (
163
+ f"PrintStatus(layer={self.get_layer_progress()} [{layer_str}], "
164
+ f"sd={self.get_sd_progress()} [{sd_str}])"
165
+ )
175
166
 
176
167
  def __repr__(self) -> str:
177
168
  """Detailed string representation for debugging."""
178
- return (f"PrintStatus("
179
- f"sd_current='{self.sd_current}', "
180
- f"sd_total='{self.sd_total}', "
181
- f"layer_current='{self.layer_current}', "
182
- f"layer_total='{self.layer_total}')")
169
+ return (
170
+ f"PrintStatus("
171
+ f"sd_current='{self.sd_current}', "
172
+ f"sd_total='{self.sd_total}', "
173
+ f"layer_current='{self.layer_current}', "
174
+ f"layer_total='{self.layer_total}')"
175
+ )
@@ -6,6 +6,7 @@ like model, firmware version, serial number, and capabilities.
6
6
  """
7
7
 
8
8
  import logging
9
+ import re
9
10
  from typing import Optional
10
11
 
11
12
  logger = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ logger = logging.getLogger(__name__)
14
15
  class PrinterInfo:
15
16
  """
16
17
  Represents general information about the FlashForge 3D printer.
17
-
18
+
18
19
  This information is typically parsed from the response of an M115 G-code command,
19
20
  which provides details about the printer's firmware and capabilities.
20
21
  """
@@ -42,13 +43,13 @@ class PrinterInfo:
42
43
  self.tool_count: str = ""
43
44
  """The number of tools (extruders) the printer has."""
44
45
 
45
- def from_replay(self, replay: str) -> Optional['PrinterInfo']:
46
+ def from_replay(self, replay: str) -> Optional["PrinterInfo"]:
46
47
  """
47
48
  Parse a raw string replay from M115 command to populate printer info.
48
-
49
+
49
50
  The M115 response is expected to be a multi-line string where each line
50
51
  provides a piece of information in a "Key: Value" format.
51
-
52
+
52
53
  Expected format:
53
54
  - Line 1: Command echo/header (ignored)
54
55
  - Line 2: "Machine Type: [TypeName]"
@@ -58,10 +59,10 @@ class PrinterInfo:
58
59
  - Line 6: Dimensions string (e.g., "X:220 Y:220 Z:220")
59
60
  - Line 7: "Tool count: [ToolCount]"
60
61
  - Line 8: "Mac Address:[MacAddress]"
61
-
62
+
62
63
  Args:
63
64
  replay: The raw multi-line string response from the M115 command
64
-
65
+
65
66
  Returns:
66
67
  The populated PrinterInfo instance, or None if parsing fails
67
68
  """
@@ -69,51 +70,40 @@ class PrinterInfo:
69
70
  return None
70
71
 
71
72
  try:
72
- data = replay.split('\n')
73
-
74
- # Parse machine type (line 2)
75
- name = self._get_right(data[1]) # Expected: "Machine Type: Adventurer 5M Pro"
76
- if name is None:
73
+ lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
74
+
75
+ for line in lines:
76
+ if line.startswith("Machine Type:"):
77
+ self.type_name = line.replace("Machine Type:", "", 1).strip()
78
+ elif line.startswith("Machine Name:"):
79
+ self.name = line.replace("Machine Name:", "", 1).strip()
80
+ elif line.startswith("Firmware:"):
81
+ self.firmware_version = line.replace("Firmware:", "", 1).strip()
82
+ elif line.startswith("SN:"):
83
+ self.serial_number = line.replace("SN:", "", 1).strip()
84
+ elif line.startswith("Serial Number:"):
85
+ self.serial_number = line.replace("Serial Number:", "", 1).strip()
86
+ elif line.startswith("Tool count:") or line.startswith("Tool Count:"):
87
+ tool_count = self._get_right(line)
88
+ if tool_count is not None:
89
+ self.tool_count = tool_count
90
+ elif line.startswith("Mac Address:"):
91
+ self.mac_address = line.replace("Mac Address:", "", 1).strip()
92
+ elif re.search(r"X:\s*\d+.*Y:\s*\d+.*Z:\s*\d+", line, re.IGNORECASE):
93
+ self.dimensions = line
94
+
95
+ if not self.type_name:
77
96
  logger.error("PrinterInfo replay has null Machine Type")
78
97
  return None
79
- self.type_name = name
80
-
81
- # Parse machine name (line 3)
82
- nick = self._get_right(data[2]) # Expected: "Machine Name: MyPrinter"
83
- if nick is None:
98
+ if not self.name:
84
99
  logger.error("PrinterInfo replay has null Machine Name")
85
100
  return None
86
- self.name = nick
87
-
88
- # Parse firmware version (line 4)
89
- fw = self._get_right(data[3]) # Expected: "Firmware: V1.2.3"
90
- if fw is None:
101
+ if not self.firmware_version:
91
102
  logger.error("PrinterInfo replay has null firmware version")
92
103
  return None
93
- self.firmware_version = fw
94
-
95
- # Parse serial number (line 5)
96
- sn = self._get_right(data[4]) # Expected: "SN: SN12345"
97
- if sn is None:
104
+ if not self.serial_number:
98
105
  logger.error("PrinterInfo replay has null serial number")
99
106
  return None
100
- self.serial_number = sn
101
-
102
- # Parse dimensions (line 6) - direct string
103
- if len(data) > 5:
104
- self.dimensions = data[5].strip() # Expected: "X:220 Y:220 Z:220"
105
-
106
- # Parse tool count (line 7)
107
- if len(data) > 6:
108
- tcs = self._get_right(data[6]) # Expected: "Tool count: 1"
109
- if tcs is None:
110
- logger.error("PrinterInfo replay has null tool count")
111
- return None
112
- self.tool_count = tcs
113
-
114
- # Parse MAC address (line 8)
115
- if len(data) > 7:
116
- self.mac_address = data[7].replace("Mac Address:", "").strip()
117
107
 
118
108
  return self
119
109
 
@@ -121,25 +111,25 @@ class PrinterInfo:
121
111
  logger.error(f"Error creating PrinterInfo instance from replay: {e}")
122
112
  return None
123
113
 
124
- def _get_right(self, rp_data: str) -> Optional[str]:
114
+ def _get_right(self, rp_data: str) -> str | None:
125
115
  """
126
116
  Helper function to extract the value part of a "Key: Value" string.
127
-
117
+
128
118
  Args:
129
119
  rp_data: The input string (e.g., "Machine Type: Adventurer 5M Pro")
130
-
120
+
131
121
  Returns:
132
122
  The extracted value string (e.g., "Adventurer 5M Pro"), or None if parsing fails
133
123
  """
134
124
  try:
135
- return rp_data.split(':', 1)[1].strip()
125
+ return rp_data.split(":", 1)[1].strip()
136
126
  except (IndexError, AttributeError):
137
127
  return None
138
128
 
139
129
  def __str__(self) -> str:
140
130
  """
141
131
  Return a string representation of the printer information.
142
-
132
+
143
133
  Returns:
144
134
  A multi-line string detailing the printer's properties
145
135
  """