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.
- flashforge/__init__.py +70 -29
- flashforge/api/__init__.py +1 -0
- flashforge/api/constants/__init__.py +1 -0
- flashforge/api/constants/commands.py +1 -0
- flashforge/api/constants/endpoints.py +7 -0
- flashforge/api/controls/__init__.py +1 -0
- flashforge/api/controls/control.py +64 -61
- flashforge/api/controls/files.py +28 -26
- flashforge/api/controls/info.py +84 -85
- flashforge/api/controls/job_control.py +120 -82
- flashforge/api/controls/temp_control.py +14 -11
- flashforge/api/misc/__init__.py +1 -1
- flashforge/api/network/__init__.py +2 -1
- flashforge/api/network/utils.py +7 -8
- flashforge/client.py +185 -64
- flashforge/discovery/__init__.py +30 -3
- flashforge/discovery/discovery.py +641 -308
- flashforge/models/__init__.py +11 -10
- flashforge/models/machine_info.py +225 -100
- flashforge/models/responses.py +103 -43
- flashforge/tcp/__init__.py +30 -17
- flashforge/tcp/a3_client.py +408 -0
- flashforge/tcp/a4_client.py +300 -0
- flashforge/tcp/ff_client.py +93 -90
- flashforge/tcp/gcode/__init__.py +4 -2
- flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
- flashforge/tcp/gcode/gcode_controller.py +48 -36
- flashforge/tcp/gcode/gcodes.py +7 -1
- flashforge/tcp/parsers/__init__.py +11 -11
- flashforge/tcp/parsers/endstop_status.py +103 -132
- flashforge/tcp/parsers/location_info.py +26 -13
- flashforge/tcp/parsers/print_status.py +49 -56
- flashforge/tcp/parsers/printer_info.py +38 -48
- flashforge/tcp/parsers/temp_info.py +46 -47
- flashforge/tcp/parsers/thumbnail_info.py +65 -40
- flashforge/tcp/tcp_client.py +296 -293
- flashforge_python_api-1.2.0.dist-info/METADATA +133 -0
- flashforge_python_api-1.2.0.dist-info/RECORD +46 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/WHEEL +1 -1
- flashforge_python_api-1.0.2.dist-info/METADATA +0 -284
- flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
FlashForge Python API - Job Control Module
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import base64
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
import aiohttp
|
|
11
12
|
|
|
12
|
-
from ...models.responses import
|
|
13
|
+
from ...models.responses import (
|
|
14
|
+
AD5XLocalJobParams,
|
|
15
|
+
AD5XMaterialMapping,
|
|
16
|
+
AD5XSingleColorJobParams,
|
|
17
|
+
AD5XUploadParams,
|
|
18
|
+
)
|
|
13
19
|
from ..constants.endpoints import Endpoints
|
|
14
20
|
from ..network.utils import NetworkUtils
|
|
15
21
|
|
|
@@ -28,12 +34,12 @@ class JobControl:
|
|
|
28
34
|
def __init__(self, client: "FlashForgeClient"):
|
|
29
35
|
"""
|
|
30
36
|
Creates an instance of the JobControl class.
|
|
31
|
-
|
|
37
|
+
|
|
32
38
|
Args:
|
|
33
39
|
client: The FlashForgeClient instance used for communication with the printer.
|
|
34
40
|
"""
|
|
35
41
|
self.client = client
|
|
36
|
-
self._control:
|
|
42
|
+
self._control: Control | None = None
|
|
37
43
|
|
|
38
44
|
@property
|
|
39
45
|
def control(self) -> "Control":
|
|
@@ -45,7 +51,7 @@ class JobControl:
|
|
|
45
51
|
async def pause_print_job(self) -> bool:
|
|
46
52
|
"""
|
|
47
53
|
Pauses the current print job.
|
|
48
|
-
|
|
54
|
+
|
|
49
55
|
Returns:
|
|
50
56
|
True if the command is successful, False otherwise.
|
|
51
57
|
"""
|
|
@@ -54,7 +60,7 @@ class JobControl:
|
|
|
54
60
|
async def resume_print_job(self) -> bool:
|
|
55
61
|
"""
|
|
56
62
|
Resumes a paused print job.
|
|
57
|
-
|
|
63
|
+
|
|
58
64
|
Returns:
|
|
59
65
|
True if the command is successful, False otherwise.
|
|
60
66
|
"""
|
|
@@ -63,7 +69,7 @@ class JobControl:
|
|
|
63
69
|
async def cancel_print_job(self) -> bool:
|
|
64
70
|
"""
|
|
65
71
|
Cancels the current print job.
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
Returns:
|
|
68
74
|
True if the command is successful, False otherwise.
|
|
69
75
|
"""
|
|
@@ -73,16 +79,16 @@ class JobControl:
|
|
|
73
79
|
"""
|
|
74
80
|
Checks if the printer's firmware version is 3.1.3 or newer.
|
|
75
81
|
This is used to determine which API payload format to use for certain commands.
|
|
76
|
-
|
|
82
|
+
|
|
77
83
|
Returns:
|
|
78
84
|
True if the firmware is new (>= 3.1.3), False otherwise or if version cannot be determined.
|
|
79
85
|
"""
|
|
80
86
|
try:
|
|
81
|
-
current_version = self.client.firmware_ver.split(
|
|
87
|
+
current_version = self.client.firmware_ver.split(".")
|
|
82
88
|
min_version = [3, 1, 3]
|
|
83
89
|
|
|
84
90
|
for i in range(3):
|
|
85
|
-
current = int(current_version[i] if i < len(current_version) else
|
|
91
|
+
current = int(current_version[i] if i < len(current_version) else "0")
|
|
86
92
|
if current > min_version[i]:
|
|
87
93
|
return True
|
|
88
94
|
if current < min_version[i]:
|
|
@@ -95,26 +101,26 @@ class JobControl:
|
|
|
95
101
|
async def clear_platform(self) -> bool:
|
|
96
102
|
"""
|
|
97
103
|
Sends a command to clear the printer's build platform.
|
|
98
|
-
|
|
104
|
+
|
|
99
105
|
Returns:
|
|
100
106
|
True if the command is successful, False otherwise.
|
|
101
107
|
"""
|
|
102
|
-
args = {
|
|
103
|
-
"action": "setClearPlatform"
|
|
104
|
-
}
|
|
108
|
+
args = {"action": "setClearPlatform"}
|
|
105
109
|
|
|
106
110
|
return await self.control.send_control_command("stateCtrl_cmd", args)
|
|
107
111
|
|
|
108
|
-
async def upload_file(
|
|
112
|
+
async def upload_file(
|
|
113
|
+
self, file_path: str, start_print: bool, level_before_print: bool
|
|
114
|
+
) -> bool:
|
|
109
115
|
"""
|
|
110
116
|
Uploads a G-code or 3MF file to the printer and optionally starts printing.
|
|
111
117
|
It handles different API requirements based on the printer's firmware version.
|
|
112
|
-
|
|
118
|
+
|
|
113
119
|
Args:
|
|
114
120
|
file_path: The local path to the G-code or 3MF file to upload.
|
|
115
121
|
start_print: If True, the printer will start printing the file immediately after upload.
|
|
116
122
|
level_before_print: If True, the printer will perform bed leveling before starting the print.
|
|
117
|
-
|
|
123
|
+
|
|
118
124
|
Returns:
|
|
119
125
|
True if the file upload (and optional print start) is successful, False otherwise.
|
|
120
126
|
"""
|
|
@@ -127,27 +133,29 @@ class JobControl:
|
|
|
127
133
|
file_size = file_path_obj.stat().st_size
|
|
128
134
|
file_name = file_path_obj.name
|
|
129
135
|
|
|
130
|
-
print(
|
|
136
|
+
print(
|
|
137
|
+
f"Starting upload for {file_name}, Size: {file_size}, Start: {start_print}, Level: {level_before_print}"
|
|
138
|
+
)
|
|
131
139
|
|
|
132
140
|
try:
|
|
133
141
|
# Prepare the custom HTTP headers with metadata
|
|
134
142
|
custom_headers = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
"serialNumber": self.client.serial_number,
|
|
144
|
+
"checkCode": self.client.check_code,
|
|
145
|
+
"fileSize": str(file_size),
|
|
146
|
+
"printNow": str(start_print).lower(),
|
|
147
|
+
"levelingBeforePrint": str(level_before_print).lower(),
|
|
148
|
+
"Expect": "100-continue",
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
# Add additional headers for new firmware
|
|
144
152
|
if self._is_new_firmware_version():
|
|
145
153
|
print("Using new firmware headers for upload.")
|
|
146
|
-
custom_headers[
|
|
147
|
-
custom_headers[
|
|
148
|
-
custom_headers[
|
|
154
|
+
custom_headers["flowCalibration"] = "false"
|
|
155
|
+
custom_headers["useMatlStation"] = "false"
|
|
156
|
+
custom_headers["gcodeToolCnt"] = "0"
|
|
149
157
|
# Base64 encode "[]" which is "W10="
|
|
150
|
-
custom_headers[
|
|
158
|
+
custom_headers["materialMappings"] = "W10="
|
|
151
159
|
else:
|
|
152
160
|
print("Using old firmware headers for upload.")
|
|
153
161
|
|
|
@@ -155,14 +163,16 @@ class JobControl:
|
|
|
155
163
|
|
|
156
164
|
# Create multipart form data
|
|
157
165
|
async with aiohttp.ClientSession() as session:
|
|
158
|
-
with open(file_path,
|
|
166
|
+
with open(file_path, "rb") as f:
|
|
159
167
|
data = aiohttp.FormData()
|
|
160
|
-
data.add_field(
|
|
168
|
+
data.add_field(
|
|
169
|
+
"gcodeFile", f, filename=file_name, content_type="application/octet-stream"
|
|
170
|
+
)
|
|
161
171
|
|
|
162
172
|
async with session.post(
|
|
163
173
|
self.client.get_endpoint(Endpoints.UPLOAD_FILE),
|
|
164
174
|
data=data,
|
|
165
|
-
headers=custom_headers
|
|
175
|
+
headers=custom_headers,
|
|
166
176
|
) as response:
|
|
167
177
|
print(f"Upload Response Status: {response.status}")
|
|
168
178
|
|
|
@@ -178,15 +188,18 @@ class JobControl:
|
|
|
178
188
|
# Fallback: manually parse as JSON if Content-Type is malformed
|
|
179
189
|
text = await response.text()
|
|
180
190
|
import json
|
|
191
|
+
|
|
181
192
|
result = json.loads(text)
|
|
182
|
-
|
|
193
|
+
|
|
183
194
|
print("Upload Response Data:", result)
|
|
184
195
|
|
|
185
196
|
if NetworkUtils.is_ok(result):
|
|
186
197
|
print("Upload successful according to printer response.")
|
|
187
198
|
return True
|
|
188
199
|
else:
|
|
189
|
-
print(
|
|
200
|
+
print(
|
|
201
|
+
f"Upload failed: Printer response code={result.get('code')}, message={result.get('message')}"
|
|
202
|
+
)
|
|
190
203
|
return False
|
|
191
204
|
|
|
192
205
|
except Exception as e:
|
|
@@ -197,11 +210,11 @@ class JobControl:
|
|
|
197
210
|
"""
|
|
198
211
|
Starts printing a file that is already stored locally on the printer.
|
|
199
212
|
It handles different API payload formats based on the printer's firmware version.
|
|
200
|
-
|
|
213
|
+
|
|
201
214
|
Args:
|
|
202
215
|
file_name: The name of the file on the printer (e.g., "my_model.gcode") to print.
|
|
203
216
|
leveling_before_print: If True, the printer will perform bed leveling before starting the print.
|
|
204
|
-
|
|
217
|
+
|
|
205
218
|
Returns:
|
|
206
219
|
True if the print command is successfully sent and acknowledged, False otherwise.
|
|
207
220
|
"""
|
|
@@ -215,7 +228,7 @@ class JobControl:
|
|
|
215
228
|
"flowCalibration": False,
|
|
216
229
|
"useMatlStation": False,
|
|
217
230
|
"gcodeToolCnt": 0,
|
|
218
|
-
"materialMappings": [] # Empty array for materialMappings
|
|
231
|
+
"materialMappings": [], # Empty array for materialMappings
|
|
219
232
|
}
|
|
220
233
|
else:
|
|
221
234
|
# Old format for firmware < 3.1.3
|
|
@@ -223,7 +236,7 @@ class JobControl:
|
|
|
223
236
|
"serialNumber": self.client.serial_number,
|
|
224
237
|
"checkCode": self.client.check_code,
|
|
225
238
|
"fileName": file_name,
|
|
226
|
-
"levelingBeforePrint": leveling_before_print
|
|
239
|
+
"levelingBeforePrint": leveling_before_print,
|
|
227
240
|
}
|
|
228
241
|
|
|
229
242
|
try:
|
|
@@ -231,7 +244,7 @@ class JobControl:
|
|
|
231
244
|
async with session.post(
|
|
232
245
|
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
233
246
|
json=payload,
|
|
234
|
-
headers={"Content-Type": "application/json"}
|
|
247
|
+
headers={"Content-Type": "application/json"},
|
|
235
248
|
) as response:
|
|
236
249
|
if response.status != 200:
|
|
237
250
|
return False
|
|
@@ -244,8 +257,9 @@ class JobControl:
|
|
|
244
257
|
# Fallback: manually parse as JSON if Content-Type is malformed
|
|
245
258
|
text = await response.text()
|
|
246
259
|
import json
|
|
260
|
+
|
|
247
261
|
result = json.loads(text)
|
|
248
|
-
|
|
262
|
+
|
|
249
263
|
return NetworkUtils.is_ok(result)
|
|
250
264
|
|
|
251
265
|
except Exception as error:
|
|
@@ -281,45 +295,53 @@ class JobControl:
|
|
|
281
295
|
file_size = file_path_obj.stat().st_size
|
|
282
296
|
file_name = file_path_obj.name
|
|
283
297
|
|
|
284
|
-
print(
|
|
298
|
+
print(
|
|
299
|
+
f"Starting AD5X upload for {file_name}, Size: {file_size}, Start: {params.start_print}, Level: {params.leveling_before_print}, Tools: {len(params.material_mappings)}"
|
|
300
|
+
)
|
|
285
301
|
|
|
286
302
|
try:
|
|
287
303
|
# Encode material mappings to base64
|
|
288
|
-
material_mappings_base64 = self._encode_material_mappings_to_base64(
|
|
304
|
+
material_mappings_base64 = self._encode_material_mappings_to_base64(
|
|
305
|
+
params.material_mappings
|
|
306
|
+
)
|
|
289
307
|
|
|
290
308
|
# Prepare AD5X-specific HTTP headers
|
|
291
309
|
custom_headers = {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
"serialNumber": self.client.serial_number,
|
|
311
|
+
"checkCode": self.client.check_code,
|
|
312
|
+
"fileSize": str(file_size),
|
|
313
|
+
"printNow": str(params.start_print).lower(),
|
|
314
|
+
"levelingBeforePrint": str(params.leveling_before_print).lower(),
|
|
315
|
+
"flowCalibration": str(params.flow_calibration).lower(),
|
|
316
|
+
"firstLayerInspection": str(params.first_layer_inspection).lower(),
|
|
317
|
+
"timeLapseVideo": str(params.time_lapse_video).lower(),
|
|
318
|
+
"useMatlStation": "true", # Always true for AD5X uploads with material mappings
|
|
319
|
+
"gcodeToolCnt": str(len(params.material_mappings)),
|
|
320
|
+
"materialMappings": material_mappings_base64,
|
|
321
|
+
"Expect": "100-continue",
|
|
304
322
|
}
|
|
305
323
|
|
|
306
324
|
print("AD5X Upload Request Headers:", custom_headers)
|
|
307
325
|
|
|
308
326
|
# Create multipart form data
|
|
309
327
|
async with aiohttp.ClientSession() as session:
|
|
310
|
-
with open(params.file_path,
|
|
328
|
+
with open(params.file_path, "rb") as f:
|
|
311
329
|
data = aiohttp.FormData()
|
|
312
|
-
data.add_field(
|
|
330
|
+
data.add_field(
|
|
331
|
+
"gcodeFile", f, filename=file_name, content_type="application/octet-stream"
|
|
332
|
+
)
|
|
313
333
|
|
|
314
334
|
async with session.post(
|
|
315
335
|
self.client.get_endpoint(Endpoints.UPLOAD_FILE),
|
|
316
336
|
data=data,
|
|
317
|
-
headers=custom_headers
|
|
337
|
+
headers=custom_headers,
|
|
318
338
|
) as response:
|
|
319
339
|
print(f"AD5X Upload Response Status: {response.status}")
|
|
320
340
|
|
|
321
341
|
if response.status != 200:
|
|
322
|
-
print(
|
|
342
|
+
print(
|
|
343
|
+
f"AD5X Upload failed: Printer responded with status {response.status}"
|
|
344
|
+
)
|
|
323
345
|
return False
|
|
324
346
|
|
|
325
347
|
# Fix for FlashForge printer's malformed Content-Type header
|
|
@@ -335,7 +357,9 @@ class JobControl:
|
|
|
335
357
|
print("AD5X Upload successful according to printer response.")
|
|
336
358
|
return True
|
|
337
359
|
else:
|
|
338
|
-
print(
|
|
360
|
+
print(
|
|
361
|
+
f"AD5X Upload failed: Printer response code={result.get('code')}, message={result.get('message')}"
|
|
362
|
+
)
|
|
339
363
|
return False
|
|
340
364
|
|
|
341
365
|
except Exception as e:
|
|
@@ -363,8 +387,8 @@ class JobControl:
|
|
|
363
387
|
return False
|
|
364
388
|
|
|
365
389
|
# Validate file name
|
|
366
|
-
if not params.file_name or params.file_name.strip() ==
|
|
367
|
-
print(
|
|
390
|
+
if not params.file_name or params.file_name.strip() == "":
|
|
391
|
+
print("AD5X Multi-Color Job error: fileName cannot be empty")
|
|
368
392
|
return False
|
|
369
393
|
|
|
370
394
|
# Create payload with AD5X-specific parameters
|
|
@@ -384,10 +408,10 @@ class JobControl:
|
|
|
384
408
|
"slotId": m.slot_id,
|
|
385
409
|
"materialName": m.material_name,
|
|
386
410
|
"toolMaterialColor": m.tool_material_color,
|
|
387
|
-
"slotMaterialColor": m.slot_material_color
|
|
411
|
+
"slotMaterialColor": m.slot_material_color,
|
|
388
412
|
}
|
|
389
413
|
for m in params.material_mappings
|
|
390
|
-
]
|
|
414
|
+
],
|
|
391
415
|
}
|
|
392
416
|
|
|
393
417
|
try:
|
|
@@ -395,7 +419,7 @@ class JobControl:
|
|
|
395
419
|
async with session.post(
|
|
396
420
|
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
397
421
|
json=payload,
|
|
398
|
-
headers={"Content-Type": "application/json"}
|
|
422
|
+
headers={"Content-Type": "application/json"},
|
|
399
423
|
) as response:
|
|
400
424
|
if response.status != 200:
|
|
401
425
|
return False
|
|
@@ -430,8 +454,8 @@ class JobControl:
|
|
|
430
454
|
return False
|
|
431
455
|
|
|
432
456
|
# Validate file name
|
|
433
|
-
if not params.file_name or params.file_name.strip() ==
|
|
434
|
-
print(
|
|
457
|
+
if not params.file_name or params.file_name.strip() == "":
|
|
458
|
+
print("AD5X Single-Color Job error: fileName cannot be empty")
|
|
435
459
|
return False
|
|
436
460
|
|
|
437
461
|
# Create payload with AD5X-specific parameters for single-color printing
|
|
@@ -445,7 +469,7 @@ class JobControl:
|
|
|
445
469
|
"timeLapseVideo": False,
|
|
446
470
|
"useMatlStation": False, # Set to false for single-color jobs
|
|
447
471
|
"gcodeToolCnt": 0, # Set to 0 for single-color jobs
|
|
448
|
-
"materialMappings": [] # Empty array for single-color jobs
|
|
472
|
+
"materialMappings": [], # Empty array for single-color jobs
|
|
449
473
|
}
|
|
450
474
|
|
|
451
475
|
try:
|
|
@@ -453,7 +477,7 @@ class JobControl:
|
|
|
453
477
|
async with session.post(
|
|
454
478
|
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
455
479
|
json=payload,
|
|
456
|
-
headers={"Content-Type": "application/json"}
|
|
480
|
+
headers={"Content-Type": "application/json"},
|
|
457
481
|
) as response:
|
|
458
482
|
if response.status != 200:
|
|
459
483
|
return False
|
|
@@ -479,11 +503,13 @@ class JobControl:
|
|
|
479
503
|
True if the printer is AD5X, false otherwise
|
|
480
504
|
"""
|
|
481
505
|
if not self.client.is_ad5x:
|
|
482
|
-
print(
|
|
506
|
+
print("AD5X Job error: This method can only be used with AD5X printers")
|
|
483
507
|
return False
|
|
484
508
|
return True
|
|
485
509
|
|
|
486
|
-
def _encode_material_mappings_to_base64(
|
|
510
|
+
def _encode_material_mappings_to_base64(
|
|
511
|
+
self, material_mappings: list[AD5XMaterialMapping]
|
|
512
|
+
) -> str:
|
|
487
513
|
"""
|
|
488
514
|
Encodes material mappings array to base64 string for HTTP headers.
|
|
489
515
|
Converts AD5XMaterialMapping array to JSON and then to base64 encoding.
|
|
@@ -501,15 +527,15 @@ class JobControl:
|
|
|
501
527
|
"slotId": m.slot_id,
|
|
502
528
|
"materialName": m.material_name,
|
|
503
529
|
"toolMaterialColor": m.tool_material_color,
|
|
504
|
-
"slotMaterialColor": m.slot_material_color
|
|
530
|
+
"slotMaterialColor": m.slot_material_color,
|
|
505
531
|
}
|
|
506
532
|
for m in material_mappings
|
|
507
533
|
]
|
|
508
534
|
json_string = json.dumps(json_array)
|
|
509
|
-
return base64.b64encode(json_string.encode(
|
|
535
|
+
return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
|
|
510
536
|
except Exception as error:
|
|
511
|
-
print(
|
|
512
|
-
raise Exception(
|
|
537
|
+
print("Failed to encode material mappings to base64:", error)
|
|
538
|
+
raise Exception("Failed to encode material mappings for upload") from error # noqa: B904
|
|
513
539
|
|
|
514
540
|
def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
|
|
515
541
|
"""
|
|
@@ -523,39 +549,51 @@ class JobControl:
|
|
|
523
549
|
True if all mappings are valid, false otherwise
|
|
524
550
|
"""
|
|
525
551
|
if not material_mappings or len(material_mappings) == 0:
|
|
526
|
-
print(
|
|
552
|
+
print(
|
|
553
|
+
"Material mappings validation error: materialMappings array cannot be empty for multi-color jobs"
|
|
554
|
+
)
|
|
527
555
|
return False
|
|
528
556
|
|
|
529
557
|
if len(material_mappings) > 4:
|
|
530
|
-
print(
|
|
558
|
+
print("Material mappings validation error: Maximum 4 material mappings allowed")
|
|
531
559
|
return False
|
|
532
560
|
|
|
533
|
-
hex_color_regex = re.compile(r
|
|
561
|
+
hex_color_regex = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
534
562
|
|
|
535
563
|
for i, mapping in enumerate(material_mappings):
|
|
536
564
|
# Validate toolId (0-3)
|
|
537
565
|
if mapping.tool_id < 0 or mapping.tool_id > 3:
|
|
538
|
-
print(
|
|
566
|
+
print(
|
|
567
|
+
f"Material mappings validation error: toolId must be between 0-3, got {mapping.tool_id} at index {i}"
|
|
568
|
+
)
|
|
539
569
|
return False
|
|
540
570
|
|
|
541
571
|
# Validate slotId (1-4)
|
|
542
572
|
if mapping.slot_id < 1 or mapping.slot_id > 4:
|
|
543
|
-
print(
|
|
573
|
+
print(
|
|
574
|
+
f"Material mappings validation error: slotId must be between 1-4, got {mapping.slot_id} at index {i}"
|
|
575
|
+
)
|
|
544
576
|
return False
|
|
545
577
|
|
|
546
578
|
# Validate materialName is not empty
|
|
547
|
-
if not mapping.material_name or mapping.material_name.strip() ==
|
|
548
|
-
print(
|
|
579
|
+
if not mapping.material_name or mapping.material_name.strip() == "":
|
|
580
|
+
print(
|
|
581
|
+
f"Material mappings validation error: materialName cannot be empty at index {i}"
|
|
582
|
+
)
|
|
549
583
|
return False
|
|
550
584
|
|
|
551
585
|
# Validate toolMaterialColor format
|
|
552
586
|
if not hex_color_regex.match(mapping.tool_material_color):
|
|
553
|
-
print(
|
|
587
|
+
print(
|
|
588
|
+
f"Material mappings validation error: toolMaterialColor must be in #RRGGBB format, got {mapping.tool_material_color} at index {i}"
|
|
589
|
+
)
|
|
554
590
|
return False
|
|
555
591
|
|
|
556
592
|
# Validate slotMaterialColor format
|
|
557
593
|
if not hex_color_regex.match(mapping.slot_material_color):
|
|
558
|
-
print(
|
|
594
|
+
print(
|
|
595
|
+
f"Material mappings validation error: slotMaterialColor must be in #RRGGBB format, got {mapping.slot_material_color} at index {i}"
|
|
596
|
+
)
|
|
559
597
|
return False
|
|
560
598
|
|
|
561
599
|
return True
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
FlashForge Python API - Temperature Control Module
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
7
8
|
from ...client import FlashForgeClient
|
|
@@ -17,12 +18,12 @@ class TempControl:
|
|
|
17
18
|
def __init__(self, client: "FlashForgeClient"):
|
|
18
19
|
"""
|
|
19
20
|
Creates an instance of the TempControl class.
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
Args:
|
|
22
23
|
client: The FlashForgeClient instance used for communication with the printer.
|
|
23
24
|
"""
|
|
24
25
|
self.client = client
|
|
25
|
-
self._tcp_client:
|
|
26
|
+
self._tcp_client: TcpClient | None = None
|
|
26
27
|
|
|
27
28
|
@property
|
|
28
29
|
def tcp_client(self) -> "TcpClient":
|
|
@@ -34,11 +35,11 @@ class TempControl:
|
|
|
34
35
|
async def set_extruder_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
35
36
|
"""
|
|
36
37
|
Sets the target temperature for an extruder.
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
Args:
|
|
39
40
|
temperature: The target temperature in Celsius.
|
|
40
41
|
wait_for: Whether to wait for the heating operation to complete
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
Returns:
|
|
43
44
|
True if the command is successful, False otherwise.
|
|
44
45
|
"""
|
|
@@ -47,11 +48,11 @@ class TempControl:
|
|
|
47
48
|
async def set_bed_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
48
49
|
"""
|
|
49
50
|
Sets the target temperature for the print bed.
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
Args:
|
|
52
53
|
temperature: The target bed temperature in Celsius.
|
|
53
54
|
wait_for: Whether to wait for the heating operation to complete
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
Returns:
|
|
56
57
|
True if the command is successful, False otherwise.
|
|
57
58
|
"""
|
|
@@ -69,20 +70,22 @@ class TempControl:
|
|
|
69
70
|
async def cancel_bed_temp(self) -> bool:
|
|
70
71
|
"""
|
|
71
72
|
Cancels the heating of the print bed (sets target temperature to 0).
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
Returns:
|
|
74
75
|
True if the command is successful, False otherwise.
|
|
75
76
|
"""
|
|
76
77
|
return await self.tcp_client.cancel_bed_temp()
|
|
77
78
|
|
|
78
|
-
async def wait_for_part_cool(
|
|
79
|
+
async def wait_for_part_cool(
|
|
80
|
+
self, target_temp: float = 50.0, timeout_seconds: int = 1800
|
|
81
|
+
) -> bool:
|
|
79
82
|
"""
|
|
80
83
|
Waits for printer components to cool down to a safe temperature.
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
Args:
|
|
83
86
|
target_temp: The target temperature to wait for (default: 50°C).
|
|
84
87
|
timeout_seconds: Maximum time to wait in seconds (default: 30 minutes).
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
Returns:
|
|
87
90
|
True if components cooled to target temperature, False if timeout or error.
|
|
88
91
|
"""
|
flashforge/api/misc/__init__.py
CHANGED
flashforge/api/network/utils.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
FlashForge Python API - Network Utilities
|
|
3
3
|
"""
|
|
4
|
-
from typing import Optional, Union
|
|
5
4
|
|
|
6
5
|
from ...models.responses import GenericResponse
|
|
7
6
|
|
|
@@ -10,13 +9,13 @@ class NetworkUtils:
|
|
|
10
9
|
"""Utility class for handling network responses and status codes."""
|
|
11
10
|
|
|
12
11
|
@staticmethod
|
|
13
|
-
def is_ok(response:
|
|
12
|
+
def is_ok(response: GenericResponse | dict | None) -> bool:
|
|
14
13
|
"""
|
|
15
14
|
Checks if a response indicates success.
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
Args:
|
|
18
17
|
response: The response object to check
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
Returns:
|
|
21
20
|
True if the response indicates success, False otherwise
|
|
22
21
|
"""
|
|
@@ -34,13 +33,13 @@ class NetworkUtils:
|
|
|
34
33
|
return code in (0, 200)
|
|
35
34
|
|
|
36
35
|
@staticmethod
|
|
37
|
-
def get_error_message(response:
|
|
36
|
+
def get_error_message(response: GenericResponse | dict | None) -> str:
|
|
38
37
|
"""
|
|
39
38
|
Extracts error message from a response.
|
|
40
|
-
|
|
39
|
+
|
|
41
40
|
Args:
|
|
42
41
|
response: The response object to extract message from
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
Returns:
|
|
45
44
|
Error message string, or empty string if none found
|
|
46
45
|
"""
|
|
@@ -49,7 +48,7 @@ class NetworkUtils:
|
|
|
49
48
|
|
|
50
49
|
# Handle dictionary responses
|
|
51
50
|
if isinstance(response, dict):
|
|
52
|
-
return response.get("message", "Unknown error")
|
|
51
|
+
return str(response.get("message", "Unknown error"))
|
|
53
52
|
else:
|
|
54
53
|
# Handle GenericResponse objects
|
|
55
54
|
return getattr(response, "message", "Unknown error")
|