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,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}')")