flashforge-python-api 1.3.0__py3-none-any.whl → 1.3.2__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/controls/creator5_palette.py +3 -3
- flashforge/api/controls/files.py +16 -4
- flashforge/api/controls/info.py +13 -4
- flashforge/api/controls/job_control.py +4 -4
- flashforge/api/controls/temp_control.py +1 -3
- flashforge/client.py +18 -1
- flashforge/discovery/discovery.py +10 -6
- flashforge/models/machine_info.py +40 -7
- flashforge/models/responses.py +22 -17
- flashforge/tcp/a3_client.py +27 -8
- flashforge/tcp/a4_client.py +5 -1
- flashforge/tcp/parsers/endstop_status.py +3 -1
- flashforge/tcp/parsers/location_info.py +3 -1
- flashforge/tcp/parsers/print_status.py +3 -1
- flashforge/tcp/parsers/printer_info.py +3 -1
- flashforge/tcp/parsers/temp_info.py +3 -1
- flashforge/tcp/parsers/thumbnail_info.py +1 -1
- flashforge/tcp/tcp_client.py +6 -5
- {flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/METADATA +5 -3
- {flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/RECORD +24 -24
- {flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/WHEEL +1 -1
- {flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/licenses/LICENSE +0 -0
flashforge/__init__.py
CHANGED
|
@@ -127,7 +127,7 @@ from .tcp import (
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
FiveMClient = FlashForgeClient
|
|
130
|
-
__version__ = "1.3.
|
|
130
|
+
__version__ = "1.3.1"
|
|
131
131
|
__author__ = "FlashForge Python API Contributors"
|
|
132
132
|
__email__ = "notghosttypes@gmail.com"
|
|
133
133
|
__description__ = "Python library for controlling FlashForge 3D printers"
|
|
@@ -70,7 +70,7 @@ CREATOR5_PALETTE: tuple[Creator5PaletteColor, ...] = (
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
# 25^7, the CIEDE2000 chroma weighting constant.
|
|
73
|
-
_25_POW_7 = 25.0
|
|
73
|
+
_25_POW_7 = 25.0**7
|
|
74
74
|
|
|
75
75
|
# D65 reference white point used by the sRGB -> XYZ transform.
|
|
76
76
|
_D65_XN = 0.95047
|
|
@@ -132,7 +132,7 @@ def _delta_e_2000(c1: tuple[float, float, float], c2: tuple[float, float, float]
|
|
|
132
132
|
C1 = math.sqrt(a1 * a1 + b1 * b1)
|
|
133
133
|
C2 = math.sqrt(a2 * a2 + b2 * b2)
|
|
134
134
|
Cbar = (C1 + C2) / 2.0
|
|
135
|
-
Cbar7 = Cbar
|
|
135
|
+
Cbar7 = Cbar**7
|
|
136
136
|
G = 0.5 * (1 - math.sqrt(Cbar7 / (Cbar7 + _25_POW_7)))
|
|
137
137
|
|
|
138
138
|
a1p = (1 + G) * a1
|
|
@@ -178,7 +178,7 @@ def _delta_e_2000(c1: tuple[float, float, float], c2: tuple[float, float, float]
|
|
|
178
178
|
)
|
|
179
179
|
|
|
180
180
|
dTheta = 30 * math.exp(-(((hbarp - 275) / 25.0) ** 2))
|
|
181
|
-
Cbarp7 = Cbarp
|
|
181
|
+
Cbarp7 = Cbarp**7
|
|
182
182
|
RC = 2 * math.sqrt(Cbarp7 / (Cbarp7 + _25_POW_7))
|
|
183
183
|
SL = 1 + (0.015 * (Lbarp - 50) ** 2) / math.sqrt(20 + (Lbarp - 50) ** 2)
|
|
184
184
|
SC = 1 + 0.045 * Cbarp
|
flashforge/api/controls/files.py
CHANGED
|
@@ -3,6 +3,7 @@ FlashForge Python API - Files Module
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import logging
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from pydantic import ValidationError
|
|
@@ -15,6 +16,8 @@ from ..network.utils import NetworkUtils, json_from_response
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from ...client import FlashForgeClient
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class Files:
|
|
20
23
|
"""
|
|
@@ -93,7 +96,18 @@ class Files:
|
|
|
93
96
|
# Parse the response using GCodeListResponse
|
|
94
97
|
try:
|
|
95
98
|
result = GCodeListResponse(**data)
|
|
96
|
-
except ValidationError:
|
|
99
|
+
except ValidationError as err:
|
|
100
|
+
# The names-only fallback below silently costs the caller
|
|
101
|
+
# every per-file field, which is indistinguishable from a
|
|
102
|
+
# printer that only reports names. Say so, loudly enough to
|
|
103
|
+
# reach an integration's log, or the next model that changes
|
|
104
|
+
# this payload looks like it reports no metadata at all.
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Could not parse the /gcodeList response; falling back to file "
|
|
107
|
+
"names only, so print time, filament weight, and per-tool "
|
|
108
|
+
"material data are unavailable for every file. %s",
|
|
109
|
+
err,
|
|
110
|
+
)
|
|
97
111
|
raw_list = data.get("gcodeList", [])
|
|
98
112
|
if isinstance(raw_list, list):
|
|
99
113
|
entries: list[FFGcodeFileEntry] = []
|
|
@@ -127,9 +141,7 @@ class Files:
|
|
|
127
141
|
elif isinstance(first_item, FFGcodeFileEntry):
|
|
128
142
|
# Already FFGcodeFileEntry objects - need explicit type narrowing
|
|
129
143
|
return [
|
|
130
|
-
item
|
|
131
|
-
for item in result.gcode_list
|
|
132
|
-
if isinstance(item, FFGcodeFileEntry)
|
|
144
|
+
item for item in result.gcode_list if isinstance(item, FFGcodeFileEntry)
|
|
133
145
|
]
|
|
134
146
|
|
|
135
147
|
return []
|
flashforge/api/controls/info.py
CHANGED
|
@@ -89,10 +89,17 @@ class MachineInfoParser:
|
|
|
89
89
|
print_progress = getattr(detail, "print_progress", 0) or 0
|
|
90
90
|
est_length = total_job_filament_meters * print_progress
|
|
91
91
|
est_weight = (getattr(detail, "estimated_right_weight", 0) or 0) * print_progress
|
|
92
|
+
# Material Station presence, derived rather than read off a single
|
|
93
|
+
# field. `hasMatlStation` is AD5X-only: the Creator 5 series omits it
|
|
94
|
+
# from /detail entirely (verified on a Creator 5 Pro, pid 41, firmware
|
|
95
|
+
# 1.9.4) while reporting a fully populated matlStationInfo with four
|
|
96
|
+
# loaded slots, so an absent flag means "not reported", never "absent
|
|
97
|
+
# hardware". Populated slot data is the reliable signal.
|
|
92
98
|
has_material_station = (
|
|
93
99
|
getattr(detail, "has_matl_station", None) is True
|
|
94
100
|
or (getattr(getattr(detail, "matl_station_info", None), "slot_cnt", 0) or 0) > 0
|
|
95
|
-
or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or [])
|
|
101
|
+
or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or [])
|
|
102
|
+
> 0
|
|
96
103
|
)
|
|
97
104
|
printer_name = getattr(detail, "name", "") or ""
|
|
98
105
|
pid = getattr(detail, "pid", None)
|
|
@@ -236,7 +243,9 @@ class MachineInfoParser:
|
|
|
236
243
|
print_speed_adjust=getattr(detail, "print_speed_adjust", 0) or 0,
|
|
237
244
|
filament_type=getattr(detail, "right_filament_type", "") or "",
|
|
238
245
|
# Machine state
|
|
239
|
-
machine_state=MachineInfoParser._get_machine_state(
|
|
246
|
+
machine_state=MachineInfoParser._get_machine_state(
|
|
247
|
+
getattr(detail, "status", "") or ""
|
|
248
|
+
),
|
|
240
249
|
status=getattr(detail, "status", "") or "",
|
|
241
250
|
total_print_layers=getattr(detail, "target_print_layer", 0) or 0,
|
|
242
251
|
tvoc=getattr(detail, "tvoc", 0) or 0,
|
|
@@ -249,8 +258,8 @@ class MachineInfoParser:
|
|
|
249
258
|
completion_time=completion_time,
|
|
250
259
|
formatted_run_time=formatted_run_time,
|
|
251
260
|
formatted_total_run_time=formatted_total_run_time,
|
|
252
|
-
# AD5X
|
|
253
|
-
has_matl_station=
|
|
261
|
+
# Material Station (AD5X / Creator 5 series)
|
|
262
|
+
has_matl_station=has_material_station,
|
|
254
263
|
matl_station_info=getattr(detail, "matl_station_info", None),
|
|
255
264
|
indep_matl_info=getattr(detail, "indep_matl_info", None),
|
|
256
265
|
)
|
|
@@ -698,9 +698,7 @@ class JobControl:
|
|
|
698
698
|
if not material_mappings or len(material_mappings) == 0:
|
|
699
699
|
if allow_empty:
|
|
700
700
|
return True
|
|
701
|
-
print(
|
|
702
|
-
f"{label} error: materialMappings array cannot be empty for multi-color jobs"
|
|
703
|
-
)
|
|
701
|
+
print(f"{label} error: materialMappings array cannot be empty for multi-color jobs")
|
|
704
702
|
return False
|
|
705
703
|
|
|
706
704
|
if len(material_mappings) > 4:
|
|
@@ -784,7 +782,9 @@ class JobControl:
|
|
|
784
782
|
return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
|
|
785
783
|
except Exception as error:
|
|
786
784
|
print("Failed to encode material mappings to base64:", error)
|
|
787
|
-
raise Exception(
|
|
785
|
+
raise Exception(
|
|
786
|
+
"Failed to encode material mappings for upload"
|
|
787
|
+
) from error # noqa: B904
|
|
788
788
|
|
|
789
789
|
def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
|
|
790
790
|
"""
|
|
@@ -270,9 +270,7 @@ class TempControl:
|
|
|
270
270
|
True if the command is acknowledged, False otherwise.
|
|
271
271
|
"""
|
|
272
272
|
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
273
|
-
print(
|
|
274
|
-
"cancel_chamber_temp() error, chamber heater only available on Creator 5 series."
|
|
275
|
-
)
|
|
273
|
+
print("cancel_chamber_temp() error, chamber heater only available on Creator 5 series.")
|
|
276
274
|
return False
|
|
277
275
|
return await self._send_http_temp_command(chamber=TEMP_OFF)
|
|
278
276
|
|
flashforge/client.py
CHANGED
|
@@ -6,9 +6,11 @@ communication layers for controlling FlashForge 3D printers.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import logging
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
|
|
11
12
|
import aiohttp
|
|
13
|
+
from pydantic import ValidationError
|
|
12
14
|
|
|
13
15
|
from .api.constants.endpoints import CAMERA_STREAM_PORT, Endpoints
|
|
14
16
|
from .api.controls import Control, Files, Info, JobControl, TempControl
|
|
@@ -19,6 +21,8 @@ from .tcp import FlashForgeClient as TcpClient
|
|
|
19
21
|
from .tcp import FlashForgeTcpClientOptions, PrinterInfo
|
|
20
22
|
from .tcp.parsers.temp_info import TempInfo
|
|
21
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
@dataclass(slots=True)
|
|
24
28
|
class FiveMClientConnectionOptions:
|
|
@@ -476,8 +480,21 @@ class FlashForgeClient:
|
|
|
476
480
|
self._apply_feature_overrides()
|
|
477
481
|
return True
|
|
478
482
|
|
|
483
|
+
except ValidationError as error:
|
|
484
|
+
# Distinct from a rejected check code, and it must not read as one:
|
|
485
|
+
# the printer answered 200 with code 0, we simply could not parse
|
|
486
|
+
# what it said. Callers treat a False return as bad credentials
|
|
487
|
+
# (ff-5mp-hass surfaces it as "check code incorrect"), so the log
|
|
488
|
+
# line is the only place the real cause can surface.
|
|
489
|
+
logger.warning(
|
|
490
|
+
"Could not parse the /product response; reporting the printer as "
|
|
491
|
+
"unavailable rather than the credentials as rejected. This usually "
|
|
492
|
+
"means the firmware added a field this library has not seen. %s",
|
|
493
|
+
error,
|
|
494
|
+
)
|
|
495
|
+
return False
|
|
479
496
|
except Exception as error:
|
|
480
|
-
|
|
497
|
+
logger.warning("Error in send_product_command: %s", error)
|
|
481
498
|
return False
|
|
482
499
|
finally:
|
|
483
500
|
self._http_client_busy = False
|
|
@@ -303,7 +303,9 @@ class PrinterDiscovery:
|
|
|
303
303
|
try:
|
|
304
304
|
transport, _ = await self._create_endpoint(message_queue, error_queue)
|
|
305
305
|
self._send_discovery_packets(transport, config)
|
|
306
|
-
discovered_printers = await self._receive_responses(
|
|
306
|
+
discovered_printers = await self._receive_responses(
|
|
307
|
+
message_queue, error_queue, config
|
|
308
|
+
)
|
|
307
309
|
|
|
308
310
|
for printer in discovered_printers:
|
|
309
311
|
key = f"{printer.ip_address}:{printer.command_port}"
|
|
@@ -454,7 +456,9 @@ class PrinterDiscovery:
|
|
|
454
456
|
status_code = int.from_bytes(buffer[0x8A:0x8C], byteorder="big")
|
|
455
457
|
product_type = int.from_bytes(buffer[0x8C:0x8E], byteorder="big")
|
|
456
458
|
event_port = int.from_bytes(buffer[0x8E:0x90], byteorder="big")
|
|
457
|
-
serial_number =
|
|
459
|
+
serial_number = (
|
|
460
|
+
buffer[0x92 : 0x92 + 128].decode("utf-8", errors="ignore").split("\x00", 1)[0]
|
|
461
|
+
)
|
|
458
462
|
model = self.detect_modern_model(name, product_type, product_id)
|
|
459
463
|
|
|
460
464
|
return DiscoveredPrinter(
|
|
@@ -586,9 +590,7 @@ class PrinterDiscovery:
|
|
|
586
590
|
continue
|
|
587
591
|
|
|
588
592
|
mask_int = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
|
|
589
|
-
netmask = ".".join(
|
|
590
|
-
str((mask_int >> (8 * (3 - i))) & 0xFF) for i in range(4)
|
|
591
|
-
)
|
|
593
|
+
netmask = ".".join(str((mask_int >> (8 * (3 - i))) & 0xFF) for i in range(4))
|
|
592
594
|
|
|
593
595
|
broadcast = self.calculate_broadcast_address(ip_addr, netmask)
|
|
594
596
|
if broadcast and broadcast not in broadcast_addresses:
|
|
@@ -695,7 +697,9 @@ class FlashForgePrinterDiscovery:
|
|
|
695
697
|
return None
|
|
696
698
|
|
|
697
699
|
if LEGACY_PROTOCOL_SIZE <= len(response) < MODERN_PROTOCOL_SIZE and len(response) >= 0x92:
|
|
698
|
-
legacy_name =
|
|
700
|
+
legacy_name = (
|
|
701
|
+
response[0:32].decode("utf-8", errors="ignore").split("\x00", 1)[0].strip()
|
|
702
|
+
)
|
|
699
703
|
legacy_serial = (
|
|
700
704
|
response[0x92 : 0x92 + 32]
|
|
701
705
|
.decode("utf-8", errors="ignore")
|
|
@@ -29,6 +29,10 @@ class MachineState(Enum):
|
|
|
29
29
|
class Temperature(BaseModel):
|
|
30
30
|
"""Represents a pair of current and target temperatures for a component like an extruder or print bed."""
|
|
31
31
|
|
|
32
|
+
# `extra="forbid"` deliberately: this model is never parsed from a printer
|
|
33
|
+
# payload, only constructed by MachineInfoParser.from_detail. Forbidding
|
|
34
|
+
# extras there catches a typo'd keyword argument in our own parser, which
|
|
35
|
+
# `extra="allow"` would silently absorb.
|
|
32
36
|
model_config = ConfigDict(extra="forbid")
|
|
33
37
|
|
|
34
38
|
current: float = Field(
|
|
@@ -42,7 +46,12 @@ class Temperature(BaseModel):
|
|
|
42
46
|
class SlotInfo(BaseModel):
|
|
43
47
|
"""Information about a single slot in the material station."""
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
# `extra="allow"`: inbound, nested under FFPrinterDetail.matl_station_info.
|
|
50
|
+
# FFPrinterDetail allowing extras is not enough on its own - a new field on
|
|
51
|
+
# a *child* fails validation for the whole /detail response, and
|
|
52
|
+
# `Info.get_detail_response` swallows that and returns None, which the HA
|
|
53
|
+
# integration reports as "could not retrieve printer information".
|
|
54
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
46
55
|
|
|
47
56
|
has_filament: bool = Field(
|
|
48
57
|
alias="hasFilament", description="Indicates if filament is present in this slot"
|
|
@@ -69,7 +78,8 @@ class SlotInfo(BaseModel):
|
|
|
69
78
|
class MatlStationInfo(BaseModel):
|
|
70
79
|
"""Detailed information about the material station."""
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
# `extra="allow"`: inbound, see SlotInfo.
|
|
82
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
73
83
|
|
|
74
84
|
current_load_slot: int = Field(
|
|
75
85
|
alias="currentLoadSlot", ge=0, le=4, description="Currently loading slot ID (0 if none)"
|
|
@@ -98,7 +108,8 @@ class MatlStationInfo(BaseModel):
|
|
|
98
108
|
class IndepMatlInfo(BaseModel):
|
|
99
109
|
"""Information related to independent material loading, often used when a single extruder printer has a material station."""
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
# `extra="allow"`: inbound, see SlotInfo.
|
|
112
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
102
113
|
|
|
103
114
|
material_color: str = Field(alias="materialColor", description="Color of the material")
|
|
104
115
|
material_name: str = Field(
|
|
@@ -123,7 +134,10 @@ class IndepMatlInfo(BaseModel):
|
|
|
123
134
|
class FFGcodeToolData(BaseModel):
|
|
124
135
|
"""Represents data for a single tool/material used in a G-code file, typically part of a multi-material print."""
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
# `extra="allow"`: inbound, nested under FFGcodeFileEntry - which already
|
|
138
|
+
# allows extras, but a child that forbids them reintroduces the same
|
|
139
|
+
# names-only fallback it was flipped to avoid.
|
|
140
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
127
141
|
|
|
128
142
|
filament_weight: float = Field(
|
|
129
143
|
alias="filamentWeight",
|
|
@@ -158,7 +172,11 @@ class FFGcodeToolData(BaseModel):
|
|
|
158
172
|
class FFGcodeFileEntry(BaseModel):
|
|
159
173
|
"""Represents a single G-code file entry as returned by the /gcodeList endpoint, especially for printers like AD5X that provide detailed material info."""
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
# `extra="allow"`, matching FFPrinterDetail: a firmware update that adds one
|
|
176
|
+
# field must not fail validation, because Files.get_recent_file_list falls
|
|
177
|
+
# back to a names-only list when it does - silently dropping print time,
|
|
178
|
+
# filament weight, and the per-tool material data for EVERY file.
|
|
179
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
162
180
|
|
|
163
181
|
gcode_file_name: str = Field(
|
|
164
182
|
alias="gcodeFileName", description="The name of the G-code file (e.g., 'FISH_PLA.3mf')"
|
|
@@ -231,6 +249,10 @@ class FFPrinterDetail(BaseModel):
|
|
|
231
249
|
fill_amount: float | None = Field(default=None, ge=0, le=100, alias="fillAmount")
|
|
232
250
|
firmware_version: str | None = Field(default=None, alias="firmwareVersion")
|
|
233
251
|
flash_register_code: str | None = Field(default=None, alias="flashRegisterCode")
|
|
252
|
+
# AD5X-only, and firmware omits what does not apply: the Creator 5 series
|
|
253
|
+
# leaves this out of /detail even with four loaded slots, so None means "not
|
|
254
|
+
# reported", NOT "no station". Never gate a feature on it - read the derived
|
|
255
|
+
# FFMachineInfo.has_matl_station, or check matl_station_info yourself.
|
|
234
256
|
has_matl_station: bool | None = Field(default=None, alias="hasMatlStation")
|
|
235
257
|
matl_station_info: MatlStationInfo | None = Field(default=None, alias="matlStationInfo")
|
|
236
258
|
indep_matl_info: IndepMatlInfo | None = Field(default=None, alias="indepMatlInfo")
|
|
@@ -289,6 +311,10 @@ class FFMachineInfo(BaseModel):
|
|
|
289
311
|
This interface is populated by transforming data from FFPrinterDetail.
|
|
290
312
|
"""
|
|
291
313
|
|
|
314
|
+
# `extra="forbid"` deliberately: see Temperature. Built only at
|
|
315
|
+
# `MachineInfoParser.from_detail`, from ~50 keyword arguments, never from
|
|
316
|
+
# raw printer JSON - so forbidding extras is a typo check on our own code,
|
|
317
|
+
# not a firmware-compatibility risk.
|
|
292
318
|
model_config = ConfigDict(extra="forbid")
|
|
293
319
|
|
|
294
320
|
# Auto-shutdown settings
|
|
@@ -395,8 +421,15 @@ class FFMachineInfo(BaseModel):
|
|
|
395
421
|
formatted_run_time: str = "00:00"
|
|
396
422
|
formatted_total_run_time: str = "0h:0m"
|
|
397
423
|
|
|
398
|
-
# AD5X
|
|
399
|
-
|
|
424
|
+
# Material Station (AD5X / Creator 5 series)
|
|
425
|
+
#
|
|
426
|
+
# A capability, not a passthrough: MachineInfoParser DERIVES this from the
|
|
427
|
+
# station data rather than copying the raw `hasMatlStation` field, which the
|
|
428
|
+
# Creator 5 series never reports even with a station attached. It is a plain
|
|
429
|
+
# bool on purpose - there is no "unknown" state to represent, and offering
|
|
430
|
+
# one is what let an unreported flag read as absent hardware. For the
|
|
431
|
+
# untouched firmware value, read FFPrinterDetail.has_matl_station.
|
|
432
|
+
has_matl_station: bool = False
|
|
400
433
|
matl_station_info: MatlStationInfo | None = None
|
|
401
434
|
indep_matl_info: IndepMatlInfo | None = None
|
|
402
435
|
|
flashforge/models/responses.py
CHANGED
|
@@ -10,7 +10,12 @@ from .machine_info import FFGcodeFileEntry, FFPrinterDetail
|
|
|
10
10
|
class GenericResponse(BaseModel):
|
|
11
11
|
"""Represents a generic response from the printer's API."""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# `extra="allow"` on every inbound model: this is the envelope each printer
|
|
14
|
+
# response inherits, so a firmware update that adds one top-level key must
|
|
15
|
+
# not fail validation here. Callers cannot tell a parse failure from a real
|
|
16
|
+
# error - `send_product_command` returns a bare False either way, which the
|
|
17
|
+
# HA integration reports to the user as "check code incorrect".
|
|
18
|
+
model_config = ConfigDict(extra="allow")
|
|
14
19
|
|
|
15
20
|
code: int
|
|
16
21
|
message: str = ""
|
|
@@ -19,8 +24,6 @@ class GenericResponse(BaseModel):
|
|
|
19
24
|
class DetailResponse(GenericResponse):
|
|
20
25
|
"""Represents the structure of the response from the printer's detail endpoint."""
|
|
21
26
|
|
|
22
|
-
model_config = ConfigDict(extra="forbid")
|
|
23
|
-
|
|
24
27
|
detail: FFPrinterDetail
|
|
25
28
|
|
|
26
29
|
|
|
@@ -36,7 +39,10 @@ class Product(BaseModel):
|
|
|
36
39
|
Field names match the actual camelCase format returned by the printer.
|
|
37
40
|
"""
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
# `extra="allow"`: an unrecognized control-state flag must not fail
|
|
43
|
+
# validation. `send_product_command` has no fallback - it returns False,
|
|
44
|
+
# which is indistinguishable from the printer rejecting the credentials.
|
|
45
|
+
model_config = ConfigDict(extra="allow")
|
|
40
46
|
|
|
41
47
|
chamberTempCtrlState: int # noqa: N815 - field must match API camelCase response format
|
|
42
48
|
externalFanCtrlState: int # noqa: N815 - field must match API camelCase response format
|
|
@@ -55,8 +61,6 @@ class ProductResponse(GenericResponse):
|
|
|
55
61
|
and a nested `product` object containing specific control states.
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
|
-
model_config = ConfigDict(extra="forbid")
|
|
59
|
-
|
|
60
64
|
product: Product
|
|
61
65
|
|
|
62
66
|
|
|
@@ -96,6 +100,10 @@ class FilamentArgs(BaseModel):
|
|
|
96
100
|
class AD5XMaterialMapping(BaseModel):
|
|
97
101
|
"""Represents a material mapping for AD5X multi-color printing. Maps a tool (extruder) to a specific material station slot."""
|
|
98
102
|
|
|
103
|
+
# `extra="forbid"` deliberately, as on every outbound param model below:
|
|
104
|
+
# these are request bodies this library constructs, so no firmware update
|
|
105
|
+
# can break them, and forbidding extras turns a caller's typo'd keyword
|
|
106
|
+
# into an error instead of a field the printer silently ignores.
|
|
99
107
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
100
108
|
|
|
101
109
|
tool_id: int = Field(ge=0, le=3, description="Tool ID (0-based: 0, 1, 2, 3)")
|
|
@@ -181,9 +189,7 @@ class Creator5JobParams(BaseModel):
|
|
|
181
189
|
leveling_before_print: bool = Field(
|
|
182
190
|
description="Whether to perform bed leveling before printing"
|
|
183
191
|
)
|
|
184
|
-
flow_calibration: bool = Field(
|
|
185
|
-
default=False, description="Whether to enable flow calibration"
|
|
186
|
-
)
|
|
192
|
+
flow_calibration: bool = Field(default=False, description="Whether to enable flow calibration")
|
|
187
193
|
time_lapse_video: bool = Field(
|
|
188
194
|
default=False, description="Whether to enable time lapse video recording"
|
|
189
195
|
)
|
|
@@ -209,15 +215,11 @@ class Creator5UploadParams(BaseModel):
|
|
|
209
215
|
leveling_before_print: bool = Field(
|
|
210
216
|
description="Whether to perform bed leveling before printing"
|
|
211
217
|
)
|
|
212
|
-
flow_calibration: bool = Field(
|
|
213
|
-
default=False, description="Whether to enable flow calibration"
|
|
214
|
-
)
|
|
218
|
+
flow_calibration: bool = Field(default=False, description="Whether to enable flow calibration")
|
|
215
219
|
time_lapse_video: bool = Field(
|
|
216
220
|
default=False, description="Whether to enable time lapse video recording"
|
|
217
221
|
)
|
|
218
|
-
use_matl_station: bool = Field(
|
|
219
|
-
description="Whether this is a multi-tool material-station job"
|
|
220
|
-
)
|
|
222
|
+
use_matl_station: bool = Field(description="Whether this is a multi-tool material-station job")
|
|
221
223
|
gcode_tool_cnt: int = Field(
|
|
222
224
|
ge=1, le=4, description="Number of tools in the G-code (1-4 for the C5)"
|
|
223
225
|
)
|
|
@@ -226,7 +228,9 @@ class Creator5UploadParams(BaseModel):
|
|
|
226
228
|
class GCodeListResponse(GenericResponse):
|
|
227
229
|
"""Represents the response structure for a G-code file list request."""
|
|
228
230
|
|
|
229
|
-
|
|
231
|
+
# `extra="allow"`: see FFGcodeFileEntry. An unknown field at either level
|
|
232
|
+
# costs the caller all per-file metadata, so neither level may forbid one.
|
|
233
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
230
234
|
|
|
231
235
|
gcode_list: list[str] | list[FFGcodeFileEntry] | None = Field(default=None, alias="gcodeList")
|
|
232
236
|
gcode_list_detail: list[FFGcodeFileEntry] | None = Field(default=None, alias="gcodeListDetail")
|
|
@@ -235,7 +239,8 @@ class GCodeListResponse(GenericResponse):
|
|
|
235
239
|
class ThumbnailResponse(GenericResponse):
|
|
236
240
|
"""Represents the response structure for a G-code thumbnail request."""
|
|
237
241
|
|
|
238
|
-
|
|
242
|
+
# `extra="allow"`: inbound, see GenericResponse.
|
|
243
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
239
244
|
|
|
240
245
|
image_data: str = Field(
|
|
241
246
|
alias="imageData", description="The thumbnail image data encoded as a base64 string"
|
flashforge/tcp/a3_client.py
CHANGED
|
@@ -84,7 +84,11 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
84
84
|
bare_cmd = self._strip_protocol_prefix(cmd)
|
|
85
85
|
if bare_cmd.startswith("M661"):
|
|
86
86
|
return 500
|
|
87
|
-
if
|
|
87
|
+
if (
|
|
88
|
+
bare_cmd.startswith("M115")
|
|
89
|
+
or bare_cmd.startswith("M119")
|
|
90
|
+
or bare_cmd.startswith("M650")
|
|
91
|
+
):
|
|
88
92
|
return 250
|
|
89
93
|
return 200
|
|
90
94
|
|
|
@@ -102,7 +106,7 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
102
106
|
if "Error: File not exists" in text_prefix:
|
|
103
107
|
return True
|
|
104
108
|
|
|
105
|
-
magic_offset = buffer.find(b"\
|
|
109
|
+
magic_offset = buffer.find(b"\xa2\xa2\x2a\x2a")
|
|
106
110
|
if magic_offset == -1 or len(buffer) < magic_offset + 8:
|
|
107
111
|
return False
|
|
108
112
|
|
|
@@ -163,7 +167,9 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
163
167
|
elif line.startswith("Mac Address:"):
|
|
164
168
|
info.mac_address = line.replace("Mac Address:", "", 1).strip()
|
|
165
169
|
else:
|
|
166
|
-
volume_match = re.search(
|
|
170
|
+
volume_match = re.search(
|
|
171
|
+
r"X:\s*(\d+)\s+Y:\s*(\d+)\s+Z:\s*(\d+)", line, re.IGNORECASE
|
|
172
|
+
)
|
|
167
173
|
if volume_match:
|
|
168
174
|
info.build_volume = A3BuildVolume(
|
|
169
175
|
x=int(volume_match.group(1)),
|
|
@@ -331,7 +337,11 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
331
337
|
|
|
332
338
|
files: list[A3FileEntry] = []
|
|
333
339
|
count_index = next(
|
|
334
|
-
(
|
|
340
|
+
(
|
|
341
|
+
index
|
|
342
|
+
for index, line in enumerate(lines)
|
|
343
|
+
if re.search(r"info_list\.size:\s*\d+", line)
|
|
344
|
+
),
|
|
335
345
|
-1,
|
|
336
346
|
)
|
|
337
347
|
if count_index == -1:
|
|
@@ -354,11 +364,13 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
354
364
|
if "Error: File not exists" in error_text:
|
|
355
365
|
return None
|
|
356
366
|
|
|
357
|
-
magic_offset = buffer.find(b"\
|
|
367
|
+
magic_offset = buffer.find(b"\xa2\xa2\x2a\x2a")
|
|
358
368
|
if magic_offset == -1 or len(buffer) < magic_offset + 8:
|
|
359
369
|
return None
|
|
360
370
|
|
|
361
|
-
payload_length = int.from_bytes(
|
|
371
|
+
payload_length = int.from_bytes(
|
|
372
|
+
buffer[magic_offset + 4 : magic_offset + 8], byteorder="big"
|
|
373
|
+
)
|
|
362
374
|
payload_end = magic_offset + 8 + payload_length
|
|
363
375
|
if len(buffer) < payload_end:
|
|
364
376
|
return None
|
|
@@ -378,7 +390,11 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
378
390
|
return normalized.rstrip()
|
|
379
391
|
|
|
380
392
|
def _get_normalized_lines(self, response: str) -> list[str]:
|
|
381
|
-
return [
|
|
393
|
+
return [
|
|
394
|
+
line.strip()
|
|
395
|
+
for line in self._normalize_a3_text_response(response).split("\n")
|
|
396
|
+
if line.strip()
|
|
397
|
+
]
|
|
382
398
|
|
|
383
399
|
def _map_controller_command(self, cmd: str) -> str:
|
|
384
400
|
if cmd == "~M146 r255 g255 b255 F0":
|
|
@@ -399,7 +415,10 @@ class FlashForgeA3Client(FlashForgeTcpClient):
|
|
|
399
415
|
if bare_cmd.startswith("M23"):
|
|
400
416
|
return "File opened" in normalized or "ok" in normalized
|
|
401
417
|
if bare_cmd.startswith("M112"):
|
|
402
|
-
return
|
|
418
|
+
return (
|
|
419
|
+
bool(re.search(r"Emergency Stop", normalized, re.IGNORECASE))
|
|
420
|
+
or "Received." in normalized
|
|
421
|
+
)
|
|
403
422
|
if "ok" in normalized or "Received." in normalized:
|
|
404
423
|
return True
|
|
405
424
|
return normalized.startswith(bare_cmd)
|
flashforge/tcp/a4_client.py
CHANGED
|
@@ -274,7 +274,11 @@ class FlashForgeA4Client(FlashForgeTcpClient):
|
|
|
274
274
|
return normalized
|
|
275
275
|
|
|
276
276
|
def _get_normalized_lines(self, response: str) -> list[str]:
|
|
277
|
-
return [
|
|
277
|
+
return [
|
|
278
|
+
line.strip()
|
|
279
|
+
for line in self._normalize_a4_text_response(response).split("\n")
|
|
280
|
+
if line.strip()
|
|
281
|
+
]
|
|
278
282
|
|
|
279
283
|
def _detect_variant(self, machine_type: str) -> A4PrinterVariant:
|
|
280
284
|
normalized_type = machine_type.upper()
|
|
@@ -118,7 +118,9 @@ class EndstopStatus:
|
|
|
118
118
|
return None
|
|
119
119
|
|
|
120
120
|
try:
|
|
121
|
-
lines = [
|
|
121
|
+
lines = [
|
|
122
|
+
line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()
|
|
123
|
+
]
|
|
122
124
|
if not lines:
|
|
123
125
|
return None
|
|
124
126
|
|
|
@@ -47,7 +47,9 @@ class LocationInfo:
|
|
|
47
47
|
The populated LocationInfo instance, or None if parsing fails
|
|
48
48
|
"""
|
|
49
49
|
try:
|
|
50
|
-
lines = [
|
|
50
|
+
lines = [
|
|
51
|
+
line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()
|
|
52
|
+
]
|
|
51
53
|
coordinate_line = next(
|
|
52
54
|
(line for line in lines if "X:" in line and "Y:" in line and "Z:" in line),
|
|
53
55
|
"",
|
|
@@ -48,7 +48,9 @@ class PrintStatus:
|
|
|
48
48
|
self.sd_total = ""
|
|
49
49
|
self.layer_current = ""
|
|
50
50
|
self.layer_total = ""
|
|
51
|
-
lines = [
|
|
51
|
+
lines = [
|
|
52
|
+
line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()
|
|
53
|
+
]
|
|
52
54
|
|
|
53
55
|
for line in lines:
|
|
54
56
|
if "SD printing byte " in line:
|
|
@@ -70,7 +70,9 @@ class PrinterInfo:
|
|
|
70
70
|
return None
|
|
71
71
|
|
|
72
72
|
try:
|
|
73
|
-
lines = [
|
|
73
|
+
lines = [
|
|
74
|
+
line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()
|
|
75
|
+
]
|
|
74
76
|
|
|
75
77
|
for line in lines:
|
|
76
78
|
if line.startswith("Machine Type:"):
|
|
@@ -127,7 +127,9 @@ class TempInfo:
|
|
|
127
127
|
return None
|
|
128
128
|
|
|
129
129
|
try:
|
|
130
|
-
lines = [
|
|
130
|
+
lines = [
|
|
131
|
+
line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()
|
|
132
|
+
]
|
|
131
133
|
if not lines:
|
|
132
134
|
logger.error("TempInfo replay has invalid data: %s", replay)
|
|
133
135
|
return None
|
|
@@ -50,7 +50,7 @@ class ThumbnailInfo:
|
|
|
50
50
|
return None
|
|
51
51
|
|
|
52
52
|
binary_buffer = replay.encode("latin1")
|
|
53
|
-
magic_marker = b"\
|
|
53
|
+
magic_marker = b"\xa2\xa2\x2a\x2a"
|
|
54
54
|
magic_offset = binary_buffer.find(magic_marker)
|
|
55
55
|
if magic_offset >= 0 and len(binary_buffer) >= magic_offset + 8:
|
|
56
56
|
payload_length = int.from_bytes(
|
flashforge/tcp/tcp_client.py
CHANGED
|
@@ -150,10 +150,9 @@ class FlashForgeTcpClient:
|
|
|
150
150
|
logger.error("Upload failed: remote file name resolved to an empty value.")
|
|
151
151
|
return False
|
|
152
152
|
|
|
153
|
-
start_command = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
153
|
+
start_command = GCodes.CMD_PREP_FILE_UPLOAD.replace(
|
|
154
|
+
"%%size%%", str(file_path.stat().st_size)
|
|
155
|
+
).replace("%%filename%%", normalized_file_name)
|
|
157
156
|
|
|
158
157
|
async with self._socket_lock:
|
|
159
158
|
try:
|
|
@@ -184,7 +183,9 @@ class FlashForgeTcpClient:
|
|
|
184
183
|
GCodes.CMD_COMPLETE_FILE_UPLOAD
|
|
185
184
|
)
|
|
186
185
|
if not finish_response:
|
|
187
|
-
logger.error(
|
|
186
|
+
logger.error(
|
|
187
|
+
"Upload failed: printer did not respond to M29 upload finalization."
|
|
188
|
+
)
|
|
188
189
|
return False
|
|
189
190
|
|
|
190
191
|
return self._is_successful_upload_boundary_response(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashforge-python-api
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
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
|
|
@@ -57,6 +57,8 @@ Python library for controlling FlashForge 3D printers with async support for the
|
|
|
57
57
|
| Adventurer 5M | Full |
|
|
58
58
|
| Adventurer 5M Pro | Full |
|
|
59
59
|
| AD5X | Full |
|
|
60
|
+
| Creator 5 | Full (HTTP-only) |
|
|
61
|
+
| Creator 5 Pro | Full (HTTP-only) |
|
|
60
62
|
| Adventurer 3 / 4 | Dedicated TCP clients |
|
|
61
63
|
|
|
62
64
|
## Installation
|
|
@@ -138,9 +140,9 @@ asyncio.run(main())
|
|
|
138
140
|
- printer status and machine information
|
|
139
141
|
- job control
|
|
140
142
|
- file listing, uploads, and thumbnails
|
|
141
|
-
- temperature and motion control
|
|
143
|
+
- temperature and motion control, including per-nozzle and chamber temperature control (Creator 5)
|
|
142
144
|
- LED, camera, and filtration control where supported
|
|
143
|
-
- AD5X
|
|
145
|
+
- AD5X and Creator 5 / Creator 5 Pro material-station, slot configuration, and material mapping
|
|
144
146
|
|
|
145
147
|
## Documentation
|
|
146
148
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
flashforge/__init__.py,sha256=
|
|
2
|
-
flashforge/client.py,sha256=
|
|
1
|
+
flashforge/__init__.py,sha256=0PGa08cJ2RjAlIyJqX4MW3w1I-FUIg8ftaJ6I-A5cc0,5336
|
|
2
|
+
flashforge/client.py,sha256=CwgRq58JZNuFSNdp3x49kUs6tyxD7KHJGifjPH4_oEw,21321
|
|
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
8
|
flashforge/api/controls/control.py,sha256=N1DGpuyvuPWzyMd4_CvbyaQQwoacPIRE8yBaFyy0d9g,14715
|
|
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=
|
|
9
|
+
flashforge/api/controls/creator5_palette.py,sha256=41weYaGnnPp5uqKzPixh4whCCo8ZyH9ZKYarzozfMUI,8774
|
|
10
|
+
flashforge/api/controls/files.py,sha256=VhzZaCZ2_dTxHH6AHyhTutklERPSF3ymq3z_qcKpNIw,7848
|
|
11
|
+
flashforge/api/controls/info.py,sha256=WfLeO_BZbqkD2kPXM9cTro7ZOCmn7sxOnd0g6QnA-aw,17559
|
|
12
|
+
flashforge/api/controls/job_control.py,sha256=pO0D0YD_DCfsAwv196XCfrw-xarGZcYkrdxq11wAZEM,31955
|
|
13
|
+
flashforge/api/controls/temp_control.py,sha256=eFMvuBJDWia_zQ5k8v2gKR0AsLtGohZao2sZBSZKgOg,12835
|
|
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
|
|
@@ -20,28 +20,28 @@ flashforge/api/network/__init__.py,sha256=G5fBoAlq-NQPDkWp579mFIqsj0v7B1Lb2TAzO6
|
|
|
20
20
|
flashforge/api/network/fnet_code.py,sha256=BR2niVKKk4ZfXtizUDIMXzDhlCxPHzqWg5AQQIcoj3g,402
|
|
21
21
|
flashforge/api/network/utils.py,sha256=Q5-Vj_1VN611QV_TGpDzrrZ2X8PqaGT0j00BhSHv5MM,2096
|
|
22
22
|
flashforge/discovery/__init__.py,sha256=G0WiP70EhfHdjXR1RNlv6xjhyHOdo-HWyOm9J0ds4SM,828
|
|
23
|
-
flashforge/discovery/discovery.py,sha256
|
|
23
|
+
flashforge/discovery/discovery.py,sha256=-hOmVIpn8gu-rPYo8FRBfhKb_tBqwamLfmoPVYaDzLI,27614
|
|
24
24
|
flashforge/models/__init__.py,sha256=hdcK0E4KeDfv_A8Y2aPOHgHziwTjP1RPb_VP-89dOzQ,1086
|
|
25
|
-
flashforge/models/machine_info.py,sha256=
|
|
26
|
-
flashforge/models/responses.py,sha256=
|
|
25
|
+
flashforge/models/machine_info.py,sha256=UD-BQ-VzceSIoFFrpuFPJLpaisdgxBRxVD8SZ20mF4E,19384
|
|
26
|
+
flashforge/models/responses.py,sha256=10-8yFfZtLuDmH1TK2xVYl-Uf-zT2WEJnpb2H5ZK3ZU,10637
|
|
27
27
|
flashforge/tcp/__init__.py,sha256=hpnqoWHeRtTwJPzdsVlwGt1njKbAkrgIHKADkLSIRec,1317
|
|
28
|
-
flashforge/tcp/a3_client.py,sha256=
|
|
29
|
-
flashforge/tcp/a4_client.py,sha256=
|
|
28
|
+
flashforge/tcp/a3_client.py,sha256=Aqb6MuKkjnNUVdE2LbZKd7bPlFK8XPIm17C_BG_PuNU,15867
|
|
29
|
+
flashforge/tcp/a4_client.py,sha256=snmAvTCpSWFjxb8h6V7H4tk5yOoMqKEzKoBY_1JaaPs,11213
|
|
30
30
|
flashforge/tcp/ff_client.py,sha256=CJXk0Qei2FVSpbXxZhiEQgAEcTJaBab1ouooqyC4OL4,20235
|
|
31
|
-
flashforge/tcp/tcp_client.py,sha256=
|
|
31
|
+
flashforge/tcp/tcp_client.py,sha256=Ae5Z5rLJFoDYxP3T6Q_PtVNkUr94QqFEWPKtgFyMIJw,17027
|
|
32
32
|
flashforge/tcp/gcode/__init__.py,sha256=tFGlqCPFxVJNizXsIG4pMOK7IPFNnc_vIEMciULYkMY,271
|
|
33
33
|
flashforge/tcp/gcode/a3_gcode_controller.py,sha256=x84_xo_Ll5k6hMAwDWuNUo54znV-BBIfwnmeOiEa9Fs,3688
|
|
34
34
|
flashforge/tcp/gcode/gcode_controller.py,sha256=6K1VGgX59AcE_Kh_gm13dmxCY_kuzV_d2ooSpheONRI,10075
|
|
35
35
|
flashforge/tcp/gcode/gcodes.py,sha256=55Ii1JpJNJNmOnBivyF83vz3TZUPDtp01lX72NINfPQ,3658
|
|
36
36
|
flashforge/tcp/parsers/__init__.py,sha256=kDaJmiH0JcR5i693Fj3pwmINLWN0ipirN3LWGFvPst0,730
|
|
37
|
-
flashforge/tcp/parsers/endstop_status.py,sha256=
|
|
38
|
-
flashforge/tcp/parsers/location_info.py,sha256=
|
|
39
|
-
flashforge/tcp/parsers/print_status.py,sha256=
|
|
40
|
-
flashforge/tcp/parsers/printer_info.py,sha256=
|
|
41
|
-
flashforge/tcp/parsers/temp_info.py,sha256=
|
|
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.
|
|
37
|
+
flashforge/tcp/parsers/endstop_status.py,sha256=4OWwE5cyBFhmTPmp18hU4lRr6YSA7sm7U13pHxI1ZJ0,9845
|
|
38
|
+
flashforge/tcp/parsers/location_info.py,sha256=0fbqCmQ2EQQe8z9fiX0zP1YsnuVayTs81zmF3_HuGZM,2895
|
|
39
|
+
flashforge/tcp/parsers/print_status.py,sha256=GdmRvkQKMq-JdefsateMYdlTrlEB16wm29DNUQAbiXA,6194
|
|
40
|
+
flashforge/tcp/parsers/printer_info.py,sha256=CHPs6nJfEByXyG3co7Nb8_ygggxkYuc7A9O9bpQEqXE,5441
|
|
41
|
+
flashforge/tcp/parsers/temp_info.py,sha256=9wRGUM9cKvDiZD3SIxBx_qVLZbeXoYsoY3-R3vN3H4g,7938
|
|
42
|
+
flashforge/tcp/parsers/thumbnail_info.py,sha256=11MXltBg3HphSOATVt4mDcyETkHlnL6ngkXJ7KevviY,10201
|
|
43
|
+
flashforge_python_api-1.3.2.dist-info/METADATA,sha256=K_4JvhnuupxESwEv9qzBpv-kHk7v7erICdSewlijVYE,4970
|
|
44
|
+
flashforge_python_api-1.3.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
45
|
+
flashforge_python_api-1.3.2.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
|
|
46
|
+
flashforge_python_api-1.3.2.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
|
|
47
|
+
flashforge_python_api-1.3.2.dist-info/RECORD,,
|
{flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{flashforge_python_api-1.3.0.dist-info → flashforge_python_api-1.3.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|