flashforge-python-api 1.2.3__py3-none-any.whl → 1.3.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.
@@ -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
- def _validate_ad5x_printer(self) -> bool:
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
- Validates that the current printer is an AD5X model.
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 if the printer is AD5X, false otherwise
493
+ True on success, False otherwise.
466
494
  """
467
- if not self.client.is_ad5x:
468
- print("AD5X Job error: This method can only be used with AD5X printers")
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
- def _encode_material_mappings_to_base64(
473
- self, material_mappings: list[AD5XMaterialMapping]
474
- ) -> str:
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
- Encodes material mappings array to base64 string for HTTP headers.
477
- Converts AD5XMaterialMapping array to JSON and then to base64 encoding.
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
- material_mappings: Array of material mappings to encode
581
+ params: File name, leveling flag, and optional flags / material mappings.
481
582
 
482
583
  Returns:
483
- Base64-encoded JSON string
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
- try:
486
- json_array = [
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
- json_string = json.dumps(json_array)
497
- return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
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("Failed to encode material mappings to base64:", error)
500
- raise Exception("Failed to encode material mappings for upload") from error # noqa: B904
637
+ print(f"Creator 5 Job error: {error}")
638
+ raise error
501
639
 
502
- def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
640
+ def _validate_material_station_printer(self) -> bool:
503
641
  """
504
- Validates material mappings for AD5X multi-color jobs.
505
- Checks toolId range (0-3), slotId range (1-4), and color format (#RRGGBB).
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 material mappings to validate
667
+ material_mappings: Array of Creator 5 mappings to validate.
509
668
 
510
669
  Returns:
511
- True if all mappings are valid, false otherwise
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
- "Material mappings validation error: materialMappings array cannot be empty for multi-color jobs"
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("Material mappings validation error: Maximum 4 material mappings allowed")
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"Material mappings validation error: toolId must be between 0-3, got {mapping.tool_id} at index {i}"
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"Material mappings validation error: slotId must be between 1-4, got {mapping.slot_id} at index {i}"
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"Material mappings validation error: toolMaterialColor must be in #RRGGBB format, got {mapping.tool_material_color} at index {i}"
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"Material mappings validation error: slotMaterialColor must be in #RRGGBB format, got {mapping.slot_material_color} at index {i}"
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
+ )