flashforge-python-api 1.3.2__py3-none-any.whl → 1.3.3__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/api/controls/control.py +16 -9
- flashforge/api/controls/creator5_palette.py +6 -3
- flashforge/api/controls/files.py +10 -4
- flashforge/api/controls/info.py +20 -5
- flashforge/api/controls/job_control.py +106 -68
- flashforge/api/controls/temp_control.py +15 -6
- flashforge/api/misc/redaction.py +88 -0
- flashforge/client.py +9 -9
- flashforge/tcp/parsers/endstop_status.py +7 -5
- flashforge/tcp/parsers/print_status.py +5 -2
- flashforge/tcp/parsers/thumbnail_info.py +18 -11
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/METADATA +1 -1
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/RECORD +16 -15
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/WHEEL +0 -0
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
FlashForge Python API - Control Module
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
from ...models.responses import FilamentArgs
|
|
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from ...client import FlashForgeClient
|
|
15
16
|
from ...tcp.ff_client import FlashForgeClient as TcpClient
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class Control:
|
|
19
22
|
"""
|
|
@@ -75,7 +78,7 @@ class Control:
|
|
|
75
78
|
return await self._send_filtration_command(
|
|
76
79
|
FilamentArgs(internal="close", external="open")
|
|
77
80
|
)
|
|
78
|
-
|
|
81
|
+
logger.warning("set_external_filtration_on: filtration is not equipped on this printer.")
|
|
79
82
|
return False
|
|
80
83
|
|
|
81
84
|
async def set_internal_filtration_on(self) -> bool:
|
|
@@ -90,7 +93,7 @@ class Control:
|
|
|
90
93
|
return await self._send_filtration_command(
|
|
91
94
|
FilamentArgs(internal="open", external="close")
|
|
92
95
|
)
|
|
93
|
-
|
|
96
|
+
logger.warning("set_internal_filtration_on: filtration is not equipped on this printer.")
|
|
94
97
|
return False
|
|
95
98
|
|
|
96
99
|
async def set_filtration_off(self) -> bool:
|
|
@@ -105,7 +108,7 @@ class Control:
|
|
|
105
108
|
return await self._send_filtration_command(
|
|
106
109
|
FilamentArgs(internal="close", external="close")
|
|
107
110
|
)
|
|
108
|
-
|
|
111
|
+
logger.warning("set_filtration_off: filtration is not equipped on this printer.")
|
|
109
112
|
return False
|
|
110
113
|
|
|
111
114
|
async def turn_camera_on(self) -> bool:
|
|
@@ -188,7 +191,7 @@ class Control:
|
|
|
188
191
|
True if the command is successful, False otherwise.
|
|
189
192
|
"""
|
|
190
193
|
if not self.client.led_control:
|
|
191
|
-
|
|
194
|
+
logger.warning("set_led_on: LED control is not equipped on this printer.")
|
|
192
195
|
return False
|
|
193
196
|
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "open"})
|
|
194
197
|
|
|
@@ -200,7 +203,7 @@ class Control:
|
|
|
200
203
|
True if the command is successful, False otherwise.
|
|
201
204
|
"""
|
|
202
205
|
if not self.client.led_control:
|
|
203
|
-
|
|
206
|
+
logger.warning("set_led_off: LED control is not equipped on this printer.")
|
|
204
207
|
return False
|
|
205
208
|
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "close"})
|
|
206
209
|
|
|
@@ -259,7 +262,9 @@ class Control:
|
|
|
259
262
|
True if the command is successful, False otherwise.
|
|
260
263
|
"""
|
|
261
264
|
if not self.client.is_ad5x and not self.client.is_creator5:
|
|
262
|
-
|
|
265
|
+
logger.warning(
|
|
266
|
+
"configure_slot: the material station is only available on the AD5X / Creator 5."
|
|
267
|
+
)
|
|
263
268
|
return False
|
|
264
269
|
# The AD5X and Creator 5 use MUTUALLY EXCLUSIVE color wire formats (see
|
|
265
270
|
# creator5_palette for the firmware match rules), so model-gate here:
|
|
@@ -293,7 +298,9 @@ class Control:
|
|
|
293
298
|
"payload": {"cmd": command, "args": args},
|
|
294
299
|
}
|
|
295
300
|
|
|
296
|
-
|
|
301
|
+
# Log the command, never `payload`: it carries the serial number and
|
|
302
|
+
# check code, and these lines end up pasted into bug reports.
|
|
303
|
+
logger.debug("Sending control command %s with args %s", command, args)
|
|
297
304
|
|
|
298
305
|
try:
|
|
299
306
|
await self.client.is_http_client_busy()
|
|
@@ -305,12 +312,12 @@ class Control:
|
|
|
305
312
|
headers={"Content-Type": "application/json"},
|
|
306
313
|
) as response:
|
|
307
314
|
data = await json_from_response(response)
|
|
308
|
-
|
|
315
|
+
logger.debug("Control command %s reply: %s", command, data)
|
|
309
316
|
|
|
310
317
|
return NetworkUtils.is_ok(data)
|
|
311
318
|
|
|
312
319
|
except Exception as e:
|
|
313
|
-
|
|
320
|
+
logger.warning("Error in send_control_command (%s): %s", command, e)
|
|
314
321
|
return False
|
|
315
322
|
finally:
|
|
316
323
|
self.client.release_http_client()
|
|
@@ -16,6 +16,7 @@ for the model-gating that splits them.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import logging
|
|
19
20
|
import math
|
|
20
21
|
import re
|
|
21
22
|
from dataclasses import dataclass
|
|
@@ -25,6 +26,8 @@ from dataclasses import dataclass
|
|
|
25
26
|
# reference. PEP 8 lowercase locals are therefore relaxed here on purpose.
|
|
26
27
|
# ruff: noqa: N806
|
|
27
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
@dataclass(frozen=True)
|
|
30
33
|
class Creator5PaletteColor:
|
|
@@ -238,9 +241,9 @@ def snap_to_creator5_palette(hex_str: str) -> Creator5PaletteColor:
|
|
|
238
241
|
"""
|
|
239
242
|
rgb = _hex_to_rgb(hex_str)
|
|
240
243
|
if rgb is None:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
logger.warning(
|
|
245
|
+
'snap_to_creator5_palette: could not parse "%s" as hex; falling back to White.',
|
|
246
|
+
hex_str,
|
|
244
247
|
)
|
|
245
248
|
return CREATOR5_PALETTE[0]
|
|
246
249
|
|
flashforge/api/controls/files.py
CHANGED
|
@@ -90,7 +90,10 @@ class Files:
|
|
|
90
90
|
data = await json_from_response(response)
|
|
91
91
|
|
|
92
92
|
if not NetworkUtils.is_ok(data):
|
|
93
|
-
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Error retrieving the file list: %s",
|
|
95
|
+
NetworkUtils.get_error_message(data),
|
|
96
|
+
)
|
|
94
97
|
return []
|
|
95
98
|
|
|
96
99
|
# Parse the response using GCodeListResponse
|
|
@@ -147,7 +150,7 @@ class Files:
|
|
|
147
150
|
return []
|
|
148
151
|
|
|
149
152
|
except Exception as err:
|
|
150
|
-
|
|
153
|
+
logger.warning("get_recent_file_list error: %s", err)
|
|
151
154
|
return []
|
|
152
155
|
|
|
153
156
|
async def get_gcode_thumbnail(self, file_name: str) -> bytes | None:
|
|
@@ -185,9 +188,12 @@ class Files:
|
|
|
185
188
|
result = ThumbnailResponse(**data)
|
|
186
189
|
return base64.b64decode(result.image_data)
|
|
187
190
|
else:
|
|
188
|
-
|
|
191
|
+
logger.warning(
|
|
192
|
+
"Error retrieving the thumbnail: %s",
|
|
193
|
+
NetworkUtils.get_error_message(data),
|
|
194
|
+
)
|
|
189
195
|
return None
|
|
190
196
|
|
|
191
197
|
except Exception as err:
|
|
192
|
-
|
|
198
|
+
logger.warning("get_gcode_thumbnail error: %s", err)
|
|
193
199
|
return None
|
flashforge/api/controls/info.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
FlashForge Python API - Info Module
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
@@ -9,11 +10,14 @@ from typing import TYPE_CHECKING
|
|
|
9
10
|
from ...models.machine_info import FFMachineInfo, MachineState, Temperature
|
|
10
11
|
from ...models.responses import DetailResponse, FFPrinterDetail
|
|
11
12
|
from ..constants.endpoints import Endpoints
|
|
13
|
+
from ..misc.redaction import redact_model
|
|
12
14
|
from ..network.utils import json_from_response
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from ...client import FlashForgeClient
|
|
16
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
# Firmware-reported PIDs from FlashForge's /detail endpoint. These are stable
|
|
19
23
|
# identifiers set by firmware, unlike the user-mutable `name` field.
|
|
@@ -267,8 +271,14 @@ class MachineInfoParser:
|
|
|
267
271
|
return machine_info
|
|
268
272
|
|
|
269
273
|
except Exception as error:
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
logger.warning(
|
|
275
|
+
"Could not parse the /detail payload into FFMachineInfo; the caller sees "
|
|
276
|
+
"this as an unreachable printer. %s",
|
|
277
|
+
error,
|
|
278
|
+
)
|
|
279
|
+
# Redacted: this dump carries the MAC, IP and cloud registration
|
|
280
|
+
# codes, and it exists to be pasted into a bug report.
|
|
281
|
+
logger.debug("Detail object that failed to parse: %s", redact_model(detail))
|
|
272
282
|
return None
|
|
273
283
|
|
|
274
284
|
@staticmethod
|
|
@@ -303,7 +313,7 @@ class MachineInfoParser:
|
|
|
303
313
|
return state_mapping[valid_status]
|
|
304
314
|
|
|
305
315
|
if valid_status:
|
|
306
|
-
|
|
316
|
+
logger.warning("Unknown machine status received: %r", status)
|
|
307
317
|
return MachineState.UNKNOWN
|
|
308
318
|
|
|
309
319
|
|
|
@@ -384,12 +394,17 @@ class Info:
|
|
|
384
394
|
headers={"Content-Type": "application/json"},
|
|
385
395
|
) as response:
|
|
386
396
|
if response.status != 200:
|
|
387
|
-
|
|
397
|
+
logger.warning("Non-200 status from the /detail endpoint: %s", response.status)
|
|
388
398
|
return None
|
|
389
399
|
|
|
390
400
|
data = await json_from_response(response)
|
|
391
401
|
return DetailResponse(**data)
|
|
392
402
|
|
|
393
403
|
except Exception as error:
|
|
394
|
-
|
|
404
|
+
logger.warning(
|
|
405
|
+
"Could not read /detail; callers cannot tell this from an unreachable "
|
|
406
|
+
"printer or a rejected check code, so this line is the only record of "
|
|
407
|
+
"the real cause. %s",
|
|
408
|
+
error,
|
|
409
|
+
)
|
|
395
410
|
return None
|
|
@@ -4,6 +4,7 @@ FlashForge Python API - Job Control Module
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
@@ -19,12 +20,15 @@ from ...models.responses import (
|
|
|
19
20
|
Creator5UploadParams,
|
|
20
21
|
)
|
|
21
22
|
from ..constants.endpoints import Endpoints
|
|
23
|
+
from ..misc.redaction import redact_mapping
|
|
22
24
|
from ..network.utils import NetworkUtils, json_from_response
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from ...client import FlashForgeClient
|
|
26
28
|
from .control import Control
|
|
27
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
class JobControl:
|
|
30
34
|
"""
|
|
@@ -136,14 +140,18 @@ class JobControl:
|
|
|
136
140
|
file_path_obj = Path(file_path)
|
|
137
141
|
|
|
138
142
|
if not file_path_obj.exists():
|
|
139
|
-
|
|
143
|
+
logger.warning("upload_file: file not found at %s", file_path)
|
|
140
144
|
return False
|
|
141
145
|
|
|
142
146
|
file_size = file_path_obj.stat().st_size
|
|
143
147
|
file_name = file_path_obj.name
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
logger.debug(
|
|
150
|
+
"Starting upload for %s (size=%s, start=%s, level=%s)",
|
|
151
|
+
file_name,
|
|
152
|
+
file_size,
|
|
153
|
+
start_print,
|
|
154
|
+
level_before_print,
|
|
147
155
|
)
|
|
148
156
|
|
|
149
157
|
try:
|
|
@@ -159,16 +167,16 @@ class JobControl:
|
|
|
159
167
|
|
|
160
168
|
# Add additional headers for new firmware
|
|
161
169
|
if self._is_new_firmware_version():
|
|
162
|
-
|
|
170
|
+
logger.debug("Using new firmware headers for upload.")
|
|
163
171
|
custom_headers["flowCalibration"] = "false"
|
|
164
172
|
custom_headers["useMatlStation"] = "false"
|
|
165
173
|
custom_headers["gcodeToolCnt"] = "0"
|
|
166
174
|
# Base64 encode "[]" which is "W10="
|
|
167
175
|
custom_headers["materialMappings"] = "W10="
|
|
168
176
|
else:
|
|
169
|
-
|
|
177
|
+
logger.debug("Using old firmware headers for upload.")
|
|
170
178
|
|
|
171
|
-
|
|
179
|
+
logger.debug("Upload request headers: %s", redact_mapping(custom_headers))
|
|
172
180
|
|
|
173
181
|
# Create multipart form data
|
|
174
182
|
async with aiohttp.ClientSession() as session:
|
|
@@ -183,26 +191,31 @@ class JobControl:
|
|
|
183
191
|
data=data,
|
|
184
192
|
headers=custom_headers,
|
|
185
193
|
) as response:
|
|
186
|
-
|
|
194
|
+
logger.debug("Upload response status: %s", response.status)
|
|
187
195
|
|
|
188
196
|
if response.status != 200:
|
|
189
|
-
|
|
197
|
+
logger.warning(
|
|
198
|
+
"Upload failed: the printer responded with status %s",
|
|
199
|
+
response.status,
|
|
200
|
+
)
|
|
190
201
|
return False
|
|
191
202
|
|
|
192
203
|
result = await json_from_response(response)
|
|
193
|
-
|
|
204
|
+
logger.debug("Upload response data: %s", result)
|
|
194
205
|
|
|
195
206
|
if NetworkUtils.is_ok(result):
|
|
196
|
-
|
|
207
|
+
logger.debug("Upload successful according to the printer response.")
|
|
197
208
|
return True
|
|
198
209
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Upload failed: printer response code=%s, message=%s",
|
|
212
|
+
result.get("code"),
|
|
213
|
+
result.get("message"),
|
|
201
214
|
)
|
|
202
215
|
return False
|
|
203
216
|
|
|
204
217
|
except Exception as e:
|
|
205
|
-
|
|
218
|
+
logger.warning("upload_file error: %s", e)
|
|
206
219
|
return False
|
|
207
220
|
|
|
208
221
|
async def print_local_file(self, file_name: str, leveling_before_print: bool) -> bool:
|
|
@@ -252,7 +265,7 @@ class JobControl:
|
|
|
252
265
|
return NetworkUtils.is_ok(result)
|
|
253
266
|
|
|
254
267
|
except Exception as error:
|
|
255
|
-
|
|
268
|
+
logger.warning("print_local_file error: %s", error)
|
|
256
269
|
raise error
|
|
257
270
|
|
|
258
271
|
async def upload_file_ad5x(self, params: AD5XUploadParams) -> bool:
|
|
@@ -278,14 +291,19 @@ class JobControl:
|
|
|
278
291
|
# Validate file exists
|
|
279
292
|
file_path_obj = Path(params.file_path)
|
|
280
293
|
if not file_path_obj.exists():
|
|
281
|
-
|
|
294
|
+
logger.warning("upload_file_ad5x: file not found at %s", params.file_path)
|
|
282
295
|
return False
|
|
283
296
|
|
|
284
297
|
file_size = file_path_obj.stat().st_size
|
|
285
298
|
file_name = file_path_obj.name
|
|
286
299
|
|
|
287
|
-
|
|
288
|
-
|
|
300
|
+
logger.debug(
|
|
301
|
+
"Starting AD5X upload for %s (size=%s, start=%s, level=%s, tools=%s)",
|
|
302
|
+
file_name,
|
|
303
|
+
file_size,
|
|
304
|
+
params.start_print,
|
|
305
|
+
params.leveling_before_print,
|
|
306
|
+
len(params.material_mappings),
|
|
289
307
|
)
|
|
290
308
|
|
|
291
309
|
try:
|
|
@@ -310,7 +328,7 @@ class JobControl:
|
|
|
310
328
|
"Expect": "100-continue",
|
|
311
329
|
}
|
|
312
330
|
|
|
313
|
-
|
|
331
|
+
logger.debug("AD5X upload request headers: %s", redact_mapping(custom_headers))
|
|
314
332
|
|
|
315
333
|
# Create multipart form data
|
|
316
334
|
async with aiohttp.ClientSession() as session:
|
|
@@ -325,28 +343,33 @@ class JobControl:
|
|
|
325
343
|
data=data,
|
|
326
344
|
headers=custom_headers,
|
|
327
345
|
) as response:
|
|
328
|
-
|
|
346
|
+
logger.debug("AD5X upload response status: %s", response.status)
|
|
329
347
|
|
|
330
348
|
if response.status != 200:
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
logger.warning(
|
|
350
|
+
"AD5X upload failed: the printer responded with status %s",
|
|
351
|
+
response.status,
|
|
333
352
|
)
|
|
334
353
|
return False
|
|
335
354
|
|
|
336
355
|
result = await json_from_response(response)
|
|
337
|
-
|
|
356
|
+
logger.debug("AD5X upload response data: %s", result)
|
|
338
357
|
|
|
339
358
|
if NetworkUtils.is_ok(result):
|
|
340
|
-
|
|
359
|
+
logger.debug(
|
|
360
|
+
"AD5X upload successful according to the printer response."
|
|
361
|
+
)
|
|
341
362
|
return True
|
|
342
363
|
else:
|
|
343
|
-
|
|
344
|
-
|
|
364
|
+
logger.warning(
|
|
365
|
+
"AD5X upload failed: printer response code=%s, message=%s",
|
|
366
|
+
result.get("code"),
|
|
367
|
+
result.get("message"),
|
|
345
368
|
)
|
|
346
369
|
return False
|
|
347
370
|
|
|
348
371
|
except Exception as e:
|
|
349
|
-
|
|
372
|
+
logger.warning("upload_file_ad5x error: %s", e)
|
|
350
373
|
return False
|
|
351
374
|
|
|
352
375
|
async def start_ad5x_multi_color_job(self, params: AD5XLocalJobParams) -> bool:
|
|
@@ -371,7 +394,7 @@ class JobControl:
|
|
|
371
394
|
|
|
372
395
|
# Validate file name
|
|
373
396
|
if not params.file_name or params.file_name.strip() == "":
|
|
374
|
-
|
|
397
|
+
logger.warning("AD5X multi-color job: fileName cannot be empty.")
|
|
375
398
|
return False
|
|
376
399
|
|
|
377
400
|
# Create payload with AD5X-specific parameters
|
|
@@ -411,7 +434,7 @@ class JobControl:
|
|
|
411
434
|
return NetworkUtils.is_ok(result)
|
|
412
435
|
|
|
413
436
|
except Exception as error:
|
|
414
|
-
|
|
437
|
+
logger.warning("AD5X multi-color job error: %s", error)
|
|
415
438
|
raise error
|
|
416
439
|
|
|
417
440
|
async def start_ad5x_single_color_job(self, params: AD5XSingleColorJobParams) -> bool:
|
|
@@ -432,7 +455,7 @@ class JobControl:
|
|
|
432
455
|
|
|
433
456
|
# Validate file name
|
|
434
457
|
if not params.file_name or params.file_name.strip() == "":
|
|
435
|
-
|
|
458
|
+
logger.warning("AD5X single-color job: fileName cannot be empty.")
|
|
436
459
|
return False
|
|
437
460
|
|
|
438
461
|
# Create payload with AD5X-specific parameters for single-color printing
|
|
@@ -463,7 +486,7 @@ class JobControl:
|
|
|
463
486
|
return NetworkUtils.is_ok(result)
|
|
464
487
|
|
|
465
488
|
except Exception as error:
|
|
466
|
-
|
|
489
|
+
logger.warning("AD5X single-color job error: %s", error)
|
|
467
490
|
raise error
|
|
468
491
|
|
|
469
492
|
# --- Creator 5 / Creator 5 Pro ---
|
|
@@ -495,16 +518,21 @@ class JobControl:
|
|
|
495
518
|
file_path_obj = Path(params.file_path)
|
|
496
519
|
|
|
497
520
|
if not file_path_obj.exists():
|
|
498
|
-
|
|
521
|
+
logger.warning("upload_file_creator5: file not found at %s", params.file_path)
|
|
499
522
|
return False
|
|
500
523
|
|
|
501
524
|
file_size = file_path_obj.stat().st_size
|
|
502
525
|
file_name = file_path_obj.name
|
|
503
526
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
527
|
+
logger.debug(
|
|
528
|
+
"Starting Creator 5 upload for %s (size=%s, start=%s, level=%s, "
|
|
529
|
+
"matl_station=%s, tools=%s)",
|
|
530
|
+
file_name,
|
|
531
|
+
file_size,
|
|
532
|
+
params.start_print,
|
|
533
|
+
params.leveling_before_print,
|
|
534
|
+
params.use_matl_station,
|
|
535
|
+
params.gcode_tool_cnt,
|
|
508
536
|
)
|
|
509
537
|
|
|
510
538
|
try:
|
|
@@ -524,7 +552,7 @@ class JobControl:
|
|
|
524
552
|
"Expect": "100-continue",
|
|
525
553
|
}
|
|
526
554
|
|
|
527
|
-
|
|
555
|
+
logger.debug("Creator 5 upload request headers: %s", redact_mapping(custom_headers))
|
|
528
556
|
|
|
529
557
|
# Create multipart form data
|
|
530
558
|
async with aiohttp.ClientSession() as session:
|
|
@@ -539,30 +567,33 @@ class JobControl:
|
|
|
539
567
|
data=data,
|
|
540
568
|
headers=custom_headers,
|
|
541
569
|
) as response:
|
|
542
|
-
|
|
570
|
+
logger.debug("Creator 5 upload response status: %s", response.status)
|
|
543
571
|
|
|
544
572
|
if response.status != 200:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
573
|
+
logger.warning(
|
|
574
|
+
"Creator 5 upload failed: the printer responded with status %s",
|
|
575
|
+
response.status,
|
|
548
576
|
)
|
|
549
577
|
return False
|
|
550
578
|
|
|
551
579
|
result = await json_from_response(response)
|
|
552
|
-
|
|
580
|
+
logger.debug("Creator 5 upload response data: %s", result)
|
|
553
581
|
|
|
554
582
|
if NetworkUtils.is_ok(result):
|
|
555
|
-
|
|
583
|
+
logger.debug(
|
|
584
|
+
"Creator 5 upload successful according to the printer response."
|
|
585
|
+
)
|
|
556
586
|
return True
|
|
557
587
|
|
|
558
|
-
|
|
559
|
-
"Creator 5
|
|
560
|
-
|
|
588
|
+
logger.warning(
|
|
589
|
+
"Creator 5 upload failed: printer response code=%s, message=%s",
|
|
590
|
+
result.get("code"),
|
|
591
|
+
result.get("message"),
|
|
561
592
|
)
|
|
562
593
|
return False
|
|
563
594
|
|
|
564
595
|
except Exception as e:
|
|
565
|
-
|
|
596
|
+
logger.warning("upload_file_creator5 error: %s", e)
|
|
566
597
|
return False
|
|
567
598
|
|
|
568
599
|
async def start_creator5_job(self, params: Creator5JobParams) -> bool:
|
|
@@ -591,7 +622,7 @@ class JobControl:
|
|
|
591
622
|
return False
|
|
592
623
|
|
|
593
624
|
if not params.file_name or params.file_name.strip() == "":
|
|
594
|
-
|
|
625
|
+
logger.warning("Creator 5 job: fileName cannot be empty.")
|
|
595
626
|
return False
|
|
596
627
|
|
|
597
628
|
has_mappings = params.material_mappings is not None and len(params.material_mappings) > 0
|
|
@@ -634,7 +665,7 @@ class JobControl:
|
|
|
634
665
|
return NetworkUtils.is_ok(result)
|
|
635
666
|
|
|
636
667
|
except Exception as error:
|
|
637
|
-
|
|
668
|
+
logger.warning("Creator 5 job error: %s", error)
|
|
638
669
|
raise error
|
|
639
670
|
|
|
640
671
|
def _validate_material_station_printer(self) -> bool:
|
|
@@ -648,9 +679,8 @@ class JobControl:
|
|
|
648
679
|
True if the printer supports material mappings, False otherwise.
|
|
649
680
|
"""
|
|
650
681
|
if not self.client.is_ad5x and not self.client.is_creator5:
|
|
651
|
-
|
|
652
|
-
"Material-station job
|
|
653
|
-
"Creator 5 series printer"
|
|
682
|
+
logger.warning(
|
|
683
|
+
"Material-station job: this method requires an AD5X or Creator 5 series printer."
|
|
654
684
|
)
|
|
655
685
|
return False
|
|
656
686
|
return True
|
|
@@ -698,45 +728,53 @@ class JobControl:
|
|
|
698
728
|
if not material_mappings or len(material_mappings) == 0:
|
|
699
729
|
if allow_empty:
|
|
700
730
|
return True
|
|
701
|
-
|
|
731
|
+
logger.warning("%s: materialMappings cannot be empty for multi-color jobs.", label)
|
|
702
732
|
return False
|
|
703
733
|
|
|
704
734
|
if len(material_mappings) > 4:
|
|
705
|
-
|
|
735
|
+
logger.warning("%s: a maximum of 4 material mappings is allowed.", label)
|
|
706
736
|
return False
|
|
707
737
|
|
|
708
738
|
hex_color_regex = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
709
739
|
|
|
710
740
|
for i, mapping in enumerate(material_mappings):
|
|
711
741
|
if mapping.tool_id < 0 or mapping.tool_id > 3:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
742
|
+
logger.warning(
|
|
743
|
+
"%s: toolId must be between 0-3, got %s at index %s",
|
|
744
|
+
label,
|
|
745
|
+
mapping.tool_id,
|
|
746
|
+
i,
|
|
715
747
|
)
|
|
716
748
|
return False
|
|
717
749
|
|
|
718
750
|
if mapping.slot_id < 1 or mapping.slot_id > 4:
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
751
|
+
logger.warning(
|
|
752
|
+
"%s: slotId must be between 1-4, got %s at index %s",
|
|
753
|
+
label,
|
|
754
|
+
mapping.slot_id,
|
|
755
|
+
i,
|
|
722
756
|
)
|
|
723
757
|
return False
|
|
724
758
|
|
|
725
759
|
if not mapping.material_name or mapping.material_name.strip() == "":
|
|
726
|
-
|
|
760
|
+
logger.warning("%s: materialName cannot be empty at index %s", label, i)
|
|
727
761
|
return False
|
|
728
762
|
|
|
729
763
|
if not hex_color_regex.match(mapping.tool_material_color):
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
764
|
+
logger.warning(
|
|
765
|
+
"%s: toolMaterialColor must be in #RRGGBB format, got %s at index %s",
|
|
766
|
+
label,
|
|
767
|
+
mapping.tool_material_color,
|
|
768
|
+
i,
|
|
733
769
|
)
|
|
734
770
|
return False
|
|
735
771
|
|
|
736
772
|
if not hex_color_regex.match(mapping.slot_material_color):
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
773
|
+
logger.warning(
|
|
774
|
+
"%s: slotMaterialColor must be in #RRGGBB format, got %s at index %s",
|
|
775
|
+
label,
|
|
776
|
+
mapping.slot_material_color,
|
|
777
|
+
i,
|
|
740
778
|
)
|
|
741
779
|
return False
|
|
742
780
|
|
|
@@ -750,7 +788,7 @@ class JobControl:
|
|
|
750
788
|
True if the printer is AD5X, false otherwise
|
|
751
789
|
"""
|
|
752
790
|
if not self.client.is_ad5x:
|
|
753
|
-
|
|
791
|
+
logger.warning("AD5X job: this method can only be used with AD5X printers.")
|
|
754
792
|
return False
|
|
755
793
|
return True
|
|
756
794
|
|
|
@@ -781,7 +819,7 @@ class JobControl:
|
|
|
781
819
|
json_string = json.dumps(json_array)
|
|
782
820
|
return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
|
|
783
821
|
except Exception as error:
|
|
784
|
-
|
|
822
|
+
logger.warning("Failed to encode the material mappings to base64: %s", error)
|
|
785
823
|
raise Exception(
|
|
786
824
|
"Failed to encode material mappings for upload"
|
|
787
825
|
) from error # noqa: B904
|
|
@@ -6,6 +6,7 @@ direct TCP G-code/M-code commands; HTTP-only printers (Creator 5 / 5 Pro, no TCP
|
|
|
6
6
|
channel) use the HTTP ``temperatureCtl_cmd`` instead.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
from ..constants.commands import Commands
|
|
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from ...client import FlashForgeClient
|
|
15
16
|
from ...tcp.ff_client import FlashForgeClient as TcpClient
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
# Sentinel value for `temperatureCtl_cmd` meaning "leave this heater unchanged"
|
|
19
22
|
# (partial update). Sending a real 0 / -100 would turn the heater off.
|
|
@@ -106,7 +109,9 @@ class TempControl:
|
|
|
106
109
|
The array, or None if ``tool_index`` is out of range.
|
|
107
110
|
"""
|
|
108
111
|
if not isinstance(tool_index, int) or tool_index < 0 or tool_index >= NOZZLE_COUNT:
|
|
109
|
-
|
|
112
|
+
logger.warning(
|
|
113
|
+
"TempControl: tool_index %s out of range (0-%s).", tool_index, NOZZLE_COUNT - 1
|
|
114
|
+
)
|
|
110
115
|
return None
|
|
111
116
|
nozzles = [TEMP_NO_CHANGE] * NOZZLE_COUNT
|
|
112
117
|
nozzles[tool_index] = value
|
|
@@ -145,7 +150,7 @@ class TempControl:
|
|
|
145
150
|
True if the command is acknowledged, False otherwise.
|
|
146
151
|
"""
|
|
147
152
|
if len(temps) != NOZZLE_COUNT:
|
|
148
|
-
|
|
153
|
+
logger.warning("set_tool_temps: expected %s temps, got %s.", NOZZLE_COUNT, len(temps))
|
|
149
154
|
return False
|
|
150
155
|
return await self._send_http_temp_command(nozzles=list(temps))
|
|
151
156
|
|
|
@@ -258,7 +263,9 @@ class TempControl:
|
|
|
258
263
|
True if the command is acknowledged, False otherwise.
|
|
259
264
|
"""
|
|
260
265
|
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
261
|
-
|
|
266
|
+
logger.warning(
|
|
267
|
+
"set_chamber_temp: the chamber heater is only available on the Creator 5 series."
|
|
268
|
+
)
|
|
262
269
|
return False
|
|
263
270
|
return await self._send_http_temp_command(chamber=temp)
|
|
264
271
|
|
|
@@ -270,7 +277,9 @@ class TempControl:
|
|
|
270
277
|
True if the command is acknowledged, False otherwise.
|
|
271
278
|
"""
|
|
272
279
|
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
273
|
-
|
|
280
|
+
logger.warning(
|
|
281
|
+
"cancel_chamber_temp: the chamber heater is only available on the Creator 5 series."
|
|
282
|
+
)
|
|
274
283
|
return False
|
|
275
284
|
return await self._send_http_temp_command(chamber=TEMP_OFF)
|
|
276
285
|
|
|
@@ -292,8 +301,8 @@ class TempControl:
|
|
|
292
301
|
or HTTP-only (no TCP polling channel).
|
|
293
302
|
"""
|
|
294
303
|
if self.client.http_only:
|
|
295
|
-
|
|
296
|
-
"wait_for_part_cool
|
|
304
|
+
logger.warning(
|
|
305
|
+
"wait_for_part_cool is unavailable over an HTTP-only connection; "
|
|
297
306
|
"poll info.get() instead."
|
|
298
307
|
)
|
|
299
308
|
return False
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Log redaction helpers.
|
|
3
|
+
|
|
4
|
+
Debug logs get pasted into bug reports verbatim, so anything that identifies or
|
|
5
|
+
grants control of a printer is masked before it can be formatted into a log
|
|
6
|
+
record. The key set mirrors what ``ff-5mp-hass`` already strips from its
|
|
7
|
+
diagnostics download, so the two surfaces cannot disagree about what is safe to
|
|
8
|
+
share.
|
|
9
|
+
|
|
10
|
+
- ``serialNumber`` / ``checkCode``: together these are full control of the
|
|
11
|
+
printer over the LAN API.
|
|
12
|
+
- ``flashRegisterCode`` / ``polarRegisterCode``: cloud-account registration
|
|
13
|
+
codes.
|
|
14
|
+
- ``macAddr`` / ``ipAddr``: identify the machine and its network.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
REDACTED = "<redacted>"
|
|
20
|
+
|
|
21
|
+
# Both the firmware's camelCase aliases and the library's snake_case field
|
|
22
|
+
# names, since payloads are logged in either form depending on the call site.
|
|
23
|
+
SENSITIVE_KEYS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"serialNumber",
|
|
26
|
+
"serial_number",
|
|
27
|
+
"checkCode",
|
|
28
|
+
"check_code",
|
|
29
|
+
"macAddr",
|
|
30
|
+
"mac_addr",
|
|
31
|
+
"mac_address",
|
|
32
|
+
"ipAddr",
|
|
33
|
+
"ip_addr",
|
|
34
|
+
"ip_address",
|
|
35
|
+
"flashRegisterCode",
|
|
36
|
+
"flash_register_code",
|
|
37
|
+
"flash_cloud_register_code",
|
|
38
|
+
"polarRegisterCode",
|
|
39
|
+
"polar_register_code",
|
|
40
|
+
"polar_cloud_register_code",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _redact_value(value: Any) -> Any:
|
|
46
|
+
"""Recurse into nested containers so a sensitive key cannot hide one level down."""
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
return redact_mapping(value)
|
|
49
|
+
if isinstance(value, list):
|
|
50
|
+
return [_redact_value(item) for item in value]
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def redact_mapping(mapping: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
"""Return a copy of ``mapping`` with every sensitive value masked.
|
|
56
|
+
|
|
57
|
+
The input is never mutated - callers are usually about to send it.
|
|
58
|
+
"""
|
|
59
|
+
return {
|
|
60
|
+
key: (REDACTED if key in SENSITIVE_KEYS else _redact_value(value))
|
|
61
|
+
for key, value in mapping.items()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def redact_model(model: Any) -> Any:
|
|
66
|
+
"""Return a log-safe representation of a Pydantic model (or anything else).
|
|
67
|
+
|
|
68
|
+
Falls back to ``repr`` for objects that cannot be dumped, and to a plain
|
|
69
|
+
``<unloggable>`` if even that raises - a redaction helper must never be the
|
|
70
|
+
reason an error path fails.
|
|
71
|
+
"""
|
|
72
|
+
if model is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
dump = getattr(model, "model_dump", None)
|
|
76
|
+
if callable(dump):
|
|
77
|
+
try:
|
|
78
|
+
return redact_mapping(dump())
|
|
79
|
+
except Exception: # noqa: BLE001 - logging must not raise
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
if isinstance(model, dict):
|
|
83
|
+
return redact_mapping(model)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return repr(model)
|
|
87
|
+
except Exception: # noqa: BLE001
|
|
88
|
+
return "<unloggable>"
|
flashforge/client.py
CHANGED
|
@@ -164,7 +164,7 @@ class FlashForgeClient:
|
|
|
164
164
|
True if TCP operations are permitted, False otherwise
|
|
165
165
|
"""
|
|
166
166
|
if self._http_only:
|
|
167
|
-
|
|
167
|
+
logger.debug("%s() unavailable: printer has no TCP control channel (HTTP-only).", op)
|
|
168
168
|
return False
|
|
169
169
|
return True
|
|
170
170
|
|
|
@@ -212,7 +212,7 @@ class FlashForgeClient:
|
|
|
212
212
|
connected = await self.verify_connection()
|
|
213
213
|
if connected:
|
|
214
214
|
return True
|
|
215
|
-
|
|
215
|
+
logger.warning("Failed to connect to the printer.")
|
|
216
216
|
return False
|
|
217
217
|
|
|
218
218
|
@property
|
|
@@ -260,7 +260,7 @@ class FlashForgeClient:
|
|
|
260
260
|
"""
|
|
261
261
|
if await self.send_product_command():
|
|
262
262
|
return await self.tcp_client.init_control()
|
|
263
|
-
|
|
263
|
+
logger.warning("New API control failed; the product command was rejected.")
|
|
264
264
|
return False
|
|
265
265
|
|
|
266
266
|
async def dispose(self) -> None:
|
|
@@ -396,13 +396,13 @@ class FlashForgeClient:
|
|
|
396
396
|
# Get HTTP API response
|
|
397
397
|
response = await self.info.get_detail_response()
|
|
398
398
|
if not response or not NetworkUtils.is_ok(response):
|
|
399
|
-
|
|
399
|
+
logger.warning("Failed to get a valid response from the printer API.")
|
|
400
400
|
return False
|
|
401
401
|
|
|
402
402
|
# Parse machine info from detail response
|
|
403
403
|
machine_info = MachineInfoParser.from_detail(response.detail)
|
|
404
404
|
if not machine_info:
|
|
405
|
-
|
|
405
|
+
logger.warning("Failed to parse machine info from the /detail response.")
|
|
406
406
|
return False
|
|
407
407
|
|
|
408
408
|
# Detect http_only from the parsed model BEFORE touching TCP. The
|
|
@@ -426,16 +426,16 @@ class FlashForgeClient:
|
|
|
426
426
|
):
|
|
427
427
|
self.is_pro = True
|
|
428
428
|
else:
|
|
429
|
-
|
|
430
|
-
"
|
|
431
|
-
"
|
|
429
|
+
logger.warning(
|
|
430
|
+
"Unable to get PrinterInfo from the TCP API; some features might "
|
|
431
|
+
"not work."
|
|
432
432
|
)
|
|
433
433
|
|
|
434
434
|
# Cache the details
|
|
435
435
|
return self.cache_details(machine_info)
|
|
436
436
|
|
|
437
437
|
except Exception as error:
|
|
438
|
-
|
|
438
|
+
logger.warning("Error in verify_connection: %s", error)
|
|
439
439
|
return False
|
|
440
440
|
|
|
441
441
|
async def send_product_command(self) -> bool:
|
|
@@ -4,10 +4,13 @@ FlashForge Python API - Endstop Status Parser
|
|
|
4
4
|
Parses endstop and machine status information from M119 command responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
# Pre-compiled regex to match key:value pairs where value is an integer
|
|
12
15
|
KV_PATTERN = re.compile(r"([A-Za-z0-9-]+):\s*(\d+)")
|
|
13
16
|
|
|
@@ -144,7 +147,7 @@ class EndstopStatus:
|
|
|
144
147
|
elif "BUSY" in machine_status:
|
|
145
148
|
self.machine_status = MachineStatus.BUSY
|
|
146
149
|
else:
|
|
147
|
-
|
|
150
|
+
logger.warning("EndstopStatus: unknown MachineStatus: %s", machine_status)
|
|
148
151
|
self.machine_status = MachineStatus.DEFAULT
|
|
149
152
|
elif line.startswith("MoveMode:"):
|
|
150
153
|
move_mode = line.replace("MoveMode:", "", 1).strip().upper()
|
|
@@ -159,7 +162,7 @@ class EndstopStatus:
|
|
|
159
162
|
elif "HOMING" in move_mode:
|
|
160
163
|
self.move_mode = MoveMode.HOMING
|
|
161
164
|
else:
|
|
162
|
-
|
|
165
|
+
logger.warning("EndstopStatus: unknown MoveMode: %s", move_mode)
|
|
163
166
|
self.move_mode = MoveMode.DEFAULT
|
|
164
167
|
elif line.startswith("Status "):
|
|
165
168
|
self.status = Status(line)
|
|
@@ -184,9 +187,8 @@ class EndstopStatus:
|
|
|
184
187
|
return self
|
|
185
188
|
|
|
186
189
|
except Exception as e:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
print(f"Error: {e}")
|
|
190
|
+
logger.warning("Unable to create an EndstopStatus from the reply: %s", e)
|
|
191
|
+
logger.debug("Reply that failed to parse: %s", replay)
|
|
190
192
|
return None
|
|
191
193
|
|
|
192
194
|
def is_print_complete(self) -> bool:
|
|
@@ -4,8 +4,11 @@ FlashForge Python API - Print Status Parser
|
|
|
4
4
|
Parses print progress information from M27 command responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class PrintStatus:
|
|
11
14
|
"""
|
|
@@ -69,13 +72,13 @@ class PrintStatus:
|
|
|
69
72
|
return None
|
|
70
73
|
|
|
71
74
|
if not self.sd_current or not self.sd_total:
|
|
72
|
-
|
|
75
|
+
logger.warning("PrintStatus: invalid SD progress format.")
|
|
73
76
|
return None
|
|
74
77
|
|
|
75
78
|
return self
|
|
76
79
|
|
|
77
80
|
except Exception as e:
|
|
78
|
-
|
|
81
|
+
logger.warning("Error parsing the print status: %s", e)
|
|
79
82
|
return None
|
|
80
83
|
|
|
81
84
|
def get_print_percent(self) -> float:
|
|
@@ -6,9 +6,12 @@ Handles the parsing, storage, and manipulation of 3D print file thumbnail images
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import base64
|
|
9
|
+
import logging
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Optional
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class ThumbnailInfo:
|
|
14
17
|
"""
|
|
@@ -65,7 +68,7 @@ class ThumbnailInfo:
|
|
|
65
68
|
# Find where the PNG data starts (after the "ok" text delimiter)
|
|
66
69
|
ok_index = replay.find("ok")
|
|
67
70
|
if ok_index == -1:
|
|
68
|
-
|
|
71
|
+
logger.warning("ThumbnailInfo: no 'ok' delimiter found in the response.")
|
|
69
72
|
return None
|
|
70
73
|
|
|
71
74
|
# Skip the 'ok' text and any immediately following control characters
|
|
@@ -87,11 +90,11 @@ class ThumbnailInfo:
|
|
|
87
90
|
self._image_data = binary_buffer[png_start:]
|
|
88
91
|
return self
|
|
89
92
|
else:
|
|
90
|
-
|
|
93
|
+
logger.warning("ThumbnailInfo: no PNG signature found in the binary data.")
|
|
91
94
|
return None
|
|
92
95
|
|
|
93
96
|
except Exception as e:
|
|
94
|
-
|
|
97
|
+
logger.warning("ThumbnailInfo: error parsing the response: %s", e)
|
|
95
98
|
return None
|
|
96
99
|
|
|
97
100
|
def get_image_data(self) -> str | None:
|
|
@@ -157,7 +160,7 @@ class ThumbnailInfo:
|
|
|
157
160
|
True if the file was saved successfully, False otherwise
|
|
158
161
|
"""
|
|
159
162
|
if not self._image_data:
|
|
160
|
-
|
|
163
|
+
logger.warning("ThumbnailInfo: no image data to save.")
|
|
161
164
|
return False
|
|
162
165
|
|
|
163
166
|
try:
|
|
@@ -169,18 +172,20 @@ class ThumbnailInfo:
|
|
|
169
172
|
file_path = f"{base_name}.png"
|
|
170
173
|
|
|
171
174
|
if not file_path:
|
|
172
|
-
|
|
175
|
+
logger.warning(
|
|
176
|
+
"ThumbnailInfo: no file path provided and no filename to generate one from."
|
|
177
|
+
)
|
|
173
178
|
return False
|
|
174
179
|
|
|
175
180
|
# Write the bytes to file in a separate thread to avoid blocking the event loop
|
|
176
181
|
loop = asyncio.get_running_loop()
|
|
177
182
|
await loop.run_in_executor(None, self._write_file_sync, file_path, self._image_data)
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
logger.debug("ThumbnailInfo: saved the thumbnail to %s", file_path)
|
|
180
185
|
return True
|
|
181
186
|
|
|
182
187
|
except Exception as e:
|
|
183
|
-
|
|
188
|
+
logger.warning("ThumbnailInfo: error saving the thumbnail to a file: %s", e)
|
|
184
189
|
return False
|
|
185
190
|
|
|
186
191
|
def save_to_file_sync(self, file_path: str | None = None) -> bool:
|
|
@@ -194,7 +199,7 @@ class ThumbnailInfo:
|
|
|
194
199
|
True if the file was saved successfully, False otherwise
|
|
195
200
|
"""
|
|
196
201
|
if not self._image_data:
|
|
197
|
-
|
|
202
|
+
logger.warning("ThumbnailInfo: no image data to save.")
|
|
198
203
|
return False
|
|
199
204
|
|
|
200
205
|
try:
|
|
@@ -206,18 +211,20 @@ class ThumbnailInfo:
|
|
|
206
211
|
file_path = f"{base_name}.png"
|
|
207
212
|
|
|
208
213
|
if not file_path:
|
|
209
|
-
|
|
214
|
+
logger.warning(
|
|
215
|
+
"ThumbnailInfo: no file path provided and no filename to generate one from."
|
|
216
|
+
)
|
|
210
217
|
return False
|
|
211
218
|
|
|
212
219
|
# Write the bytes to file
|
|
213
220
|
with open(file_path, "wb") as f:
|
|
214
221
|
f.write(self._image_data)
|
|
215
222
|
|
|
216
|
-
|
|
223
|
+
logger.debug("ThumbnailInfo: saved the thumbnail to %s", file_path)
|
|
217
224
|
return True
|
|
218
225
|
|
|
219
226
|
except Exception as e:
|
|
220
|
-
|
|
227
|
+
logger.warning("ThumbnailInfo: error saving the thumbnail to a file: %s", e)
|
|
221
228
|
return False
|
|
222
229
|
|
|
223
230
|
def get_image_size(self) -> tuple[int, int]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashforge-python-api
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: A comprehensive Python library for controlling FlashForge 3D printers
|
|
5
5
|
Project-URL: Homepage, https://github.com/GhostTypes/ff-5mp-api-py
|
|
6
6
|
Project-URL: Documentation, https://github.com/GhostTypes/ff-5mp-api-py#readme
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
flashforge/__init__.py,sha256=0PGa08cJ2RjAlIyJqX4MW3w1I-FUIg8ftaJ6I-A5cc0,5336
|
|
2
|
-
flashforge/client.py,sha256=
|
|
2
|
+
flashforge/client.py,sha256=nDtnv3y9XlEQknIkgIUiuOYAiyTEGbQ7tHBdCs2fg1I,21432
|
|
3
3
|
flashforge/api/__init__.py,sha256=vQz-DkG6LTH39bI4fyiUc0D9jQzemLh45pORLptSOlg,335
|
|
4
4
|
flashforge/api/constants/__init__.py,sha256=Q0HL2tqSBYPd4Oz49VHLS3qUvRuv__GCvTGecaLrQ-Y,163
|
|
5
5
|
flashforge/api/constants/commands.py,sha256=XM2rooBESPDaywlZrfW2ECTaLLFaSKXkJ7FQ-RBmVdY,578
|
|
6
6
|
flashforge/api/constants/endpoints.py,sha256=oZtOFfkU64THDeCMUIVv6_0L-BOUZzgLKAUfjHkUhi8,337
|
|
7
7
|
flashforge/api/controls/__init__.py,sha256=53s-H25Pjwr0kvPk8MZ2Twy8VHGqGcO6Q6uBphhEOh4,293
|
|
8
|
-
flashforge/api/controls/control.py,sha256=
|
|
9
|
-
flashforge/api/controls/creator5_palette.py,sha256=
|
|
10
|
-
flashforge/api/controls/files.py,sha256=
|
|
11
|
-
flashforge/api/controls/info.py,sha256=
|
|
12
|
-
flashforge/api/controls/job_control.py,sha256=
|
|
13
|
-
flashforge/api/controls/temp_control.py,sha256=
|
|
8
|
+
flashforge/api/controls/control.py,sha256=OgE7K6NkFLwFN1r_q5g1RPwYC8MCt0f6L5CZxDW44d8,15151
|
|
9
|
+
flashforge/api/controls/creator5_palette.py,sha256=zdD3PIXx7hCVj2Fmv1CSFOaqogRUPeBaymgY2DFiMxk,8835
|
|
10
|
+
flashforge/api/controls/files.py,sha256=25yxln9NROas9TtFMZR9e8LtA8ruk8RUu2C81vpNgTk,8043
|
|
11
|
+
flashforge/api/controls/info.py,sha256=jvP4mCkywfV79Uz_z1uJ7qWvEx0MwRoB6ziuvwps_YU,18206
|
|
12
|
+
flashforge/api/controls/job_control.py,sha256=NnACm4LPUz4n8lnRiUl33jilWekS1b6HQTbP--nDzZc,33225
|
|
13
|
+
flashforge/api/controls/temp_control.py,sha256=EOO5TkODirjFcnQ7rZrirxAweHBdVin032AWj-Z8VQs,13040
|
|
14
14
|
flashforge/api/filament/__init__.py,sha256=isT2dl0hzUS0xMTXMm4Tip0GTG1gCsm8LKWa9xPu4Y8,104
|
|
15
15
|
flashforge/api/filament/filament.py,sha256=TtcC2G2nD9Sq4cOrCZzFLZ6Q0Ve-zfrfuEF9uQ8M3eE,1214
|
|
16
16
|
flashforge/api/misc/__init__.py,sha256=1AG7vOMRBZQtHp6QUzfr9alcJPeNv3JpU44NrznxDHs,211
|
|
17
|
+
flashforge/api/misc/redaction.py,sha256=8LG2WsFXhdBqbZ4AXYwpm8hQ_PxaOeAKmZBeG5kzAT4,2675
|
|
17
18
|
flashforge/api/misc/scientific_notation.py,sha256=KSMTrrYhPWLv0gbcEAD6u-jEOcIwUjOCOPMBi0LiO-k,826
|
|
18
19
|
flashforge/api/misc/temperature.py,sha256=qeHlCdPzLyqGDEB874Ib5AgYHMR9Qtw61shnw4G095E,1066
|
|
19
20
|
flashforge/api/network/__init__.py,sha256=G5fBoAlq-NQPDkWp579mFIqsj0v7B1Lb2TAzO6IZA0Q,164
|
|
@@ -34,14 +35,14 @@ flashforge/tcp/gcode/a3_gcode_controller.py,sha256=x84_xo_Ll5k6hMAwDWuNUo54znV-B
|
|
|
34
35
|
flashforge/tcp/gcode/gcode_controller.py,sha256=6K1VGgX59AcE_Kh_gm13dmxCY_kuzV_d2ooSpheONRI,10075
|
|
35
36
|
flashforge/tcp/gcode/gcodes.py,sha256=55Ii1JpJNJNmOnBivyF83vz3TZUPDtp01lX72NINfPQ,3658
|
|
36
37
|
flashforge/tcp/parsers/__init__.py,sha256=kDaJmiH0JcR5i693Fj3pwmINLWN0ipirN3LWGFvPst0,730
|
|
37
|
-
flashforge/tcp/parsers/endstop_status.py,sha256=
|
|
38
|
+
flashforge/tcp/parsers/endstop_status.py,sha256=k7C5P_TiKlqBxSq2ybRoBDaaDcQ_EgiZe2NLYo7ubiE,9902
|
|
38
39
|
flashforge/tcp/parsers/location_info.py,sha256=0fbqCmQ2EQQe8z9fiX0zP1YsnuVayTs81zmF3_HuGZM,2895
|
|
39
|
-
flashforge/tcp/parsers/print_status.py,sha256=
|
|
40
|
+
flashforge/tcp/parsers/print_status.py,sha256=C-KzukhK0TR86AP3t3XrMH4dief6pa3I-yiST4rL0mk,6271
|
|
40
41
|
flashforge/tcp/parsers/printer_info.py,sha256=CHPs6nJfEByXyG3co7Nb8_ygggxkYuc7A9O9bpQEqXE,5441
|
|
41
42
|
flashforge/tcp/parsers/temp_info.py,sha256=9wRGUM9cKvDiZD3SIxBx_qVLZbeXoYsoY3-R3vN3H4g,7938
|
|
42
|
-
flashforge/tcp/parsers/thumbnail_info.py,sha256=
|
|
43
|
-
flashforge_python_api-1.3.
|
|
44
|
-
flashforge_python_api-1.3.
|
|
45
|
-
flashforge_python_api-1.3.
|
|
46
|
-
flashforge_python_api-1.3.
|
|
47
|
-
flashforge_python_api-1.3.
|
|
43
|
+
flashforge/tcp/parsers/thumbnail_info.py,sha256=1U1S_gcIZ2lDpEA4622ywUew6TVw0BnzjlBAiihQwzk,10478
|
|
44
|
+
flashforge_python_api-1.3.3.dist-info/METADATA,sha256=UpYv26NPSHt5nqH57396UOE9s-JcirHPuNsD8e1afBM,4970
|
|
45
|
+
flashforge_python_api-1.3.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
46
|
+
flashforge_python_api-1.3.3.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
|
|
47
|
+
flashforge_python_api-1.3.3.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
|
|
48
|
+
flashforge_python_api-1.3.3.dist-info/RECORD,,
|
|
File without changes
|
{flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|