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.
- flashforge/__init__.py +188 -0
- flashforge/api/__init__.py +17 -0
- flashforge/api/constants/__init__.py +10 -0
- flashforge/api/constants/commands.py +10 -0
- flashforge/api/constants/endpoints.py +11 -0
- flashforge/api/controls/__init__.py +16 -0
- flashforge/api/controls/control.py +357 -0
- flashforge/api/controls/files.py +171 -0
- flashforge/api/controls/info.py +292 -0
- flashforge/api/controls/job_control.py +561 -0
- flashforge/api/controls/temp_control.py +89 -0
- flashforge/api/filament/__init__.py +7 -0
- flashforge/api/filament/filament.py +39 -0
- flashforge/api/misc/__init__.py +8 -0
- flashforge/api/misc/scientific_notation.py +28 -0
- flashforge/api/misc/temperature.py +42 -0
- flashforge/api/network/__init__.py +10 -0
- flashforge/api/network/fnet_code.py +15 -0
- flashforge/api/network/utils.py +55 -0
- flashforge/client.py +381 -0
- flashforge/discovery/__init__.py +12 -0
- flashforge/discovery/discovery.py +388 -0
- flashforge/models/__init__.py +50 -0
- flashforge/models/machine_info.py +247 -0
- flashforge/models/responses.py +123 -0
- flashforge/tcp/__init__.py +41 -0
- flashforge/tcp/ff_client.py +587 -0
- flashforge/tcp/gcode/__init__.py +11 -0
- flashforge/tcp/gcode/gcode_controller.py +296 -0
- flashforge/tcp/gcode/gcodes.py +93 -0
- flashforge/tcp/parsers/__init__.py +27 -0
- flashforge/tcp/parsers/endstop_status.py +273 -0
- flashforge/tcp/parsers/location_info.py +68 -0
- flashforge/tcp/parsers/print_status.py +182 -0
- flashforge/tcp/parsers/printer_info.py +154 -0
- flashforge/tcp/parsers/temp_info.py +217 -0
- flashforge/tcp/parsers/thumbnail_info.py +242 -0
- flashforge/tcp/tcp_client.py +448 -0
- flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
- flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
- flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
- flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
- flashforge_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Job Control Module
|
|
3
|
+
"""
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from ...models.responses import AD5XMaterialMapping, AD5XUploadParams, AD5XLocalJobParams, AD5XSingleColorJobParams
|
|
13
|
+
from ..constants.endpoints import Endpoints
|
|
14
|
+
from ..network.utils import NetworkUtils
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ...client import FlashForgeClient
|
|
18
|
+
from .control import Control
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JobControl:
|
|
22
|
+
"""
|
|
23
|
+
Provides methods for managing print jobs on the FlashForge 3D printer.
|
|
24
|
+
This includes pausing, resuming, canceling prints, uploading files for printing,
|
|
25
|
+
and starting prints from local files.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: "FlashForgeClient"):
|
|
29
|
+
"""
|
|
30
|
+
Creates an instance of the JobControl class.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
client: The FlashForgeClient instance used for communication with the printer.
|
|
34
|
+
"""
|
|
35
|
+
self.client = client
|
|
36
|
+
self._control: Optional[Control] = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def control(self) -> "Control":
|
|
40
|
+
"""Get the control instance."""
|
|
41
|
+
if self._control is None:
|
|
42
|
+
self._control = self.client.control
|
|
43
|
+
return self._control
|
|
44
|
+
|
|
45
|
+
async def pause_print_job(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Pauses the current print job.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if the command is successful, False otherwise.
|
|
51
|
+
"""
|
|
52
|
+
return await self.control.send_job_control_cmd("pause")
|
|
53
|
+
|
|
54
|
+
async def resume_print_job(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Resumes a paused print job.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if the command is successful, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
return await self.control.send_job_control_cmd("continue")
|
|
62
|
+
|
|
63
|
+
async def cancel_print_job(self) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Cancels the current print job.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if the command is successful, False otherwise.
|
|
69
|
+
"""
|
|
70
|
+
return await self.control.send_job_control_cmd("cancel")
|
|
71
|
+
|
|
72
|
+
def _is_new_firmware_version(self) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Checks if the printer's firmware version is 3.1.3 or newer.
|
|
75
|
+
This is used to determine which API payload format to use for certain commands.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the firmware is new (>= 3.1.3), False otherwise or if version cannot be determined.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
current_version = self.client.firmware_ver.split('.')
|
|
82
|
+
min_version = [3, 1, 3]
|
|
83
|
+
|
|
84
|
+
for i in range(3):
|
|
85
|
+
current = int(current_version[i] if i < len(current_version) else '0')
|
|
86
|
+
if current > min_version[i]:
|
|
87
|
+
return True
|
|
88
|
+
if current < min_version[i]:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
return True # Equal versions
|
|
92
|
+
except Exception:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def clear_platform(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Sends a command to clear the printer's build platform.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if the command is successful, False otherwise.
|
|
101
|
+
"""
|
|
102
|
+
args = {
|
|
103
|
+
"action": "setClearPlatform"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return await self.control.send_control_command("stateCtrl_cmd", args)
|
|
107
|
+
|
|
108
|
+
async def upload_file(self, file_path: str, start_print: bool, level_before_print: bool) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Uploads a G-code or 3MF file to the printer and optionally starts printing.
|
|
111
|
+
It handles different API requirements based on the printer's firmware version.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
file_path: The local path to the G-code or 3MF file to upload.
|
|
115
|
+
start_print: If True, the printer will start printing the file immediately after upload.
|
|
116
|
+
level_before_print: If True, the printer will perform bed leveling before starting the print.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if the file upload (and optional print start) is successful, False otherwise.
|
|
120
|
+
"""
|
|
121
|
+
file_path_obj = Path(file_path)
|
|
122
|
+
|
|
123
|
+
if not file_path_obj.exists():
|
|
124
|
+
print(f"UploadFile error: File not found at {file_path}")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
file_size = file_path_obj.stat().st_size
|
|
128
|
+
file_name = file_path_obj.name
|
|
129
|
+
|
|
130
|
+
print(f"Starting upload for {file_name}, Size: {file_size}, Start: {start_print}, Level: {level_before_print}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Prepare the custom HTTP headers with metadata
|
|
134
|
+
custom_headers = {
|
|
135
|
+
'serialNumber': self.client.serial_number,
|
|
136
|
+
'checkCode': self.client.check_code,
|
|
137
|
+
'fileSize': str(file_size),
|
|
138
|
+
'printNow': str(start_print).lower(),
|
|
139
|
+
'levelingBeforePrint': str(level_before_print).lower(),
|
|
140
|
+
'Expect': '100-continue'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Add additional headers for new firmware
|
|
144
|
+
if self._is_new_firmware_version():
|
|
145
|
+
print("Using new firmware headers for upload.")
|
|
146
|
+
custom_headers['flowCalibration'] = 'false'
|
|
147
|
+
custom_headers['useMatlStation'] = 'false'
|
|
148
|
+
custom_headers['gcodeToolCnt'] = '0'
|
|
149
|
+
# Base64 encode "[]" which is "W10="
|
|
150
|
+
custom_headers['materialMappings'] = 'W10='
|
|
151
|
+
else:
|
|
152
|
+
print("Using old firmware headers for upload.")
|
|
153
|
+
|
|
154
|
+
print("Upload Request Headers:", custom_headers)
|
|
155
|
+
|
|
156
|
+
# Create multipart form data
|
|
157
|
+
async with aiohttp.ClientSession() as session:
|
|
158
|
+
with open(file_path, 'rb') as f:
|
|
159
|
+
data = aiohttp.FormData()
|
|
160
|
+
data.add_field('gcodeFile', f, filename=file_name, content_type='application/octet-stream')
|
|
161
|
+
|
|
162
|
+
async with session.post(
|
|
163
|
+
self.client.get_endpoint(Endpoints.UPLOAD_FILE),
|
|
164
|
+
data=data,
|
|
165
|
+
headers=custom_headers
|
|
166
|
+
) as response:
|
|
167
|
+
print(f"Upload Response Status: {response.status}")
|
|
168
|
+
|
|
169
|
+
if response.status != 200:
|
|
170
|
+
print(f"Upload failed: Printer responded with status {response.status}")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Fix for FlashForge printer's malformed Content-Type header
|
|
174
|
+
# Some printers return "appliation/json" instead of "application/json"
|
|
175
|
+
try:
|
|
176
|
+
result = await response.json()
|
|
177
|
+
except aiohttp.ContentTypeError:
|
|
178
|
+
# Fallback: manually parse as JSON if Content-Type is malformed
|
|
179
|
+
text = await response.text()
|
|
180
|
+
import json
|
|
181
|
+
result = json.loads(text)
|
|
182
|
+
|
|
183
|
+
print("Upload Response Data:", result)
|
|
184
|
+
|
|
185
|
+
if NetworkUtils.is_ok(result):
|
|
186
|
+
print("Upload successful according to printer response.")
|
|
187
|
+
return True
|
|
188
|
+
else:
|
|
189
|
+
print(f"Upload failed: Printer response code={result.get('code')}, message={result.get('message')}")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"UploadFile error: {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
async def print_local_file(self, file_name: str, leveling_before_print: bool) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Starts printing a file that is already stored locally on the printer.
|
|
199
|
+
It handles different API payload formats based on the printer's firmware version.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
file_name: The name of the file on the printer (e.g., "my_model.gcode") to print.
|
|
203
|
+
leveling_before_print: If True, the printer will perform bed leveling before starting the print.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if the print command is successfully sent and acknowledged, False otherwise.
|
|
207
|
+
"""
|
|
208
|
+
if self._is_new_firmware_version():
|
|
209
|
+
# New format for firmware >= 3.1.3
|
|
210
|
+
payload = {
|
|
211
|
+
"serialNumber": self.client.serial_number,
|
|
212
|
+
"checkCode": self.client.check_code,
|
|
213
|
+
"fileName": file_name,
|
|
214
|
+
"levelingBeforePrint": leveling_before_print,
|
|
215
|
+
"flowCalibration": False,
|
|
216
|
+
"useMatlStation": False,
|
|
217
|
+
"gcodeToolCnt": 0,
|
|
218
|
+
"materialMappings": [] # Empty array for materialMappings
|
|
219
|
+
}
|
|
220
|
+
else:
|
|
221
|
+
# Old format for firmware < 3.1.3
|
|
222
|
+
payload = {
|
|
223
|
+
"serialNumber": self.client.serial_number,
|
|
224
|
+
"checkCode": self.client.check_code,
|
|
225
|
+
"fileName": file_name,
|
|
226
|
+
"levelingBeforePrint": leveling_before_print
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
async with aiohttp.ClientSession() as session:
|
|
231
|
+
async with session.post(
|
|
232
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
233
|
+
json=payload,
|
|
234
|
+
headers={"Content-Type": "application/json"}
|
|
235
|
+
) as response:
|
|
236
|
+
if response.status != 200:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
# Fix for FlashForge printer's malformed Content-Type header
|
|
240
|
+
# Some printers return "appliation/json" instead of "application/json"
|
|
241
|
+
try:
|
|
242
|
+
result = await response.json()
|
|
243
|
+
except aiohttp.ContentTypeError:
|
|
244
|
+
# Fallback: manually parse as JSON if Content-Type is malformed
|
|
245
|
+
text = await response.text()
|
|
246
|
+
import json
|
|
247
|
+
result = json.loads(text)
|
|
248
|
+
|
|
249
|
+
return NetworkUtils.is_ok(result)
|
|
250
|
+
|
|
251
|
+
except Exception as error:
|
|
252
|
+
print(f"PrintLocalFile error: {error}")
|
|
253
|
+
raise error
|
|
254
|
+
|
|
255
|
+
async def upload_file_ad5x(self, params: AD5XUploadParams) -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Uploads a G-code or 3MF file to AD5X printer with material station support.
|
|
258
|
+
Handles material mappings, flow calibration, and other AD5X-specific features.
|
|
259
|
+
Material mappings are base64-encoded in HTTP headers according to AD5X API requirements.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
params: AD5X upload parameters including file path, print options, and material mappings
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
True if the file upload is successful, False otherwise
|
|
266
|
+
"""
|
|
267
|
+
# Validate that this is an AD5X printer
|
|
268
|
+
if not self._validate_ad5x_printer():
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
# Validate material mappings
|
|
272
|
+
if not self._validate_material_mappings(params.material_mappings):
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
# Validate file exists
|
|
276
|
+
file_path_obj = Path(params.file_path)
|
|
277
|
+
if not file_path_obj.exists():
|
|
278
|
+
print(f"UploadFileAD5X error: File not found at {params.file_path}")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
file_size = file_path_obj.stat().st_size
|
|
282
|
+
file_name = file_path_obj.name
|
|
283
|
+
|
|
284
|
+
print(f"Starting AD5X upload for {file_name}, Size: {file_size}, Start: {params.start_print}, Level: {params.leveling_before_print}, Tools: {len(params.material_mappings)}")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Encode material mappings to base64
|
|
288
|
+
material_mappings_base64 = self._encode_material_mappings_to_base64(params.material_mappings)
|
|
289
|
+
|
|
290
|
+
# Prepare AD5X-specific HTTP headers
|
|
291
|
+
custom_headers = {
|
|
292
|
+
'serialNumber': self.client.serial_number,
|
|
293
|
+
'checkCode': self.client.check_code,
|
|
294
|
+
'fileSize': str(file_size),
|
|
295
|
+
'printNow': str(params.start_print).lower(),
|
|
296
|
+
'levelingBeforePrint': str(params.leveling_before_print).lower(),
|
|
297
|
+
'flowCalibration': str(params.flow_calibration).lower(),
|
|
298
|
+
'firstLayerInspection': str(params.first_layer_inspection).lower(),
|
|
299
|
+
'timeLapseVideo': str(params.time_lapse_video).lower(),
|
|
300
|
+
'useMatlStation': 'true', # Always true for AD5X uploads with material mappings
|
|
301
|
+
'gcodeToolCnt': str(len(params.material_mappings)),
|
|
302
|
+
'materialMappings': material_mappings_base64,
|
|
303
|
+
'Expect': '100-continue'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
print("AD5X Upload Request Headers:", custom_headers)
|
|
307
|
+
|
|
308
|
+
# Create multipart form data
|
|
309
|
+
async with aiohttp.ClientSession() as session:
|
|
310
|
+
with open(params.file_path, 'rb') as f:
|
|
311
|
+
data = aiohttp.FormData()
|
|
312
|
+
data.add_field('gcodeFile', f, filename=file_name, content_type='application/octet-stream')
|
|
313
|
+
|
|
314
|
+
async with session.post(
|
|
315
|
+
self.client.get_endpoint(Endpoints.UPLOAD_FILE),
|
|
316
|
+
data=data,
|
|
317
|
+
headers=custom_headers
|
|
318
|
+
) as response:
|
|
319
|
+
print(f"AD5X Upload Response Status: {response.status}")
|
|
320
|
+
|
|
321
|
+
if response.status != 200:
|
|
322
|
+
print(f"AD5X Upload failed: Printer responded with status {response.status}")
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
# Fix for FlashForge printer's malformed Content-Type header
|
|
326
|
+
try:
|
|
327
|
+
result = await response.json()
|
|
328
|
+
except aiohttp.ContentTypeError:
|
|
329
|
+
text = await response.text()
|
|
330
|
+
result = json.loads(text)
|
|
331
|
+
|
|
332
|
+
print("AD5X Upload Response Data:", result)
|
|
333
|
+
|
|
334
|
+
if NetworkUtils.is_ok(result):
|
|
335
|
+
print("AD5X Upload successful according to printer response.")
|
|
336
|
+
return True
|
|
337
|
+
else:
|
|
338
|
+
print(f"AD5X Upload failed: Printer response code={result.get('code')}, message={result.get('message')}")
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
print(f"UploadFileAD5X error: {e}")
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
async def start_ad5x_multi_color_job(self, params: AD5XLocalJobParams) -> bool:
|
|
346
|
+
"""
|
|
347
|
+
Starts a multi-color local print job on AD5X printers with material mappings.
|
|
348
|
+
This method automatically configures the material station settings and validates
|
|
349
|
+
all parameters before sending the print command.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
params: Job parameters including file name, leveling option, and material mappings
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
True if successful, False if validation fails or printer rejects
|
|
356
|
+
"""
|
|
357
|
+
# Validate that this is an AD5X printer
|
|
358
|
+
if not self._validate_ad5x_printer():
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
# Validate material mappings
|
|
362
|
+
if not self._validate_material_mappings(params.material_mappings):
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
# Validate file name
|
|
366
|
+
if not params.file_name or params.file_name.strip() == '':
|
|
367
|
+
print('AD5X Multi-Color Job error: fileName cannot be empty')
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
# Create payload with AD5X-specific parameters
|
|
371
|
+
payload = {
|
|
372
|
+
"serialNumber": self.client.serial_number,
|
|
373
|
+
"checkCode": self.client.check_code,
|
|
374
|
+
"fileName": params.file_name,
|
|
375
|
+
"levelingBeforePrint": params.leveling_before_print,
|
|
376
|
+
"firstLayerInspection": False,
|
|
377
|
+
"flowCalibration": False,
|
|
378
|
+
"timeLapseVideo": False,
|
|
379
|
+
"useMatlStation": True, # Automatically set to true for multi-color jobs
|
|
380
|
+
"gcodeToolCnt": len(params.material_mappings), # Set based on material mappings count
|
|
381
|
+
"materialMappings": [
|
|
382
|
+
{
|
|
383
|
+
"toolId": m.tool_id,
|
|
384
|
+
"slotId": m.slot_id,
|
|
385
|
+
"materialName": m.material_name,
|
|
386
|
+
"toolMaterialColor": m.tool_material_color,
|
|
387
|
+
"slotMaterialColor": m.slot_material_color
|
|
388
|
+
}
|
|
389
|
+
for m in params.material_mappings
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
async with aiohttp.ClientSession() as session:
|
|
395
|
+
async with session.post(
|
|
396
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
397
|
+
json=payload,
|
|
398
|
+
headers={"Content-Type": "application/json"}
|
|
399
|
+
) as response:
|
|
400
|
+
if response.status != 200:
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
# Fix for FlashForge printer's malformed Content-Type header
|
|
404
|
+
try:
|
|
405
|
+
result = await response.json()
|
|
406
|
+
except aiohttp.ContentTypeError:
|
|
407
|
+
text = await response.text()
|
|
408
|
+
result = json.loads(text)
|
|
409
|
+
|
|
410
|
+
return NetworkUtils.is_ok(result)
|
|
411
|
+
|
|
412
|
+
except Exception as error:
|
|
413
|
+
print(f"AD5X Multi-Color Job error: {error}")
|
|
414
|
+
raise error
|
|
415
|
+
|
|
416
|
+
async def start_ad5x_single_color_job(self, params: AD5XSingleColorJobParams) -> bool:
|
|
417
|
+
"""
|
|
418
|
+
Starts a single-color local print job on AD5X printers.
|
|
419
|
+
This method automatically configures the printer for single-color printing
|
|
420
|
+
without using the material station.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
params: Job parameters including file name and leveling option
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
True if successful, False if validation fails or printer rejects
|
|
427
|
+
"""
|
|
428
|
+
# Validate that this is an AD5X printer
|
|
429
|
+
if not self._validate_ad5x_printer():
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
# Validate file name
|
|
433
|
+
if not params.file_name or params.file_name.strip() == '':
|
|
434
|
+
print('AD5X Single-Color Job error: fileName cannot be empty')
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
# Create payload with AD5X-specific parameters for single-color printing
|
|
438
|
+
payload = {
|
|
439
|
+
"serialNumber": self.client.serial_number,
|
|
440
|
+
"checkCode": self.client.check_code,
|
|
441
|
+
"fileName": params.file_name,
|
|
442
|
+
"levelingBeforePrint": params.leveling_before_print,
|
|
443
|
+
"firstLayerInspection": False,
|
|
444
|
+
"flowCalibration": False,
|
|
445
|
+
"timeLapseVideo": False,
|
|
446
|
+
"useMatlStation": False, # Set to false for single-color jobs
|
|
447
|
+
"gcodeToolCnt": 0, # Set to 0 for single-color jobs
|
|
448
|
+
"materialMappings": [] # Empty array for single-color jobs
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
async with aiohttp.ClientSession() as session:
|
|
453
|
+
async with session.post(
|
|
454
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
455
|
+
json=payload,
|
|
456
|
+
headers={"Content-Type": "application/json"}
|
|
457
|
+
) as response:
|
|
458
|
+
if response.status != 200:
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
# Fix for FlashForge printer's malformed Content-Type header
|
|
462
|
+
try:
|
|
463
|
+
result = await response.json()
|
|
464
|
+
except aiohttp.ContentTypeError:
|
|
465
|
+
text = await response.text()
|
|
466
|
+
result = json.loads(text)
|
|
467
|
+
|
|
468
|
+
return NetworkUtils.is_ok(result)
|
|
469
|
+
|
|
470
|
+
except Exception as error:
|
|
471
|
+
print(f"AD5X Single-Color Job error: {error}")
|
|
472
|
+
raise error
|
|
473
|
+
|
|
474
|
+
def _validate_ad5x_printer(self) -> bool:
|
|
475
|
+
"""
|
|
476
|
+
Validates that the current printer is an AD5X model.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
True if the printer is AD5X, false otherwise
|
|
480
|
+
"""
|
|
481
|
+
if not self.client.is_ad5x:
|
|
482
|
+
print('AD5X Job error: This method can only be used with AD5X printers')
|
|
483
|
+
return False
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
def _encode_material_mappings_to_base64(self, material_mappings: list[AD5XMaterialMapping]) -> str:
|
|
487
|
+
"""
|
|
488
|
+
Encodes material mappings array to base64 string for HTTP headers.
|
|
489
|
+
Converts AD5XMaterialMapping array to JSON and then to base64 encoding.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
material_mappings: Array of material mappings to encode
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Base64-encoded JSON string
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
json_array = [
|
|
499
|
+
{
|
|
500
|
+
"toolId": m.tool_id,
|
|
501
|
+
"slotId": m.slot_id,
|
|
502
|
+
"materialName": m.material_name,
|
|
503
|
+
"toolMaterialColor": m.tool_material_color,
|
|
504
|
+
"slotMaterialColor": m.slot_material_color
|
|
505
|
+
}
|
|
506
|
+
for m in material_mappings
|
|
507
|
+
]
|
|
508
|
+
json_string = json.dumps(json_array)
|
|
509
|
+
return base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
|
|
510
|
+
except Exception as error:
|
|
511
|
+
print('Failed to encode material mappings to base64:', error)
|
|
512
|
+
raise Exception('Failed to encode material mappings for upload')
|
|
513
|
+
|
|
514
|
+
def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
|
|
515
|
+
"""
|
|
516
|
+
Validates material mappings for AD5X multi-color jobs.
|
|
517
|
+
Checks toolId range (0-3), slotId range (1-4), and color format (#RRGGBB).
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
material_mappings: Array of material mappings to validate
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if all mappings are valid, false otherwise
|
|
524
|
+
"""
|
|
525
|
+
if not material_mappings or len(material_mappings) == 0:
|
|
526
|
+
print('Material mappings validation error: materialMappings array cannot be empty for multi-color jobs')
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
if len(material_mappings) > 4:
|
|
530
|
+
print('Material mappings validation error: Maximum 4 material mappings allowed')
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
hex_color_regex = re.compile(r'^#[0-9A-Fa-f]{6}$')
|
|
534
|
+
|
|
535
|
+
for i, mapping in enumerate(material_mappings):
|
|
536
|
+
# Validate toolId (0-3)
|
|
537
|
+
if mapping.tool_id < 0 or mapping.tool_id > 3:
|
|
538
|
+
print(f'Material mappings validation error: toolId must be between 0-3, got {mapping.tool_id} at index {i}')
|
|
539
|
+
return False
|
|
540
|
+
|
|
541
|
+
# Validate slotId (1-4)
|
|
542
|
+
if mapping.slot_id < 1 or mapping.slot_id > 4:
|
|
543
|
+
print(f'Material mappings validation error: slotId must be between 1-4, got {mapping.slot_id} at index {i}')
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
# Validate materialName is not empty
|
|
547
|
+
if not mapping.material_name or mapping.material_name.strip() == '':
|
|
548
|
+
print(f'Material mappings validation error: materialName cannot be empty at index {i}')
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
# Validate toolMaterialColor format
|
|
552
|
+
if not hex_color_regex.match(mapping.tool_material_color):
|
|
553
|
+
print(f'Material mappings validation error: toolMaterialColor must be in #RRGGBB format, got {mapping.tool_material_color} at index {i}')
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
# Validate slotMaterialColor format
|
|
557
|
+
if not hex_color_regex.match(mapping.slot_material_color):
|
|
558
|
+
print(f'Material mappings validation error: slotMaterialColor must be in #RRGGBB format, got {mapping.slot_material_color} at index {i}')
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
return True
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Temperature Control Module
|
|
3
|
+
"""
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ...client import FlashForgeClient
|
|
8
|
+
from ...tcp.ff_client import FlashForgeClient as TcpClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TempControl:
|
|
12
|
+
"""
|
|
13
|
+
Provides methods for controlling the temperatures of various printer components,
|
|
14
|
+
including extruders and the print bed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: "FlashForgeClient"):
|
|
18
|
+
"""
|
|
19
|
+
Creates an instance of the TempControl class.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
client: The FlashForgeClient instance used for communication with the printer.
|
|
23
|
+
"""
|
|
24
|
+
self.client = client
|
|
25
|
+
self._tcp_client: Optional[TcpClient] = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def tcp_client(self) -> "TcpClient":
|
|
29
|
+
"""Get the TCP client instance."""
|
|
30
|
+
if self._tcp_client is None:
|
|
31
|
+
self._tcp_client = self.client.tcp_client
|
|
32
|
+
return self._tcp_client
|
|
33
|
+
|
|
34
|
+
async def set_extruder_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Sets the target temperature for an extruder.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
temperature: The target temperature in Celsius.
|
|
40
|
+
wait_for: Whether to wait for the heating operation to complete
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the command is successful, False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
return await self.tcp_client.set_extruder_temp(temperature, wait_for)
|
|
46
|
+
|
|
47
|
+
async def set_bed_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Sets the target temperature for the print bed.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
temperature: The target bed temperature in Celsius.
|
|
53
|
+
wait_for: Whether to wait for the heating operation to complete
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if the command is successful, False otherwise.
|
|
57
|
+
"""
|
|
58
|
+
return await self.tcp_client.set_bed_temp(temperature, wait_for)
|
|
59
|
+
|
|
60
|
+
async def cancel_extruder_temp(self) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Cancels the heating of an extruder (sets target temperature to 0).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if the command is successful, False otherwise.
|
|
66
|
+
"""
|
|
67
|
+
return await self.tcp_client.cancel_extruder_temp()
|
|
68
|
+
|
|
69
|
+
async def cancel_bed_temp(self) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Cancels the heating of the print bed (sets target temperature to 0).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if the command is successful, False otherwise.
|
|
75
|
+
"""
|
|
76
|
+
return await self.tcp_client.cancel_bed_temp()
|
|
77
|
+
|
|
78
|
+
async def wait_for_part_cool(self, target_temp: float = 50.0, timeout_seconds: int = 1800) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Waits for printer components to cool down to a safe temperature.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
target_temp: The target temperature to wait for (default: 50°C).
|
|
84
|
+
timeout_seconds: Maximum time to wait in seconds (default: 30 minutes).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if components cooled to target temperature, False if timeout or error.
|
|
88
|
+
"""
|
|
89
|
+
return await self.tcp_client.wait_for_part_cool(target_temp, timeout_seconds)
|