flashforge-python-api 1.2.3__py3-none-any.whl → 1.3.1__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 +4 -0
- flashforge/api/constants/endpoints.py +0 -1
- flashforge/api/controls/control.py +65 -4
- flashforge/api/controls/creator5_palette.py +255 -0
- flashforge/api/controls/files.py +12 -1
- flashforge/api/controls/info.py +92 -3
- flashforge/api/controls/job_control.py +282 -40
- flashforge/api/controls/temp_control.py +217 -7
- flashforge/api/network/utils.py +1 -1
- flashforge/client.py +92 -13
- flashforge/discovery/discovery.py +43 -6
- flashforge/models/__init__.py +4 -0
- flashforge/models/machine_info.py +35 -0
- flashforge/models/responses.py +59 -0
- {flashforge_python_api-1.2.3.dist-info → flashforge_python_api-1.3.1.dist-info}/METADATA +5 -3
- {flashforge_python_api-1.2.3.dist-info → flashforge_python_api-1.3.1.dist-info}/RECORD +20 -19
- {flashforge_python_api-1.2.3.dist-info → flashforge_python_api-1.3.1.dist-info}/WHEEL +1 -1
- {flashforge_python_api-1.2.3.dist-info → flashforge_python_api-1.3.1.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.2.3.dist-info → flashforge_python_api-1.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,7 @@ import base64
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
import aiohttp
|
|
12
12
|
|
|
@@ -15,6 +15,8 @@ from ...models.responses import (
|
|
|
15
15
|
AD5XMaterialMapping,
|
|
16
16
|
AD5XSingleColorJobParams,
|
|
17
17
|
AD5XUploadParams,
|
|
18
|
+
Creator5JobParams,
|
|
19
|
+
Creator5UploadParams,
|
|
18
20
|
)
|
|
19
21
|
from ..constants.endpoints import Endpoints
|
|
20
22
|
from ..network.utils import NetworkUtils, json_from_response
|
|
@@ -83,6 +85,13 @@ class JobControl:
|
|
|
83
85
|
Returns:
|
|
84
86
|
True if the firmware is new (>= 3.1.3), False otherwise or if version cannot be determined.
|
|
85
87
|
"""
|
|
88
|
+
# The 3.1.3 threshold only applies to the 5M family. The AD5X and Creator 5
|
|
89
|
+
# series always use the new payload/header format, and their firmware
|
|
90
|
+
# versioning isn't comparable to the 5M's 3.x line (e.g. the C5 reports
|
|
91
|
+
# 1.9.2, which the numeric check below would wrongly read as "old"), so
|
|
92
|
+
# short-circuit to the new format for them.
|
|
93
|
+
if self.client.is_ad5x or self.client.is_creator5 or self.client.is_creator5_pro:
|
|
94
|
+
return True
|
|
86
95
|
try:
|
|
87
96
|
current_version = self.client.firmware_ver.split(".")
|
|
88
97
|
min_version = [3, 1, 3]
|
|
@@ -457,33 +466,150 @@ class JobControl:
|
|
|
457
466
|
print(f"AD5X Single-Color Job error: {error}")
|
|
458
467
|
raise error
|
|
459
468
|
|
|
460
|
-
|
|
469
|
+
# --- Creator 5 / Creator 5 Pro ---
|
|
470
|
+
# The Creator 5 splits the material-station workflow across two requests,
|
|
471
|
+
# unlike the AD5X (which maps materials at upload time). On the C5:
|
|
472
|
+
# 1. Upload the file (POST /uploadGcode). The firmware reads `useMatlStation`
|
|
473
|
+
# and `gcodeToolCnt` here to register the file as a multi-tool job. There
|
|
474
|
+
# is NO `firstLayerInspection` header (the field doesn't exist on the
|
|
475
|
+
# C5), and the booleans are checked as the string "true"/"false".
|
|
476
|
+
# 2. Start the print (POST /printGcode) with the per-tool
|
|
477
|
+
# `materialMappings`. See start_creator5_job.
|
|
478
|
+
|
|
479
|
+
async def upload_file_creator5(self, params: Creator5UploadParams) -> bool:
|
|
461
480
|
"""
|
|
462
|
-
|
|
481
|
+
Uploads a file (.gcode or .3mf) to a Creator 5 / Creator 5 Pro via
|
|
482
|
+
``POST /uploadGcode``, with the C5-specific material-station headers.
|
|
483
|
+
|
|
484
|
+
Unlike :meth:`upload_file_ad5x` this sends no ``firstLayerInspection``
|
|
485
|
+
header (the C5 has no such field) and no ``materialMappings`` header (the
|
|
486
|
+
C5 maps materials at print-start, not upload). Booleans are sent as the
|
|
487
|
+
string "true"/"false".
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
params: Creator 5 upload parameters.
|
|
463
491
|
|
|
464
492
|
Returns:
|
|
465
|
-
True
|
|
493
|
+
True on success, False otherwise.
|
|
466
494
|
"""
|
|
467
|
-
|
|
468
|
-
|
|
495
|
+
file_path_obj = Path(params.file_path)
|
|
496
|
+
|
|
497
|
+
if not file_path_obj.exists():
|
|
498
|
+
print(f"upload_file_creator5 error: File not found at {params.file_path}")
|
|
469
499
|
return False
|
|
470
|
-
return True
|
|
471
500
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
501
|
+
file_size = file_path_obj.stat().st_size
|
|
502
|
+
file_name = file_path_obj.name
|
|
503
|
+
|
|
504
|
+
print(
|
|
505
|
+
f"Starting Creator 5 upload for {file_name}, Size: {file_size}, "
|
|
506
|
+
f"Start: {params.start_print}, Level: {params.leveling_before_print}, "
|
|
507
|
+
f"MatlStation: {params.use_matl_station}, Tools: {params.gcode_tool_cnt}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
# C5 upload headers. No firstLayerInspection (absent on the C5);
|
|
512
|
+
# booleans sent as "true"/"false" (firmware checks for "true"); no
|
|
513
|
+
# materialMappings header (C5 maps at print-start, not upload).
|
|
514
|
+
custom_headers = {
|
|
515
|
+
"serialNumber": self.client.serial_number,
|
|
516
|
+
"checkCode": self.client.check_code,
|
|
517
|
+
"fileSize": str(file_size),
|
|
518
|
+
"printNow": str(params.start_print).lower(),
|
|
519
|
+
"levelingBeforePrint": str(params.leveling_before_print).lower(),
|
|
520
|
+
"flowCalibration": str(params.flow_calibration).lower(),
|
|
521
|
+
"timeLapseVideo": str(params.time_lapse_video).lower(),
|
|
522
|
+
"useMatlStation": str(params.use_matl_station).lower(),
|
|
523
|
+
"gcodeToolCnt": str(params.gcode_tool_cnt),
|
|
524
|
+
"Expect": "100-continue",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
print("Creator 5 Upload Request Headers:", custom_headers)
|
|
528
|
+
|
|
529
|
+
# Create multipart form data
|
|
530
|
+
async with aiohttp.ClientSession() as session:
|
|
531
|
+
with open(params.file_path, "rb") as f:
|
|
532
|
+
data = aiohttp.FormData()
|
|
533
|
+
data.add_field(
|
|
534
|
+
"gcodeFile", f, filename=file_name, content_type="application/octet-stream"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
async with session.post(
|
|
538
|
+
self.client.get_endpoint(Endpoints.UPLOAD_FILE),
|
|
539
|
+
data=data,
|
|
540
|
+
headers=custom_headers,
|
|
541
|
+
) as response:
|
|
542
|
+
print(f"Creator 5 Upload Response Status: {response.status}")
|
|
543
|
+
|
|
544
|
+
if response.status != 200:
|
|
545
|
+
print(
|
|
546
|
+
f"Creator 5 Upload failed: Printer responded with "
|
|
547
|
+
f"status {response.status}"
|
|
548
|
+
)
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
result = await json_from_response(response)
|
|
552
|
+
print("Creator 5 Upload Response Data:", result)
|
|
553
|
+
|
|
554
|
+
if NetworkUtils.is_ok(result):
|
|
555
|
+
print("Creator 5 Upload successful according to printer response.")
|
|
556
|
+
return True
|
|
557
|
+
|
|
558
|
+
print(
|
|
559
|
+
"Creator 5 Upload failed: Printer response code="
|
|
560
|
+
f"{result.get('code')}, message={result.get('message')}"
|
|
561
|
+
)
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
print(f"upload_file_creator5 error: {e}")
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
async def start_creator5_job(self, params: Creator5JobParams) -> bool:
|
|
475
569
|
"""
|
|
476
|
-
|
|
477
|
-
|
|
570
|
+
Starts a local print on a Creator 5 / Creator 5 Pro via ``POST /printGcode``.
|
|
571
|
+
|
|
572
|
+
This is the Creator 5's print-start material-matching command (distinct
|
|
573
|
+
from the AD5X, which maps materials at upload time). The file must already
|
|
574
|
+
be on the printer. Provide ``material_mappings`` for a multi-tool print, or
|
|
575
|
+
omit them for a single-tool print. Sends only the fields the Creator 5
|
|
576
|
+
firmware reads: NO ``useMatlStation`` / ``gcodeToolCnt`` (those live on the
|
|
577
|
+
upload) and NO ``firstLayerInspection`` (doesn't exist on the C5).
|
|
578
|
+
``flowCalibration`` and ``timeLapseVideo`` are always present (default False).
|
|
478
579
|
|
|
479
580
|
Args:
|
|
480
|
-
|
|
581
|
+
params: File name, leveling flag, and optional flags / material mappings.
|
|
481
582
|
|
|
482
583
|
Returns:
|
|
483
|
-
|
|
584
|
+
True if the printer accepts the print command, False if validation
|
|
585
|
+
fails or the printer rejects it.
|
|
586
|
+
|
|
587
|
+
Raises:
|
|
588
|
+
Exception: On a network failure sending the command.
|
|
484
589
|
"""
|
|
485
|
-
|
|
486
|
-
|
|
590
|
+
if not self._validate_material_station_printer():
|
|
591
|
+
return False
|
|
592
|
+
|
|
593
|
+
if not params.file_name or params.file_name.strip() == "":
|
|
594
|
+
print("Creator 5 Job error: fileName cannot be empty")
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
has_mappings = params.material_mappings is not None and len(params.material_mappings) > 0
|
|
598
|
+
if has_mappings and not self._validate_creator5_material_mappings(
|
|
599
|
+
params.material_mappings or []
|
|
600
|
+
):
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
payload: dict[str, Any] = {
|
|
604
|
+
"serialNumber": self.client.serial_number,
|
|
605
|
+
"checkCode": self.client.check_code,
|
|
606
|
+
"fileName": params.file_name,
|
|
607
|
+
"levelingBeforePrint": params.leveling_before_print,
|
|
608
|
+
"flowCalibration": params.flow_calibration,
|
|
609
|
+
"timeLapseVideo": params.time_lapse_video,
|
|
610
|
+
}
|
|
611
|
+
if has_mappings:
|
|
612
|
+
payload["materialMappings"] = [
|
|
487
613
|
{
|
|
488
614
|
"toolId": m.tool_id,
|
|
489
615
|
"slotId": m.slot_id,
|
|
@@ -491,71 +617,187 @@ class JobControl:
|
|
|
491
617
|
"toolMaterialColor": m.tool_material_color,
|
|
492
618
|
"slotMaterialColor": m.slot_material_color,
|
|
493
619
|
}
|
|
494
|
-
for m in material_mappings
|
|
620
|
+
for m in params.material_mappings or []
|
|
495
621
|
]
|
|
496
|
-
|
|
497
|
-
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
session = await self.client.get_http_session()
|
|
625
|
+
async with session.post(
|
|
626
|
+
self.client.get_endpoint(Endpoints.GCODE_PRINT),
|
|
627
|
+
json=payload,
|
|
628
|
+
headers={"Content-Type": "application/json"},
|
|
629
|
+
) as response:
|
|
630
|
+
if response.status != 200:
|
|
631
|
+
return False
|
|
632
|
+
|
|
633
|
+
result = await json_from_response(response)
|
|
634
|
+
return NetworkUtils.is_ok(result)
|
|
635
|
+
|
|
498
636
|
except Exception as error:
|
|
499
|
-
print("
|
|
500
|
-
raise
|
|
637
|
+
print(f"Creator 5 Job error: {error}")
|
|
638
|
+
raise error
|
|
501
639
|
|
|
502
|
-
def
|
|
640
|
+
def _validate_material_station_printer(self) -> bool:
|
|
503
641
|
"""
|
|
504
|
-
Validates
|
|
505
|
-
|
|
642
|
+
Validates that the current printer has a material station and therefore
|
|
643
|
+
supports material-mapping uploads/jobs. Covers AD5X and the Creator 5
|
|
644
|
+
series, which share the same material-mapping print flow (Creator 5 =
|
|
645
|
+
"AD5X + more").
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
True if the printer supports material mappings, False otherwise.
|
|
649
|
+
"""
|
|
650
|
+
if not self.client.is_ad5x and not self.client.is_creator5:
|
|
651
|
+
print(
|
|
652
|
+
"Material-station job error: this method requires an AD5X or "
|
|
653
|
+
"Creator 5 series printer"
|
|
654
|
+
)
|
|
655
|
+
return False
|
|
656
|
+
return True
|
|
657
|
+
|
|
658
|
+
def _validate_creator5_material_mappings(
|
|
659
|
+
self, material_mappings: list[AD5XMaterialMapping]
|
|
660
|
+
) -> bool:
|
|
661
|
+
"""
|
|
662
|
+
Validates Creator 5 material mappings. Delegates to the shared validator
|
|
663
|
+
with ``allow_empty=True`` (the C5 uses an empty array for the
|
|
664
|
+
single-tool print start).
|
|
506
665
|
|
|
507
666
|
Args:
|
|
508
|
-
material_mappings: Array of
|
|
667
|
+
material_mappings: Array of Creator 5 mappings to validate.
|
|
509
668
|
|
|
510
669
|
Returns:
|
|
511
|
-
True if all mappings are valid,
|
|
670
|
+
True if all mappings are valid, False otherwise.
|
|
671
|
+
"""
|
|
672
|
+
return self._validate_mappings(
|
|
673
|
+
material_mappings, allow_empty=True, label="Creator 5 material mappings"
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
def _validate_mappings(
|
|
677
|
+
self,
|
|
678
|
+
material_mappings: list[AD5XMaterialMapping],
|
|
679
|
+
*,
|
|
680
|
+
allow_empty: bool,
|
|
681
|
+
label: str,
|
|
682
|
+
) -> bool:
|
|
683
|
+
"""
|
|
684
|
+
Shared material-mapping validator used by both the AD5X and Creator 5
|
|
685
|
+
flows. Checks toolId 0-3, slotId 1-4, non-empty materialName, and
|
|
686
|
+
``#RRGGBB`` tool/slot colors.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
material_mappings: Array of mappings to validate.
|
|
690
|
+
allow_empty: If True, an empty array is accepted (Creator 5
|
|
691
|
+
single-tool start). If False, an empty array is rejected
|
|
692
|
+
(AD5X multi-color jobs require at least one mapping).
|
|
693
|
+
label: Prefix for the printed validation-error messages.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
True if all mappings are valid, False otherwise.
|
|
512
697
|
"""
|
|
513
698
|
if not material_mappings or len(material_mappings) == 0:
|
|
699
|
+
if allow_empty:
|
|
700
|
+
return True
|
|
514
701
|
print(
|
|
515
|
-
"
|
|
702
|
+
f"{label} error: materialMappings array cannot be empty for multi-color jobs"
|
|
516
703
|
)
|
|
517
704
|
return False
|
|
518
705
|
|
|
519
706
|
if len(material_mappings) > 4:
|
|
520
|
-
print("
|
|
707
|
+
print(f"{label} error: Maximum 4 material mappings allowed")
|
|
521
708
|
return False
|
|
522
709
|
|
|
523
710
|
hex_color_regex = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
524
711
|
|
|
525
712
|
for i, mapping in enumerate(material_mappings):
|
|
526
|
-
# Validate toolId (0-3)
|
|
527
713
|
if mapping.tool_id < 0 or mapping.tool_id > 3:
|
|
528
714
|
print(
|
|
529
|
-
f"
|
|
715
|
+
f"{label} error: toolId must be between 0-3, "
|
|
716
|
+
f"got {mapping.tool_id} at index {i}"
|
|
530
717
|
)
|
|
531
718
|
return False
|
|
532
719
|
|
|
533
|
-
# Validate slotId (1-4)
|
|
534
720
|
if mapping.slot_id < 1 or mapping.slot_id > 4:
|
|
535
721
|
print(
|
|
536
|
-
f"
|
|
722
|
+
f"{label} error: slotId must be between 1-4, "
|
|
723
|
+
f"got {mapping.slot_id} at index {i}"
|
|
537
724
|
)
|
|
538
725
|
return False
|
|
539
726
|
|
|
540
|
-
# Validate materialName is not empty
|
|
541
727
|
if not mapping.material_name or mapping.material_name.strip() == "":
|
|
542
|
-
print(
|
|
543
|
-
f"Material mappings validation error: materialName cannot be empty at index {i}"
|
|
544
|
-
)
|
|
728
|
+
print(f"{label} error: materialName cannot be empty at index {i}")
|
|
545
729
|
return False
|
|
546
730
|
|
|
547
|
-
# Validate toolMaterialColor format
|
|
548
731
|
if not hex_color_regex.match(mapping.tool_material_color):
|
|
549
732
|
print(
|
|
550
|
-
f"
|
|
733
|
+
f"{label} error: toolMaterialColor must be in "
|
|
734
|
+
f"#RRGGBB format, got {mapping.tool_material_color} at index {i}"
|
|
551
735
|
)
|
|
552
736
|
return False
|
|
553
737
|
|
|
554
|
-
# Validate slotMaterialColor format
|
|
555
738
|
if not hex_color_regex.match(mapping.slot_material_color):
|
|
556
739
|
print(
|
|
557
|
-
f"
|
|
740
|
+
f"{label} error: slotMaterialColor must be in "
|
|
741
|
+
f"#RRGGBB format, got {mapping.slot_material_color} at index {i}"
|
|
558
742
|
)
|
|
559
743
|
return False
|
|
560
744
|
|
|
561
745
|
return True
|
|
746
|
+
|
|
747
|
+
def _validate_ad5x_printer(self) -> bool:
|
|
748
|
+
"""
|
|
749
|
+
Validates that the current printer is an AD5X model.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
True if the printer is AD5X, false otherwise
|
|
753
|
+
"""
|
|
754
|
+
if not self.client.is_ad5x:
|
|
755
|
+
print("AD5X Job error: This method can only be used with AD5X printers")
|
|
756
|
+
return False
|
|
757
|
+
return True
|
|
758
|
+
|
|
759
|
+
def _encode_material_mappings_to_base64(
|
|
760
|
+
self, material_mappings: list[AD5XMaterialMapping]
|
|
761
|
+
) -> str:
|
|
762
|
+
"""
|
|
763
|
+
Encodes material mappings array to base64 string for HTTP headers.
|
|
764
|
+
Converts AD5XMaterialMapping array to JSON and then to base64 encoding.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
material_mappings: Array of material mappings to encode
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
Base64-encoded JSON string
|
|
771
|
+
"""
|
|
772
|
+
try:
|
|
773
|
+
json_array = [
|
|
774
|
+
{
|
|
775
|
+
"toolId": m.tool_id,
|
|
776
|
+
"slotId": m.slot_id,
|
|
777
|
+
"materialName": m.material_name,
|
|
778
|
+
"toolMaterialColor": m.tool_material_color,
|
|
779
|
+
"slotMaterialColor": m.slot_material_color,
|
|
780
|
+
}
|
|
781
|
+
for m in material_mappings
|
|
782
|
+
]
|
|
783
|
+
json_string = json.dumps(json_array)
|
|
784
|
+
return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
|
|
785
|
+
except Exception as error:
|
|
786
|
+
print("Failed to encode material mappings to base64:", error)
|
|
787
|
+
raise Exception("Failed to encode material mappings for upload") from error # noqa: B904
|
|
788
|
+
|
|
789
|
+
def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
|
|
790
|
+
"""
|
|
791
|
+
Validates material mappings for AD5X multi-color jobs. Delegates to the
|
|
792
|
+
shared validator with ``allow_empty=False`` (AD5X multi-color jobs
|
|
793
|
+
require at least one mapping).
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
material_mappings: Array of material mappings to validate
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
True if all mappings are valid, false otherwise
|
|
800
|
+
"""
|
|
801
|
+
return self._validate_mappings(
|
|
802
|
+
material_mappings, allow_empty=False, label="Material mappings validation"
|
|
803
|
+
)
|