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,39 @@
1
+ """
2
+ FlashForge Python API - Filament Module
3
+ """
4
+
5
+
6
+ class Filament:
7
+ """
8
+ Represents a type of filament used in a 3D printer.
9
+ It stores information about the filament's name and its recommended loading temperature.
10
+ This class can be used to define specific filament types for printer operations
11
+ like loading or preheating.
12
+ """
13
+
14
+ def __init__(self, name: str, load_temp: float = 220.0):
15
+ """
16
+ Creates an instance of the Filament class.
17
+
18
+ Args:
19
+ name: The name of the filament type (e.g., "PLA", "ABS", "PETG").
20
+ load_temp: The recommended loading temperature for the filament in Celsius. Defaults to 220°C.
21
+ """
22
+ self._name = name
23
+ self._load_temp = load_temp
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ """The name of the filament type."""
28
+ return self._name
29
+
30
+ @property
31
+ def load_temp(self) -> float:
32
+ """The recommended loading temperature for this filament in Celsius."""
33
+ return self._load_temp
34
+
35
+ def __repr__(self) -> str:
36
+ return f"Filament(name='{self._name}', load_temp={self._load_temp})"
37
+
38
+ def __str__(self) -> str:
39
+ return f"{self._name} @ {self._load_temp}°C"
@@ -0,0 +1,8 @@
1
+ """
2
+ FlashForge Python API - Miscellaneous Utilities
3
+ """
4
+
5
+ from .temperature import Temperature
6
+ from .scientific_notation import format_scientific_notation
7
+
8
+ __all__ = ["Temperature", "format_scientific_notation"]
@@ -0,0 +1,28 @@
1
+ """
2
+ FlashForge Python API - Scientific Notation Formatter
3
+ """
4
+
5
+
6
+ def format_scientific_notation(value: float) -> str:
7
+ """
8
+ Formats a number into a string, using scientific notation if the number is
9
+ very small (absolute value < 0.001) or very large (absolute value >= 10000).
10
+ Otherwise, it returns the standard string representation of the number.
11
+
12
+ Args:
13
+ value: The number to format.
14
+
15
+ Returns:
16
+ A string representation of the number, potentially in scientific notation.
17
+
18
+ Examples:
19
+ >>> format_scientific_notation(0.000123)
20
+ '1.23e-04'
21
+ >>> format_scientific_notation(12345)
22
+ '1.2345e+04'
23
+ >>> format_scientific_notation(12.34)
24
+ '12.34'
25
+ """
26
+ if abs(value) < 0.001 or abs(value) >= 10000:
27
+ return f"{value:e}"
28
+ return str(value)
@@ -0,0 +1,42 @@
1
+ """
2
+ FlashForge Python API - Temperature Utility Class
3
+ """
4
+
5
+
6
+ class Temperature:
7
+ """
8
+ Represents a temperature value.
9
+ This class is a simple wrapper around a numeric temperature value,
10
+ providing methods to get the value and its string representation.
11
+ It's a general temperature representation that can be used for various purposes.
12
+ """
13
+
14
+ def __init__(self, value: float):
15
+ """
16
+ Creates an instance of the Temperature class.
17
+
18
+ Args:
19
+ value: The numeric temperature value, typically in Celsius.
20
+ """
21
+ self._value = value
22
+
23
+ def get_value(self) -> float:
24
+ """
25
+ Gets the numeric temperature value.
26
+
27
+ Returns:
28
+ The temperature value.
29
+ """
30
+ return self._value
31
+
32
+ def __str__(self) -> str:
33
+ """
34
+ Gets the string representation of the temperature value.
35
+
36
+ Returns:
37
+ The temperature value as a string.
38
+ """
39
+ return str(self._value)
40
+
41
+ def __repr__(self) -> str:
42
+ return f"Temperature({self._value})"
@@ -0,0 +1,10 @@
1
+ """
2
+ FlashForge Python API - Network Package
3
+ """
4
+ from .utils import NetworkUtils
5
+ from .fnet_code import FNetCode
6
+
7
+ __all__ = [
8
+ "NetworkUtils",
9
+ "FNetCode",
10
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ FlashForge Python API - Network Operation Codes
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class FNetCode(Enum):
9
+ """
10
+ Represents network operation codes, typically used in API responses
11
+ to indicate the success or failure of a requested operation.
12
+ """
13
+
14
+ OK = 0 # Indicates that the network operation was successful
15
+ ERROR = 1 # Indicates that an error occurred during the network operation
@@ -0,0 +1,55 @@
1
+ """
2
+ FlashForge Python API - Network Utilities
3
+ """
4
+ from typing import Optional, Union
5
+
6
+ from ...models.responses import GenericResponse
7
+
8
+
9
+ class NetworkUtils:
10
+ """Utility class for handling network responses and status codes."""
11
+
12
+ @staticmethod
13
+ def is_ok(response: Optional[Union[GenericResponse, dict]]) -> bool:
14
+ """
15
+ Checks if a response indicates success.
16
+
17
+ Args:
18
+ response: The response object to check
19
+
20
+ Returns:
21
+ True if the response indicates success, False otherwise
22
+ """
23
+ if response is None:
24
+ return False
25
+
26
+ # Handle dictionary responses
27
+ if isinstance(response, dict):
28
+ code = response.get("code", -1)
29
+ else:
30
+ # Handle GenericResponse objects
31
+ code = getattr(response, "code", -1)
32
+
33
+ # Success codes: 0 or 200
34
+ return code in (0, 200)
35
+
36
+ @staticmethod
37
+ def get_error_message(response: Optional[Union[GenericResponse, dict]]) -> str:
38
+ """
39
+ Extracts error message from a response.
40
+
41
+ Args:
42
+ response: The response object to extract message from
43
+
44
+ Returns:
45
+ Error message string, or empty string if none found
46
+ """
47
+ if response is None:
48
+ return "No response received"
49
+
50
+ # Handle dictionary responses
51
+ if isinstance(response, dict):
52
+ return response.get("message", "Unknown error")
53
+ else:
54
+ # Handle GenericResponse objects
55
+ return getattr(response, "message", "Unknown error")
flashforge/client.py ADDED
@@ -0,0 +1,381 @@
1
+ """
2
+ FlashForge Python API - Main Unified Client
3
+
4
+ This module provides the main FlashForgeClient class that orchestrates both HTTP and TCP
5
+ communication layers for controlling FlashForge 3D printers.
6
+ """
7
+ import asyncio
8
+ from typing import Optional
9
+
10
+ import aiohttp
11
+
12
+ from .api.constants.endpoints import Endpoints
13
+ from .api.controls import Control, Files, Info, JobControl, TempControl
14
+ from .api.controls.info import MachineInfoParser
15
+ from .api.network.utils import NetworkUtils
16
+ from .models import FFMachineInfo, ProductResponse
17
+ from .tcp import FlashForgeClient as TcpClient
18
+ from .tcp import PrinterInfo
19
+
20
+
21
+ class FlashForgeClient:
22
+ """
23
+ Main client for interacting with a FlashForge 3D printer.
24
+
25
+ This class provides methods for controlling the printer, managing print jobs,
26
+ retrieving information, and handling file operations. It orchestrates both
27
+ HTTP and TCP communication layers to provide a unified interface.
28
+ """
29
+
30
+ def __init__(self, ip_address: str, serial_number: str, check_code: str):
31
+ """
32
+ Creates an instance of FlashForgeClient.
33
+
34
+ Args:
35
+ ip_address: The IP address of the printer
36
+ serial_number: The serial number of the printer
37
+ check_code: The check code for the printer
38
+ """
39
+ # Connection parameters
40
+ self.ip_address = ip_address
41
+ self.serial_number = serial_number
42
+ self.check_code = check_code
43
+
44
+ # Constants
45
+ self._PORT = 8898
46
+ self._HTTP_TIMEOUT = 5.0
47
+
48
+ # HTTP client state
49
+ self._http_session: Optional[aiohttp.ClientSession] = None
50
+ self._http_client_busy = False
51
+
52
+ # TCP client setup
53
+ self.tcp_client = TcpClient(ip_address)
54
+
55
+ # Control instances
56
+ self.control = Control(self)
57
+ self.job_control = JobControl(self)
58
+ self.info = Info(self)
59
+ self.files = Files(self)
60
+ self.temp_control = TempControl(self)
61
+
62
+ # Printer information cache
63
+ self.printer_name: str = ""
64
+ self.is_pro: bool = False
65
+ self._is_ad5x: bool = False
66
+ self.firmware_version: str = ""
67
+ self.firmware_ver: str = ""
68
+ self.mac_address: str = ""
69
+ self.flash_cloud_code: str = ""
70
+ self.polar_cloud_code: str = ""
71
+ self.lifetime_print_time: str = ""
72
+ self.lifetime_filament_meters: str = ""
73
+
74
+ # Control states
75
+ self.led_control: bool = False
76
+ self.filtration_control: bool = False
77
+
78
+ @property
79
+ def is_ad5x(self) -> bool:
80
+ """
81
+ Indicates if the printer is an AD5X model.
82
+
83
+ Returns:
84
+ True if the printer is AD5X, False otherwise
85
+ """
86
+ return self._is_ad5x
87
+
88
+ async def __aenter__(self):
89
+ """Async context manager entry."""
90
+ await self._ensure_http_session()
91
+ return self
92
+
93
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
94
+ """Async context manager exit."""
95
+ await self.dispose()
96
+
97
+ async def _ensure_http_session(self) -> aiohttp.ClientSession:
98
+ """
99
+ Ensures that an HTTP session is available.
100
+
101
+ Returns:
102
+ The HTTP session instance
103
+ """
104
+ if self._http_session is None or self._http_session.closed:
105
+ timeout = aiohttp.ClientTimeout(total=self._HTTP_TIMEOUT)
106
+ self._http_session = aiohttp.ClientSession(
107
+ timeout=timeout,
108
+ headers={"Accept": "*/*"}
109
+ )
110
+ return self._http_session
111
+
112
+ async def initialize(self) -> bool:
113
+ """
114
+ Initializes the FlashForgeClient and verifies the connection to the printer.
115
+
116
+ Returns:
117
+ True if initialization is successful, False otherwise
118
+ """
119
+ connected = await self.verify_connection()
120
+ if connected:
121
+ return True
122
+ print("Failed to connect to printer")
123
+ return False
124
+
125
+ async def is_http_client_busy(self) -> bool:
126
+ """
127
+ Checks if the HTTP client is currently busy.
128
+
129
+ Returns:
130
+ True if the HTTP client is busy, False otherwise
131
+ """
132
+ # Wait a bit if busy to prevent tight loops
133
+ while self._http_client_busy:
134
+ await asyncio.sleep(0.01)
135
+ return self._http_client_busy
136
+
137
+ def release_http_client(self) -> None:
138
+ """Releases the HTTP client, allowing it to be used for new requests."""
139
+ self._http_client_busy = False
140
+
141
+ async def init_control(self) -> bool:
142
+ """
143
+ Initializes the control interface with the printer.
144
+
145
+ This involves sending a product command and initializing TCP control.
146
+
147
+ Returns:
148
+ True if control initialization is successful, False otherwise
149
+ """
150
+ if await self.send_product_command():
151
+ return await self.tcp_client.init_control()
152
+ print("New API control failed!")
153
+ return False
154
+
155
+ async def dispose(self) -> None:
156
+ """
157
+ Disposes of the FlashForgeClient instance, stopping keep-alive messages
158
+ and cleaning up resources.
159
+ """
160
+ # Stop TCP keep-alive and dispose
161
+ if hasattr(self.tcp_client, 'stop_keep_alive'):
162
+ await self.tcp_client.stop_keep_alive(True)
163
+ if hasattr(self.tcp_client, 'dispose'):
164
+ await self.tcp_client.dispose()
165
+
166
+ # Close HTTP session
167
+ if self._http_session and not self._http_session.closed:
168
+ await self._http_session.close()
169
+
170
+ def cache_details(self, info: Optional[FFMachineInfo]) -> bool:
171
+ """
172
+ Caches machine details from the provided FFMachineInfo object.
173
+
174
+ Args:
175
+ info: The FFMachineInfo object containing printer details
176
+
177
+ Returns:
178
+ True if caching is successful, False otherwise
179
+ """
180
+ if not info:
181
+ return False
182
+
183
+ # Cache all printer information
184
+ self.printer_name = info.name or ""
185
+ self.firmware_version = info.firmware_version or ""
186
+ self.firmware_ver = info.firmware_version.split('-')[0] if info.firmware_version else ""
187
+ self.mac_address = info.mac_address or ""
188
+ self.flash_cloud_code = info.flash_cloud_register_code or ""
189
+ self.polar_cloud_code = info.polar_cloud_register_code or ""
190
+ self.lifetime_print_time = info.formatted_total_run_time or ""
191
+ self._is_ad5x = info.is_ad5x
192
+
193
+ # Format filament usage
194
+ if info.cumulative_filament is not None:
195
+ self.lifetime_filament_meters = f"{info.cumulative_filament:.2f}m"
196
+ else:
197
+ self.lifetime_filament_meters = "0.00m"
198
+
199
+ return True
200
+
201
+ def get_endpoint(self, endpoint: str) -> str:
202
+ """
203
+ Constructs the full API endpoint URL.
204
+
205
+ Args:
206
+ endpoint: The specific API endpoint path
207
+
208
+ Returns:
209
+ The full URL for the API endpoint
210
+ """
211
+ return f"http://{self.ip_address}:{self._PORT}{endpoint}"
212
+
213
+ async def verify_connection(self) -> bool:
214
+ """
215
+ Verifies the connection to the printer by retrieving machine details and TCP information.
216
+
217
+ Returns:
218
+ True if the connection is verified, False otherwise
219
+ """
220
+ try:
221
+ # Get HTTP API response
222
+ response = await self.info.get_detail_response()
223
+ if not response or not NetworkUtils.is_ok(response):
224
+ print("Failed to get valid response from printer API")
225
+ return False
226
+
227
+ # Parse machine info from detail response
228
+ machine_info = MachineInfoParser.from_detail(response.detail)
229
+ if not machine_info:
230
+ print("Failed to parse machine info from detail response")
231
+ return False
232
+
233
+ # Get TCP printer information to check for Pro model
234
+ tcp_info: Optional[PrinterInfo] = await self.tcp_client.get_printer_info()
235
+ if tcp_info:
236
+ if "Pro" in tcp_info.type_name:
237
+ self.is_pro = True
238
+ else:
239
+ print("Warning: Unable to get PrinterInfo from TCP API, some features might not work")
240
+
241
+ # Cache the details
242
+ return self.cache_details(machine_info)
243
+
244
+ except Exception as error:
245
+ print(f"Error in verify_connection: {error}")
246
+ return False
247
+
248
+ async def send_product_command(self) -> bool:
249
+ """
250
+ Sends a product command to the printer to retrieve control states.
251
+
252
+ This method sets the http_client_busy flag while the request is in progress.
253
+
254
+ Returns:
255
+ True if the product command is sent successfully and valid data is received,
256
+ False otherwise
257
+ """
258
+ self._http_client_busy = True
259
+
260
+ payload = {
261
+ "serialNumber": self.serial_number,
262
+ "checkCode": self.check_code
263
+ }
264
+
265
+ try:
266
+ session = await self._ensure_http_session()
267
+ async with session.post(
268
+ self.get_endpoint(Endpoints.PRODUCT),
269
+ json=payload,
270
+ headers={"Content-Type": "application/json"}
271
+ ) as response:
272
+
273
+ if response.status != 200:
274
+ return False
275
+
276
+ # Fix for FlashForge printer's malformed Content-Type header
277
+ # Some printers return "appliation/json" instead of "application/json"
278
+ try:
279
+ data = await response.json()
280
+ except aiohttp.ContentTypeError:
281
+ # Fallback: manually parse as JSON if Content-Type is malformed
282
+ text = await response.text()
283
+ import json
284
+ data = json.loads(text)
285
+
286
+ # Validate response structure
287
+ if not NetworkUtils.is_ok(data):
288
+ return False
289
+
290
+ # Parse product response and set control states
291
+ product_response = ProductResponse(**data)
292
+ if product_response and product_response.product:
293
+ product = product_response.product
294
+ self.led_control = product.lightCtrlState != 0
295
+ self.filtration_control = not (
296
+ product.internalFanCtrlState == 0 or
297
+ product.externalFanCtrlState == 0
298
+ )
299
+ return True
300
+
301
+ except Exception as error:
302
+ print(f"Error in send_product_command: {error}")
303
+ return False
304
+ finally:
305
+ self._http_client_busy = False
306
+
307
+ return False
308
+
309
+ async def get_http_session(self) -> aiohttp.ClientSession:
310
+ """
311
+ Gets the HTTP session for making requests.
312
+
313
+ Returns:
314
+ The HTTP session instance
315
+ """
316
+ return await self._ensure_http_session()
317
+
318
+ # Additional convenience methods for direct access to common operations
319
+
320
+ async def get_printer_status(self) -> Optional[FFMachineInfo]:
321
+ """
322
+ Gets the current printer status and information.
323
+
324
+ Returns:
325
+ FFMachineInfo object with current printer status, or None if failed
326
+ """
327
+ return await self.info.get()
328
+
329
+ async def get_temperatures(self):
330
+ """
331
+ Gets current temperature readings from the printer.
332
+
333
+ Returns:
334
+ Temperature information from the TCP client
335
+ """
336
+ return await self.tcp_client.get_temp_info()
337
+
338
+ async def home_all_axes(self) -> bool:
339
+ """
340
+ Homes all axes (X, Y, Z) of the printer.
341
+
342
+ Returns:
343
+ True if successful, False otherwise
344
+ """
345
+ return await self.control.home_axes()
346
+
347
+ async def emergency_stop(self) -> bool:
348
+ """
349
+ Performs an emergency stop of the printer.
350
+
351
+ Returns:
352
+ True if successful, False otherwise
353
+ """
354
+ return await self.job_control.cancel_print_job()
355
+
356
+ async def pause_print(self) -> bool:
357
+ """
358
+ Pauses the current print job.
359
+
360
+ Returns:
361
+ True if successful, False otherwise
362
+ """
363
+ return await self.job_control.pause_print_job()
364
+
365
+ async def resume_print(self) -> bool:
366
+ """
367
+ Resumes a paused print job.
368
+
369
+ Returns:
370
+ True if successful, False otherwise
371
+ """
372
+ return await self.job_control.resume_print_job()
373
+
374
+ def __repr__(self) -> str:
375
+ """String representation of the client."""
376
+ return (
377
+ f"FlashForgeClient(ip={self.ip_address}, "
378
+ f"printer='{self.printer_name}', "
379
+ f"pro={self.is_pro}, "
380
+ f"firmware='{self.firmware_ver}')"
381
+ )
@@ -0,0 +1,12 @@
1
+ """
2
+ FlashForge Python API - Discovery Package
3
+
4
+ UDP-based printer discovery for finding FlashForge printers on the local network.
5
+ """
6
+
7
+ from .discovery import FlashForgePrinter, FlashForgePrinterDiscovery
8
+
9
+ __all__ = [
10
+ 'FlashForgePrinter',
11
+ 'FlashForgePrinterDiscovery',
12
+ ]