flashforge-python-api 1.3.5__py3-none-any.whl → 1.5.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 +1 -1
- flashforge/api/constants/commands.py +1 -1
- flashforge/api/controls/control.py +11 -14
- flashforge/api/controls/creator5_palette.py +3 -3
- flashforge/api/controls/files.py +3 -4
- flashforge/api/controls/info.py +24 -6
- flashforge/api/controls/job_control.py +96 -57
- flashforge/api/controls/temp_control.py +4 -4
- flashforge/client.py +49 -30
- flashforge/exceptions.py +1 -1
- flashforge/models/machine_info.py +8 -3
- flashforge/models/responses.py +17 -10
- {flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/METADATA +2 -2
- {flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/RECORD +17 -17
- {flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/WHEEL +1 -1
- {flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/licenses/LICENSE +0 -0
flashforge/__init__.py
CHANGED
|
@@ -128,7 +128,7 @@ from .tcp import (
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
FiveMClient = FlashForgeClient
|
|
131
|
-
__version__ = "1.
|
|
131
|
+
__version__ = "1.5.0"
|
|
132
132
|
__author__ = "FlashForge Python API Contributors"
|
|
133
133
|
__email__ = "notghosttypes@gmail.com"
|
|
134
134
|
__description__ = "Python library for controlling FlashForge 3D printers"
|
|
@@ -11,5 +11,5 @@ class Commands:
|
|
|
11
11
|
TEMP_CONTROL_CMD = "temperatureCtl_cmd"
|
|
12
12
|
# AD5X + Creator 5 material-station slot metadata (name + color). Filament
|
|
13
13
|
# load/unload (`ms_cmd`) stays AD5X-only — the Creator 5 firmware has no
|
|
14
|
-
# `ms_cmd` (
|
|
14
|
+
# `ms_cmd` (verified in the firmware).
|
|
15
15
|
MATERIAL_STATION_CONFIG_CMD = "msConfig_cmd"
|
|
@@ -237,8 +237,8 @@ class Control:
|
|
|
237
237
|
This information is shown on the printer UI and used for print validation; it
|
|
238
238
|
does not move any filament. Available on the AD5X and the Creator 5 / Creator 5 Pro.
|
|
239
239
|
|
|
240
|
-
The ``msConfig_cmd`` handler is
|
|
241
|
-
(
|
|
240
|
+
The ``msConfig_cmd`` handler is confirmed present on the Creator 5
|
|
241
|
+
(verified in firmware 1.9.2). The Creator 5 has no removable IFS; it
|
|
242
242
|
surfaces its 4 tool heads as the 4 "slots", so this sets per-tool material
|
|
243
243
|
metadata. Both models share the same ``OrcaServer`` command path and wire
|
|
244
244
|
format. Note that **filament load/unload (``slotAction`` / ``ms_cmd``)
|
|
@@ -303,24 +303,21 @@ class Control:
|
|
|
303
303
|
logger.debug("Sending control command %s with args %s", command, args)
|
|
304
304
|
|
|
305
305
|
try:
|
|
306
|
-
await self.client.is_http_client_busy()
|
|
307
|
-
|
|
308
306
|
session = await self.client.get_http_session()
|
|
309
|
-
async with
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
307
|
+
async with self.client.command_lock:
|
|
308
|
+
async with session.post(
|
|
309
|
+
self.client.get_endpoint(Endpoints.CONTROL),
|
|
310
|
+
json=payload,
|
|
311
|
+
headers={"Content-Type": "application/json"},
|
|
312
|
+
) as response:
|
|
313
|
+
data = await json_from_response(response)
|
|
314
|
+
logger.debug("Control command %s reply: %s", command, data)
|
|
316
315
|
|
|
317
|
-
|
|
316
|
+
return NetworkUtils.is_ok(data)
|
|
318
317
|
|
|
319
318
|
except Exception as e:
|
|
320
319
|
logger.warning("Error in send_control_command (%s): %s", command, e)
|
|
321
320
|
return False
|
|
322
|
-
finally:
|
|
323
|
-
self.client.release_http_client()
|
|
324
321
|
|
|
325
322
|
async def send_job_control_cmd(self, command: str) -> bool:
|
|
326
323
|
"""
|
|
@@ -4,8 +4,8 @@ palette and perceptual nearest-color snapping.
|
|
|
4
4
|
|
|
5
5
|
The Creator 5 ``msConfig_cmd`` only renders a color icon when the ``rgb`` field is
|
|
6
6
|
an EXACT, case-sensitive, byte-for-byte match against one of the firmware's 24
|
|
7
|
-
built-in palette strings (
|
|
8
|
-
|
|
7
|
+
built-in palette strings (verified in firmware 1.9.2). A non-match leaves the
|
|
8
|
+
slot's color index at 0 (White). These values
|
|
9
9
|
DIFFER from the AD5X palette (e.g. Blue is ``#4CAAF8`` here vs ``#45A8F9`` on the
|
|
10
10
|
AD5X), so callers must snap against THIS list specifically.
|
|
11
11
|
|
|
@@ -43,7 +43,7 @@ class Creator5PaletteColor:
|
|
|
43
43
|
"""Wire value sent to the printer, always uppercase ``#RRGGBB``."""
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
#: The firmware's 24-entry UI palette (
|
|
46
|
+
#: The firmware's 24-entry UI palette (verified in firmware 1.9.2).
|
|
47
47
|
#: Index 0 (White) is also what the firmware falls back to on a no-match.
|
|
48
48
|
CREATOR5_PALETTE: tuple[Creator5PaletteColor, ...] = (
|
|
49
49
|
Creator5PaletteColor(0, "White", "#FFFFFF"),
|
flashforge/api/controls/files.py
CHANGED
|
@@ -78,10 +78,9 @@ class Files:
|
|
|
78
78
|
|
|
79
79
|
The Creator 5 is the surprise there, and it is firmware, not a parsing gap:
|
|
80
80
|
it is a newer printer than the AD5X but reports less (confirmed against a
|
|
81
|
-
Creator 5 Pro, 2026-08-05). Do not describe this method as "AD5X and
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
must parse the 3mf themselves at upload time.
|
|
81
|
+
live Creator 5 Pro, 2026-08-05). Do not describe this method as "AD5X and
|
|
82
|
+
newer" - only the AD5X answers with `gcodeListDetail`. Callers that need
|
|
83
|
+
per-tool data on a Creator 5 must parse the 3mf themselves at upload time.
|
|
85
84
|
|
|
86
85
|
Returns:
|
|
87
86
|
A list of FFGcodeFileEntry objects. Returns an empty list if the request fails or an error occurs.
|
flashforge/api/controls/info.py
CHANGED
|
@@ -46,6 +46,19 @@ PID_MODEL_NAMES: dict[int, str] = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# The one state in which the firmware actually counts `estimatedTime` down.
|
|
50
|
+
# Outside it the field freezes at its last value while the wall clock keeps
|
|
51
|
+
# moving, so `now() + estimatedTime` walks forward one minute per minute instead
|
|
52
|
+
# of holding still - a paused print appears to recede forever. The duration
|
|
53
|
+
# stays correct throughout; only its conversion to an absolute timestamp is
|
|
54
|
+
# invalid, which is why `print_eta` is ungated and `completion_time` is not.
|
|
55
|
+
#
|
|
56
|
+
# HEATING is deliberately excluded. The pre-print warmup does not advance the
|
|
57
|
+
# job either, so the same drift applies - it just lasts minutes rather than
|
|
58
|
+
# hours, which is why it is easy to miss.
|
|
59
|
+
_ADVANCING_STATES = frozenset({MachineState.PRINTING})
|
|
60
|
+
|
|
61
|
+
|
|
49
62
|
class MachineInfoParser:
|
|
50
63
|
"""
|
|
51
64
|
Transforms printer detail data from the API response format into a structured FFMachineInfo object.
|
|
@@ -75,7 +88,14 @@ class MachineInfoParser:
|
|
|
75
88
|
formatted_run_time = MachineInfoParser._format_time_from_seconds(
|
|
76
89
|
getattr(detail, "print_duration", 0) or 0
|
|
77
90
|
)
|
|
78
|
-
|
|
91
|
+
machine_state = MachineInfoParser._get_machine_state(
|
|
92
|
+
getattr(detail, "status", "") or ""
|
|
93
|
+
)
|
|
94
|
+
completion_time = (
|
|
95
|
+
datetime.now() + timedelta(seconds=estimated_time)
|
|
96
|
+
if machine_state in _ADVANCING_STATES
|
|
97
|
+
else None
|
|
98
|
+
)
|
|
79
99
|
|
|
80
100
|
total_minutes = getattr(detail, "cumulative_print_time", 0) or 0
|
|
81
101
|
hours = total_minutes // 60
|
|
@@ -255,9 +275,7 @@ class MachineInfoParser:
|
|
|
255
275
|
print_speed_adjust=getattr(detail, "print_speed_adjust", 0) or 0,
|
|
256
276
|
filament_type=getattr(detail, "right_filament_type", "") or "",
|
|
257
277
|
# Machine state
|
|
258
|
-
machine_state=
|
|
259
|
-
getattr(detail, "status", "") or ""
|
|
260
|
-
),
|
|
278
|
+
machine_state=machine_state,
|
|
261
279
|
status=getattr(detail, "status", "") or "",
|
|
262
280
|
total_print_layers=getattr(detail, "target_print_layer", 0) or 0,
|
|
263
281
|
tvoc=getattr(detail, "tvoc", 0) or 0,
|
|
@@ -386,7 +404,7 @@ class Info:
|
|
|
386
404
|
if machine_info is None:
|
|
387
405
|
# We validated the payload but could not build our own model from
|
|
388
406
|
# it - that is a bug on our side, not an unreachable printer, and
|
|
389
|
-
# reporting it as the latter is what
|
|
407
|
+
# reporting it as the latter is what ff-5mp-hass#18 was about.
|
|
390
408
|
raise FlashForgeResponseError(
|
|
391
409
|
"The /detail payload was read but could not be converted into machine info",
|
|
392
410
|
endpoint=Endpoints.DETAIL,
|
|
@@ -444,7 +462,7 @@ class Info:
|
|
|
444
462
|
This exists so callers can inspect identity fields - above all `pid` -
|
|
445
463
|
without first having to successfully validate the ~50 unrelated fields
|
|
446
464
|
in the payload. A supported printer must never be rejected because of a
|
|
447
|
-
field that has nothing to do with whether it is supported (
|
|
465
|
+
field that has nothing to do with whether it is supported (ff-5mp-hass#18).
|
|
448
466
|
|
|
449
467
|
Returns:
|
|
450
468
|
The decoded response body, or None if the printer could not be
|
|
@@ -253,16 +253,17 @@ class JobControl:
|
|
|
253
253
|
|
|
254
254
|
try:
|
|
255
255
|
session = await self.client.get_http_session()
|
|
256
|
-
async with
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
256
|
+
async with self.client.command_lock:
|
|
257
|
+
async with session.post(
|
|
258
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
259
|
+
json=payload,
|
|
260
|
+
headers={"Content-Type": "application/json"},
|
|
261
|
+
) as response:
|
|
262
|
+
if response.status != 200:
|
|
263
|
+
return False
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
result = await json_from_response(response)
|
|
266
|
+
return NetworkUtils.is_ok(result)
|
|
266
267
|
|
|
267
268
|
except Exception as error:
|
|
268
269
|
logger.warning("print_local_file error: %s", error)
|
|
@@ -422,16 +423,17 @@ class JobControl:
|
|
|
422
423
|
|
|
423
424
|
try:
|
|
424
425
|
session = await self.client.get_http_session()
|
|
425
|
-
async with
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
426
|
+
async with self.client.command_lock:
|
|
427
|
+
async with session.post(
|
|
428
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
429
|
+
json=payload,
|
|
430
|
+
headers={"Content-Type": "application/json"},
|
|
431
|
+
) as response:
|
|
432
|
+
if response.status != 200:
|
|
433
|
+
return False
|
|
432
434
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
result = await json_from_response(response)
|
|
436
|
+
return NetworkUtils.is_ok(result)
|
|
435
437
|
|
|
436
438
|
except Exception as error:
|
|
437
439
|
logger.warning("AD5X multi-color job error: %s", error)
|
|
@@ -474,40 +476,59 @@ class JobControl:
|
|
|
474
476
|
|
|
475
477
|
try:
|
|
476
478
|
session = await self.client.get_http_session()
|
|
477
|
-
async with
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
async with self.client.command_lock:
|
|
480
|
+
async with session.post(
|
|
481
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
482
|
+
json=payload,
|
|
483
|
+
headers={"Content-Type": "application/json"},
|
|
484
|
+
) as response:
|
|
485
|
+
if response.status != 200:
|
|
486
|
+
return False
|
|
484
487
|
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
result = await json_from_response(response)
|
|
489
|
+
return NetworkUtils.is_ok(result)
|
|
487
490
|
|
|
488
491
|
except Exception as error:
|
|
489
492
|
logger.warning("AD5X single-color job error: %s", error)
|
|
490
493
|
raise error
|
|
491
494
|
|
|
492
495
|
# --- Creator 5 / Creator 5 Pro ---
|
|
493
|
-
# The Creator 5
|
|
494
|
-
#
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
#
|
|
498
|
-
#
|
|
499
|
-
#
|
|
500
|
-
#
|
|
496
|
+
# The Creator 5 firmware applies material mappings in two places:
|
|
497
|
+
# - POST /printGcode reads `materialMappings` for any file already on the
|
|
498
|
+
# printer (see start_creator5_job). This is the recommended path: upload
|
|
499
|
+
# with start_print=False, then start with the mappings.
|
|
500
|
+
# - POST /uploadGcode also reads a base64 `materialMappings` header, the
|
|
501
|
+
# same format as the AD5X, and applies it when `printNow` is true.
|
|
502
|
+
# The firmware only logs `useMatlStation` and `gcodeToolCnt` on upload; they
|
|
503
|
+
# have no effect. There is NO `firstLayerInspection` header (the field doesn't
|
|
504
|
+
# exist on the C5), and the booleans are checked as the string "true"/"false".
|
|
505
|
+
#
|
|
506
|
+
# Mappings only take effect for a .3mf file. Without mappings, tool N prints
|
|
507
|
+
# from the slot with the slicer's filament number (filament 1 -> slot 1),
|
|
508
|
+
# whatever is loaded there. Send a mapping for every tool, including a
|
|
509
|
+
# single-tool print.
|
|
501
510
|
|
|
502
511
|
async def upload_file_creator5(self, params: Creator5UploadParams) -> bool:
|
|
503
512
|
"""
|
|
504
513
|
Uploads a file (.gcode or .3mf) to a Creator 5 / Creator 5 Pro via
|
|
505
514
|
``POST /uploadGcode``, with the C5-specific material-station headers.
|
|
506
515
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
516
|
+
Recommended flow: upload with ``start_print=False``, then call
|
|
517
|
+
:meth:`start_creator5_job` with a mapping for every tool. To start in one
|
|
518
|
+
request instead, pass ``material_mappings`` with ``start_print=True``: the
|
|
519
|
+
firmware reads them from a base64 ``materialMappings`` header, like the
|
|
520
|
+
AD5X.
|
|
521
|
+
|
|
522
|
+
``material_mappings`` is sent only when ``start_print`` is true. The
|
|
523
|
+
firmware keeps upload mappings in memory until the next print ends, so
|
|
524
|
+
mappings on an upload that does not start would apply to a later,
|
|
525
|
+
unrelated print.
|
|
526
|
+
|
|
527
|
+
``useMatlStation`` and ``gcodeToolCnt`` are sent for parity with
|
|
528
|
+
FlashForge's own clients; the firmware only logs them. Unlike
|
|
529
|
+
:meth:`upload_file_ad5x` this sends no ``firstLayerInspection`` header
|
|
530
|
+
(the C5 has no such field). Booleans are sent as the string
|
|
531
|
+
"true"/"false".
|
|
511
532
|
|
|
512
533
|
Args:
|
|
513
534
|
params: Creator 5 upload parameters.
|
|
@@ -537,8 +558,7 @@ class JobControl:
|
|
|
537
558
|
|
|
538
559
|
try:
|
|
539
560
|
# C5 upload headers. No firstLayerInspection (absent on the C5);
|
|
540
|
-
# booleans sent as "true"/"false" (firmware checks for "true")
|
|
541
|
-
# materialMappings header (C5 maps at print-start, not upload).
|
|
561
|
+
# booleans sent as "true"/"false" (firmware checks for "true").
|
|
542
562
|
custom_headers = {
|
|
543
563
|
"serialNumber": self.client.serial_number,
|
|
544
564
|
"checkCode": self.client.check_code,
|
|
@@ -552,6 +572,20 @@ class JobControl:
|
|
|
552
572
|
"Expect": "100-continue",
|
|
553
573
|
}
|
|
554
574
|
|
|
575
|
+
mappings = params.material_mappings or []
|
|
576
|
+
if mappings:
|
|
577
|
+
if not params.start_print:
|
|
578
|
+
logger.warning(
|
|
579
|
+
"upload_file_creator5: material_mappings ignored because start_print "
|
|
580
|
+
"is False; pass them to start_creator5_job instead."
|
|
581
|
+
)
|
|
582
|
+
else:
|
|
583
|
+
if not self._validate_creator5_material_mappings(mappings):
|
|
584
|
+
return False
|
|
585
|
+
custom_headers["materialMappings"] = self._encode_material_mappings_to_base64(
|
|
586
|
+
mappings
|
|
587
|
+
)
|
|
588
|
+
|
|
555
589
|
logger.debug("Creator 5 upload request headers: %s", redact_mapping(custom_headers))
|
|
556
590
|
|
|
557
591
|
# Create multipart form data
|
|
@@ -600,13 +634,17 @@ class JobControl:
|
|
|
600
634
|
"""
|
|
601
635
|
Starts a local print on a Creator 5 / Creator 5 Pro via ``POST /printGcode``.
|
|
602
636
|
|
|
603
|
-
This is the Creator 5's print-start material-matching command
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
637
|
+
This is the Creator 5's print-start material-matching command. The file
|
|
638
|
+
must already be on the printer; it can be a file uploaded long before, not
|
|
639
|
+
only the last upload. Sends only the fields the Creator 5 firmware reads:
|
|
640
|
+
NO ``useMatlStation`` / ``gcodeToolCnt`` and NO ``firstLayerInspection``
|
|
641
|
+
(doesn't exist on the C5). ``flowCalibration`` and ``timeLapseVideo`` are
|
|
642
|
+
always present (default False).
|
|
643
|
+
|
|
644
|
+
Provide a mapping for every tool, including a single-tool print. Without
|
|
645
|
+
mappings the firmware prints each tool from the slot with the slicer's
|
|
646
|
+
filament number (filament 1 -> slot 1), whatever is loaded there.
|
|
647
|
+
Mappings take effect only for a .3mf file.
|
|
610
648
|
|
|
611
649
|
Args:
|
|
612
650
|
params: File name, leveling flag, and optional flags / material mappings.
|
|
@@ -653,16 +691,17 @@ class JobControl:
|
|
|
653
691
|
|
|
654
692
|
try:
|
|
655
693
|
session = await self.client.get_http_session()
|
|
656
|
-
async with
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
694
|
+
async with self.client.command_lock:
|
|
695
|
+
async with session.post(
|
|
696
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
697
|
+
json=payload,
|
|
698
|
+
headers={"Content-Type": "application/json"},
|
|
699
|
+
) as response:
|
|
700
|
+
if response.status != 200:
|
|
701
|
+
return False
|
|
663
702
|
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
result = await json_from_response(response)
|
|
704
|
+
return NetworkUtils.is_ok(result)
|
|
666
705
|
|
|
667
706
|
except Exception as error:
|
|
668
707
|
logger.warning("Creator 5 job error: %s", error)
|
|
@@ -27,12 +27,12 @@ TEMP_OFF = -100
|
|
|
27
27
|
# Value that turns a tool/nozzle OFF inside the `nozzles` array. Unlike the scalar
|
|
28
28
|
# heater fields (which accept TEMP_OFF = -100), the Creator 5 firmware's
|
|
29
29
|
# per-nozzle parser only treats a literal 0 as "off" — it ignores -100 in the
|
|
30
|
-
# `nozzles` array and the tool keeps heating
|
|
31
|
-
#
|
|
30
|
+
# `nozzles` array and the tool keeps heating (observed on live hardware;
|
|
31
|
+
# this is the v1.6.1 nozzle-off bugfix).
|
|
32
32
|
NOZZLE_OFF = 0
|
|
33
33
|
# Number of tool/nozzle entries the Creator 5 firmware requires in the
|
|
34
34
|
# `nozzles` array. The firmware ignores the array unless its length is exactly
|
|
35
|
-
# this (
|
|
35
|
+
# this (verified in the firmware).
|
|
36
36
|
NOZZLE_COUNT = 4
|
|
37
37
|
|
|
38
38
|
|
|
@@ -188,7 +188,7 @@ class TempControl:
|
|
|
188
188
|
"""
|
|
189
189
|
if self.client.http_only:
|
|
190
190
|
# Creator 5 tools are driven ONLY via the `nozzles` array — the
|
|
191
|
-
# firmware's
|
|
191
|
+
# firmware's temperature-control handler never reads
|
|
192
192
|
# rightNozzle/leftNozzle. Target the primary tool (T0): the active
|
|
193
193
|
# tool isn't reliably known over HTTP, so callers that need a
|
|
194
194
|
# specific tool should use set_tool_temp(index, temp).
|
flashforge/client.py
CHANGED
|
@@ -75,6 +75,15 @@ class FlashForgeClient:
|
|
|
75
75
|
self._http_client_event = asyncio.Event()
|
|
76
76
|
self._http_client_event.set() # Not busy initially
|
|
77
77
|
|
|
78
|
+
# FIFO mutex for command submission. asyncio.Lock hands the lock to
|
|
79
|
+
# waiters in the order they asked for it. So command POSTs run one at
|
|
80
|
+
# a time, in submission order. Scope: command POSTs only (/control,
|
|
81
|
+
# /product, /printGcode). File uploads (/uploadGcode) and read/poll
|
|
82
|
+
# POSTs (/detail, /gcodeList, /gcodeThumb) stay outside the lock. An
|
|
83
|
+
# upload can run for minutes; pause and stop must never queue behind
|
|
84
|
+
# one.
|
|
85
|
+
self._command_lock = asyncio.Lock()
|
|
86
|
+
|
|
78
87
|
# TCP client setup
|
|
79
88
|
tcp_options = None
|
|
80
89
|
if options and options.tcp_port is not None:
|
|
@@ -202,6 +211,19 @@ class FlashForgeClient:
|
|
|
202
211
|
self._http_session = aiohttp.ClientSession(timeout=timeout, headers={"Accept": "*/*"})
|
|
203
212
|
return self._http_session
|
|
204
213
|
|
|
214
|
+
@property
|
|
215
|
+
def command_lock(self) -> asyncio.Lock:
|
|
216
|
+
"""
|
|
217
|
+
The FIFO lock that serializes command-submission HTTP POSTs.
|
|
218
|
+
|
|
219
|
+
Command POSTs (/control, /product, /printGcode) hold this lock while
|
|
220
|
+
the request is in flight. asyncio.Lock serves waiters in arrival order,
|
|
221
|
+
so concurrent commands run one at a time, first come first served.
|
|
222
|
+
Uploads and read/poll requests never take this lock. So a long upload
|
|
223
|
+
cannot delay a pause or stop command.
|
|
224
|
+
"""
|
|
225
|
+
return self._command_lock
|
|
226
|
+
|
|
205
227
|
async def initialize(self) -> bool:
|
|
206
228
|
"""
|
|
207
229
|
Initializes the FlashForgeClient and verifies the connection to the printer.
|
|
@@ -442,43 +464,42 @@ class FlashForgeClient:
|
|
|
442
464
|
"""
|
|
443
465
|
Sends a product command to the printer to retrieve control states.
|
|
444
466
|
|
|
445
|
-
This method
|
|
467
|
+
This method holds the command lock while the request is in progress.
|
|
446
468
|
|
|
447
469
|
Returns:
|
|
448
470
|
True if the product command is sent successfully and valid data is received,
|
|
449
471
|
False otherwise
|
|
450
472
|
"""
|
|
451
|
-
self._http_client_busy = True
|
|
452
|
-
|
|
453
473
|
payload = {"serialNumber": self.serial_number, "checkCode": self.check_code}
|
|
454
474
|
|
|
455
475
|
try:
|
|
456
476
|
session = await self._ensure_http_session()
|
|
457
|
-
async with
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
477
|
+
async with self._command_lock:
|
|
478
|
+
async with session.post(
|
|
479
|
+
self.get_endpoint(Endpoints.PRODUCT),
|
|
480
|
+
json=payload,
|
|
481
|
+
headers={"Content-Type": "application/json"},
|
|
482
|
+
) as response:
|
|
483
|
+
if response.status != 200:
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
data = await json_from_response(response)
|
|
487
|
+
|
|
488
|
+
# Validate response structure
|
|
489
|
+
if not NetworkUtils.is_ok(data):
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
# Parse product response and set control states
|
|
493
|
+
product_response = ProductResponse(**data)
|
|
494
|
+
if product_response and product_response.product:
|
|
495
|
+
product = product_response.product
|
|
496
|
+
self.product_info = product
|
|
497
|
+
self._detected_led_control = product.lightCtrlState != 0
|
|
498
|
+
self._detected_filtration_control = not (
|
|
499
|
+
product.internalFanCtrlState == 0 or product.externalFanCtrlState == 0
|
|
500
|
+
)
|
|
501
|
+
self._apply_feature_overrides()
|
|
502
|
+
return True
|
|
482
503
|
|
|
483
504
|
except ValidationError as error:
|
|
484
505
|
# Distinct from a rejected check code, and it must not read as one:
|
|
@@ -496,8 +517,6 @@ class FlashForgeClient:
|
|
|
496
517
|
except Exception as error:
|
|
497
518
|
logger.warning("Error in send_product_command: %s", error)
|
|
498
519
|
return False
|
|
499
|
-
finally:
|
|
500
|
-
self._http_client_busy = False
|
|
501
520
|
|
|
502
521
|
return False
|
|
503
522
|
|
flashforge/exceptions.py
CHANGED
|
@@ -15,7 +15,7 @@ class FlashForgeResponseError(FlashForgeError):
|
|
|
15
15
|
|
|
16
16
|
This is deliberately distinct from a ``None`` return, which every HTTP
|
|
17
17
|
control method uses for "could not reach the printer / the printer refused
|
|
18
|
-
us". Collapsing the two is what made
|
|
18
|
+
us". Collapsing the two is what made ff-5mp-hass#18 take three releases to
|
|
19
19
|
diagnose: a Creator 5 reporting ``chamberTemp: -108`` (the firmware's "no
|
|
20
20
|
chamber sensor" sentinel) surfaced in Home Assistant as ``cannot_connect``,
|
|
21
21
|
pointing the user at their network for a schema problem.
|
|
@@ -20,7 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
|
|
20
20
|
# blast radius: pydantic fails a model as a unit, and `Info.get_detail_response`
|
|
21
21
|
# turns any failure into "no data", which Home Assistant shows as an offline
|
|
22
22
|
# printer. So a `ge=0` on a field nobody reads could take the whole integration
|
|
23
|
-
# down. That is exactly what happened in
|
|
23
|
+
# down. That is exactly what happened in ff-5mp-hass#18, where a Creator 5 reporting
|
|
24
24
|
# `chamberTemp: -108` made every entity unavailable and the config flow report
|
|
25
25
|
# `cannot_connect`.
|
|
26
26
|
#
|
|
@@ -369,7 +369,7 @@ class FFPrinterDetail(BaseModel):
|
|
|
369
369
|
z_axis_compensation: float | None = Field(default=None, alias="zAxisCompensation")
|
|
370
370
|
|
|
371
371
|
# Firmware reports absent temperature hardware with a negative sentinel
|
|
372
|
-
# (-108 on a chamber-less Creator 5,
|
|
372
|
+
# (-108 on a chamber-less Creator 5, ff-5mp-hass#18) rather than by omitting the
|
|
373
373
|
# field. Map those to None here so "no sensor" reads as "not reported"
|
|
374
374
|
# everywhere downstream, instead of as a -108 C reading or a hard failure.
|
|
375
375
|
@field_validator(
|
|
@@ -514,7 +514,12 @@ class FFMachineInfo(BaseModel):
|
|
|
514
514
|
|
|
515
515
|
# Extras
|
|
516
516
|
print_eta: str = "00:00"
|
|
517
|
-
|
|
517
|
+
# None whenever the print is not advancing (paused, heating, ready, error,
|
|
518
|
+
# ...). The firmware freezes `estimatedTime` outside the PRINTING state, so
|
|
519
|
+
# an absolute timestamp derived from it would recede in real time rather
|
|
520
|
+
# than hold. Use `print_eta` / `estimated_time` for the remaining duration,
|
|
521
|
+
# which stays valid in every state.
|
|
522
|
+
completion_time: datetime | None = None
|
|
518
523
|
formatted_run_time: str = "00:00"
|
|
519
524
|
formatted_total_run_time: str = "0h:0m"
|
|
520
525
|
|
flashforge/models/responses.py
CHANGED
|
@@ -175,12 +175,12 @@ class AD5XUploadParams(BaseModel):
|
|
|
175
175
|
class Creator5JobParams(BaseModel):
|
|
176
176
|
"""Parameters for starting a Creator 5 / Creator 5 Pro local print job.
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
(
|
|
180
|
-
``
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
The body carries NO ``useMatlStation`` / ``gcodeToolCnt`` /
|
|
179
|
+
``firstLayerInspection`` (the latter doesn't exist on the C5).
|
|
180
|
+
``flowCalibration`` and ``timeLapseVideo`` are always present (default False).
|
|
181
|
+
Map every tool, including a single-tool print: without mappings the firmware
|
|
182
|
+
prints each tool from the slot with the slicer's filament number (filament 1
|
|
183
|
+
-> slot 1). Mappings take effect only for a .3mf file.
|
|
184
184
|
"""
|
|
185
185
|
|
|
186
186
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
@@ -196,16 +196,18 @@ class Creator5JobParams(BaseModel):
|
|
|
196
196
|
material_mappings: list[AD5XMaterialMapping] | None = Field(
|
|
197
197
|
default=None,
|
|
198
198
|
max_length=4,
|
|
199
|
-
description="
|
|
199
|
+
description="Per-tool material mappings (1-4 items); recommended for every print",
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
class Creator5UploadParams(BaseModel):
|
|
204
204
|
"""Parameters for uploading a file to a Creator 5 / Creator 5 Pro.
|
|
205
205
|
|
|
206
|
-
Mirrors the AD5X upload but omits ``firstLayerInspection`` (absent on the
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
Mirrors the AD5X upload but omits ``firstLayerInspection`` (absent on the
|
|
207
|
+
C5). ``material_mappings`` is sent as the base64 ``materialMappings`` header
|
|
208
|
+
only when ``start_print`` is true. The firmware only logs ``use_matl_station``
|
|
209
|
+
and ``gcode_tool_cnt``. The C5 firmware checks the booleans as the string
|
|
210
|
+
"true"/"false".
|
|
209
211
|
"""
|
|
210
212
|
|
|
211
213
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
@@ -223,6 +225,11 @@ class Creator5UploadParams(BaseModel):
|
|
|
223
225
|
gcode_tool_cnt: int = Field(
|
|
224
226
|
ge=1, le=4, description="Number of tools in the G-code (1-4 for the C5)"
|
|
225
227
|
)
|
|
228
|
+
material_mappings: list[AD5XMaterialMapping] | None = Field(
|
|
229
|
+
default=None,
|
|
230
|
+
max_length=4,
|
|
231
|
+
description="Optional per-tool material mappings, sent only when start_print is true",
|
|
232
|
+
)
|
|
226
233
|
|
|
227
234
|
|
|
228
235
|
class GCodeListResponse(GenericResponse):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: flashforge-python-api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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,17 +1,17 @@
|
|
|
1
|
-
flashforge/__init__.py,sha256=
|
|
2
|
-
flashforge/client.py,sha256=
|
|
3
|
-
flashforge/exceptions.py,sha256=
|
|
1
|
+
flashforge/__init__.py,sha256=dDzpUX4K063NRYBvhj1QkfFxxQpb1aSMMkSEFQTzxhI,5472
|
|
2
|
+
flashforge/client.py,sha256=5DxKJyicq5LX8xfZUtIB0Q-_sh8kIFNzMMV_3Dos5fk,22519
|
|
3
|
+
flashforge/exceptions.py,sha256=LQli1pAH2X8fm3jsRD4caz0Mn-nyBjNpaJh_En7YJw0,1737
|
|
4
4
|
flashforge/api/__init__.py,sha256=vQz-DkG6LTH39bI4fyiUc0D9jQzemLh45pORLptSOlg,335
|
|
5
5
|
flashforge/api/constants/__init__.py,sha256=Q0HL2tqSBYPd4Oz49VHLS3qUvRuv__GCvTGecaLrQ-Y,163
|
|
6
|
-
flashforge/api/constants/commands.py,sha256=
|
|
6
|
+
flashforge/api/constants/commands.py,sha256=9WggRLBgidf4cBBXMllQVAZr64UvHOf7gNHlPEKvo2g,579
|
|
7
7
|
flashforge/api/constants/endpoints.py,sha256=oZtOFfkU64THDeCMUIVv6_0L-BOUZzgLKAUfjHkUhi8,337
|
|
8
8
|
flashforge/api/controls/__init__.py,sha256=53s-H25Pjwr0kvPk8MZ2Twy8VHGqGcO6Q6uBphhEOh4,293
|
|
9
|
-
flashforge/api/controls/control.py,sha256=
|
|
10
|
-
flashforge/api/controls/creator5_palette.py,sha256=
|
|
11
|
-
flashforge/api/controls/files.py,sha256=
|
|
12
|
-
flashforge/api/controls/info.py,sha256=
|
|
13
|
-
flashforge/api/controls/job_control.py,sha256=
|
|
14
|
-
flashforge/api/controls/temp_control.py,sha256=
|
|
9
|
+
flashforge/api/controls/control.py,sha256=mK-k3SX53wrq3-aPmMaAbAnqcOaJgvuNSR-sm1aowJM,15099
|
|
10
|
+
flashforge/api/controls/creator5_palette.py,sha256=1I6QfAYlCJcpqPzrySsXfn2EST04QYHZKVxCRnxu4AQ,8795
|
|
11
|
+
flashforge/api/controls/files.py,sha256=tOqPgf9SBIvJBgno1GMAROdoDHF_ZJOmbEac1ikq_HI,8769
|
|
12
|
+
flashforge/api/controls/info.py,sha256=E-VepEeTyquh50ssckpMGIkH9cagNZGUzn_Zb-bPplY,24846
|
|
13
|
+
flashforge/api/controls/job_control.py,sha256=e_8A1-avghNGQ3OBzTbScDnUCUOJobdiulFCyXtyb8w,35294
|
|
14
|
+
flashforge/api/controls/temp_control.py,sha256=_qrKCfjKJsPE4dj6bXZeJRH2FjLKx2T2V3pyG2oLrgI,12991
|
|
15
15
|
flashforge/api/filament/__init__.py,sha256=isT2dl0hzUS0xMTXMm4Tip0GTG1gCsm8LKWa9xPu4Y8,104
|
|
16
16
|
flashforge/api/filament/filament.py,sha256=TtcC2G2nD9Sq4cOrCZzFLZ6Q0Ve-zfrfuEF9uQ8M3eE,1214
|
|
17
17
|
flashforge/api/misc/__init__.py,sha256=1AG7vOMRBZQtHp6QUzfr9alcJPeNv3JpU44NrznxDHs,211
|
|
@@ -24,8 +24,8 @@ flashforge/api/network/utils.py,sha256=Q5-Vj_1VN611QV_TGpDzrrZ2X8PqaGT0j00BhSHv5
|
|
|
24
24
|
flashforge/discovery/__init__.py,sha256=G0WiP70EhfHdjXR1RNlv6xjhyHOdo-HWyOm9J0ds4SM,828
|
|
25
25
|
flashforge/discovery/discovery.py,sha256=-hOmVIpn8gu-rPYo8FRBfhKb_tBqwamLfmoPVYaDzLI,27614
|
|
26
26
|
flashforge/models/__init__.py,sha256=hdcK0E4KeDfv_A8Y2aPOHgHziwTjP1RPb_VP-89dOzQ,1086
|
|
27
|
-
flashforge/models/machine_info.py,sha256=
|
|
28
|
-
flashforge/models/responses.py,sha256=
|
|
27
|
+
flashforge/models/machine_info.py,sha256=Chcpoeus1pppPKMDNsGIiCGGQ6tATezUT6ML5MjubD0,24407
|
|
28
|
+
flashforge/models/responses.py,sha256=P0Zo8c6QywbS0_kEWq-8HsRwwQkCiG4MfeDcGADpPHg,10965
|
|
29
29
|
flashforge/tcp/__init__.py,sha256=hpnqoWHeRtTwJPzdsVlwGt1njKbAkrgIHKADkLSIRec,1317
|
|
30
30
|
flashforge/tcp/a3_client.py,sha256=Aqb6MuKkjnNUVdE2LbZKd7bPlFK8XPIm17C_BG_PuNU,15867
|
|
31
31
|
flashforge/tcp/a4_client.py,sha256=snmAvTCpSWFjxb8h6V7H4tk5yOoMqKEzKoBY_1JaaPs,11213
|
|
@@ -42,8 +42,8 @@ flashforge/tcp/parsers/print_status.py,sha256=C-KzukhK0TR86AP3t3XrMH4dief6pa3I-y
|
|
|
42
42
|
flashforge/tcp/parsers/printer_info.py,sha256=CHPs6nJfEByXyG3co7Nb8_ygggxkYuc7A9O9bpQEqXE,5441
|
|
43
43
|
flashforge/tcp/parsers/temp_info.py,sha256=9wRGUM9cKvDiZD3SIxBx_qVLZbeXoYsoY3-R3vN3H4g,7938
|
|
44
44
|
flashforge/tcp/parsers/thumbnail_info.py,sha256=1U1S_gcIZ2lDpEA4622ywUew6TVw0BnzjlBAiihQwzk,10478
|
|
45
|
-
flashforge_python_api-1.
|
|
46
|
-
flashforge_python_api-1.
|
|
47
|
-
flashforge_python_api-1.
|
|
48
|
-
flashforge_python_api-1.
|
|
49
|
-
flashforge_python_api-1.
|
|
45
|
+
flashforge_python_api-1.5.0.dist-info/METADATA,sha256=hpoGseNvotHfDNDJXpItMUbp9Ehr75sj-wgOgO2jfDU,4970
|
|
46
|
+
flashforge_python_api-1.5.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
47
|
+
flashforge_python_api-1.5.0.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
|
|
48
|
+
flashforge_python_api-1.5.0.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
|
|
49
|
+
flashforge_python_api-1.5.0.dist-info/RECORD,,
|
{flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{flashforge_python_api-1.3.5.dist-info → flashforge_python_api-1.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|