flashforge-python-api 1.2.2__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.
- 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 +109 -2
- 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 +36 -0
- flashforge/models/responses.py +59 -0
- {flashforge_python_api-1.2.2.dist-info → flashforge_python_api-1.3.0.dist-info}/METADATA +1 -1
- {flashforge_python_api-1.2.2.dist-info → flashforge_python_api-1.3.0.dist-info}/RECORD +20 -19
- {flashforge_python_api-1.2.2.dist-info → flashforge_python_api-1.3.0.dist-info}/WHEEL +1 -1
- {flashforge_python_api-1.2.2.dist-info → flashforge_python_api-1.3.0.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.2.2.dist-info → flashforge_python_api-1.3.0.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.
|
|
130
|
+
__version__ = "1.3.0"
|
|
131
131
|
__author__ = "FlashForge Python API Contributors"
|
|
132
132
|
__email__ = "notghosttypes@gmail.com"
|
|
133
133
|
__description__ = "Python library for controlling FlashForge 3D printers"
|
|
@@ -9,3 +9,7 @@ class Commands:
|
|
|
9
9
|
CIRCULATION_CONTROL_CMD = "circulateCtl_cmd"
|
|
10
10
|
CAMERA_CONTROL_CMD = "streamCtrl_cmd"
|
|
11
11
|
TEMP_CONTROL_CMD = "temperatureCtl_cmd"
|
|
12
|
+
# AD5X + Creator 5 material-station slot metadata (name + color). Filament
|
|
13
|
+
# load/unload (`ms_cmd`) stays AD5X-only — the Creator 5 firmware has no
|
|
14
|
+
# `ms_cmd` (confirmed via Ghidra RE).
|
|
15
|
+
MATERIAL_STATION_CONFIG_CMD = "msConfig_cmd"
|
|
@@ -8,6 +8,7 @@ from ...models.responses import FilamentArgs
|
|
|
8
8
|
from ..constants.commands import Commands
|
|
9
9
|
from ..constants.endpoints import Endpoints
|
|
10
10
|
from ..network.utils import NetworkUtils, json_from_response
|
|
11
|
+
from .creator5_palette import snap_to_creator5_palette
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from ...client import FlashForgeClient
|
|
@@ -43,8 +44,11 @@ class Control:
|
|
|
43
44
|
Homes the X, Y, and Z axes of the printer.
|
|
44
45
|
|
|
45
46
|
Returns:
|
|
46
|
-
True if the command is successful, False otherwise
|
|
47
|
+
True if the command is successful, False otherwise (including when the
|
|
48
|
+
printer has no TCP control channel, e.g. Creator 5).
|
|
47
49
|
"""
|
|
50
|
+
if not self.client.can_use_tcp("home_axes"):
|
|
51
|
+
return False
|
|
48
52
|
return await self.tcp_client.home_axes()
|
|
49
53
|
|
|
50
54
|
async def home_axes_rapid(self) -> bool:
|
|
@@ -52,8 +56,11 @@ class Control:
|
|
|
52
56
|
Performs a rapid homing of the X, Y, and Z axes.
|
|
53
57
|
|
|
54
58
|
Returns:
|
|
55
|
-
True if the command is successful, False otherwise
|
|
59
|
+
True if the command is successful, False otherwise (including when the
|
|
60
|
+
printer has no TCP control channel, e.g. Creator 5).
|
|
56
61
|
"""
|
|
62
|
+
if not self.client.can_use_tcp("home_axes_rapid"):
|
|
63
|
+
return False
|
|
57
64
|
return await self.tcp_client.rapid_home()
|
|
58
65
|
|
|
59
66
|
async def set_external_filtration_on(self) -> bool:
|
|
@@ -202,8 +209,11 @@ class Control:
|
|
|
202
209
|
Turns on the filament runout sensor.
|
|
203
210
|
|
|
204
211
|
Returns:
|
|
205
|
-
True if the command is successful, False otherwise
|
|
212
|
+
True if the command is successful, False otherwise (including when the
|
|
213
|
+
printer has no TCP control channel, e.g. Creator 5).
|
|
206
214
|
"""
|
|
215
|
+
if not self.client.can_use_tcp("turn_runout_sensor_on"):
|
|
216
|
+
return False
|
|
207
217
|
return await self.tcp_client.turn_runout_sensor_on()
|
|
208
218
|
|
|
209
219
|
async def turn_runout_sensor_off(self) -> bool:
|
|
@@ -211,10 +221,61 @@ class Control:
|
|
|
211
221
|
Turns off the filament runout sensor.
|
|
212
222
|
|
|
213
223
|
Returns:
|
|
214
|
-
True if the command is successful, False otherwise
|
|
224
|
+
True if the command is successful, False otherwise (including when the
|
|
225
|
+
printer has no TCP control channel, e.g. Creator 5).
|
|
215
226
|
"""
|
|
227
|
+
if not self.client.can_use_tcp("turn_runout_sensor_off"):
|
|
228
|
+
return False
|
|
216
229
|
return await self.tcp_client.turn_runout_sensor_off()
|
|
217
230
|
|
|
231
|
+
async def configure_slot(self, slot: int, material_name: str, hex_rgb: str) -> bool:
|
|
232
|
+
"""
|
|
233
|
+
Configures the material name and color metadata for a material-station slot.
|
|
234
|
+
This information is shown on the printer UI and used for print validation; it
|
|
235
|
+
does not move any filament. Available on the AD5X and the Creator 5 / Creator 5 Pro.
|
|
236
|
+
|
|
237
|
+
The ``msConfig_cmd`` handler is firmware-confirmed present on the Creator 5
|
|
238
|
+
(Ghidra RE of ``firmwareExe`` 1.9.2). The Creator 5 has no removable IFS; it
|
|
239
|
+
surfaces its 4 tool heads as the 4 "slots", so this sets per-tool material
|
|
240
|
+
metadata. Both models share the same ``OrcaServer`` command path and wire
|
|
241
|
+
format. Note that **filament load/unload (``slotAction`` / ``ms_cmd``)
|
|
242
|
+
remains AD5X-only** — the Creator 5 firmware has no ``ms_cmd``.
|
|
243
|
+
|
|
244
|
+
The firmware accepts arbitrary material strings, but the two models render
|
|
245
|
+
the slot color icon differently:
|
|
246
|
+
|
|
247
|
+
- AD5X: accepts freeform hex (the leading "#" is stripped before sending).
|
|
248
|
+
- Creator 5 / 5 Pro: renders an icon ONLY when ``rgb`` is a byte-for-byte,
|
|
249
|
+
case-sensitive match against the firmware's 24-entry palette (WITH the
|
|
250
|
+
"#"); any other value falls back to White. This method snaps the
|
|
251
|
+
caller's color to the nearest palette entry automatically for the Creator 5.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
slot: The slot number (1-4).
|
|
255
|
+
material_name: The material type (e.g., "PLA", "PETG").
|
|
256
|
+
hex_rgb: The color as a hex string.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if the command is successful, False otherwise.
|
|
260
|
+
"""
|
|
261
|
+
if not self.client.is_ad5x and not self.client.is_creator5:
|
|
262
|
+
print("configure_slot() error, material station only available on AD5X / Creator 5.")
|
|
263
|
+
return False
|
|
264
|
+
# The AD5X and Creator 5 use MUTUALLY EXCLUSIVE color wire formats (see
|
|
265
|
+
# creator5_palette for the firmware match rules), so model-gate here:
|
|
266
|
+
# - AD5X: freeform hex, the leading "#" stripped ("RRGGBB"). Unchanged.
|
|
267
|
+
# - Creator 5 / 5 Pro: the firmware renders an icon ONLY on a byte-for-byte
|
|
268
|
+
# match against its 24-entry palette (case-sensitive, WITH the "#"). Snap
|
|
269
|
+
# the caller's color to the nearest palette entry in uppercase "#RRGGBB".
|
|
270
|
+
if self.client.is_creator5:
|
|
271
|
+
rgb = snap_to_creator5_palette(hex_rgb).hex
|
|
272
|
+
else:
|
|
273
|
+
rgb = hex_rgb[1:] if hex_rgb.startswith("#") else hex_rgb
|
|
274
|
+
return await self.send_control_command(
|
|
275
|
+
Commands.MATERIAL_STATION_CONFIG_CMD,
|
|
276
|
+
{"slot": slot, "mt": material_name, "rgb": rgb},
|
|
277
|
+
)
|
|
278
|
+
|
|
218
279
|
async def send_control_command(self, command: str, args: dict[str, Any]) -> bool:
|
|
219
280
|
"""
|
|
220
281
|
Sends a generic control command to the printer via HTTP POST.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Creator 5 / Creator 5 Pro material-station slot color
|
|
3
|
+
palette and perceptual nearest-color snapping.
|
|
4
|
+
|
|
5
|
+
The Creator 5 ``msConfig_cmd`` only renders a color icon when the ``rgb`` field is
|
|
6
|
+
an EXACT, case-sensitive, byte-for-byte match against one of the firmware's 24
|
|
7
|
+
built-in palette strings (compared via ``std::operator==`` in ``firmwareExe``
|
|
8
|
+
1.9.2). A non-match leaves the slot's color index at 0 (White). These values
|
|
9
|
+
DIFFER from the AD5X palette (e.g. Blue is ``#4CAAF8`` here vs ``#45A8F9`` on the
|
|
10
|
+
AD5X), so callers must snap against THIS list specifically.
|
|
11
|
+
|
|
12
|
+
By contrast the AD5X accepts freeform hex (with the ``#`` stripped), so the two
|
|
13
|
+
wire formats are mutually exclusive — see :func:`flashforge.api.controls.control.Control.configure_slot`
|
|
14
|
+
for the model-gating that splits them.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
# The color-space math in this module mirrors the published CIEDE2000 / CIE L*a*b*
|
|
24
|
+
# notation (L, a, b, C', h', etc.) verbatim so it can be checked against the
|
|
25
|
+
# reference. PEP 8 lowercase locals are therefore relaxed here on purpose.
|
|
26
|
+
# ruff: noqa: N806
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Creator5PaletteColor:
|
|
31
|
+
"""A single entry in the Creator 5 firmware color palette."""
|
|
32
|
+
|
|
33
|
+
index: int
|
|
34
|
+
"""Firmware palette index (0 = White = the no-match fallback)."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
"""Color name as shown on the printer UI."""
|
|
38
|
+
|
|
39
|
+
hex: str # noqa: N815 - field mirrors the TS `hex` property name
|
|
40
|
+
"""Wire value sent to the printer, always uppercase ``#RRGGBB``."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: The firmware's 24-entry UI palette (firmwareExe 1.9.2, Ghidra-confirmed).
|
|
44
|
+
#: Index 0 (White) is also what the firmware falls back to on a no-match.
|
|
45
|
+
CREATOR5_PALETTE: tuple[Creator5PaletteColor, ...] = (
|
|
46
|
+
Creator5PaletteColor(0, "White", "#FFFFFF"),
|
|
47
|
+
Creator5PaletteColor(1, "Yellow", "#FFF245"),
|
|
48
|
+
Creator5PaletteColor(2, "Light Green", "#DEF578"),
|
|
49
|
+
Creator5PaletteColor(3, "Green", "#21CC3D"),
|
|
50
|
+
Creator5PaletteColor(4, "Dark Green", "#167A4B"),
|
|
51
|
+
Creator5PaletteColor(5, "Teal", "#156682"),
|
|
52
|
+
Creator5PaletteColor(6, "Cyan", "#24E4A0"),
|
|
53
|
+
Creator5PaletteColor(7, "Light Blue", "#7BD9F0"),
|
|
54
|
+
Creator5PaletteColor(8, "Blue", "#4CAAF8"),
|
|
55
|
+
Creator5PaletteColor(9, "Dark Blue", "#2E54DD"),
|
|
56
|
+
Creator5PaletteColor(10, "Purple", "#48358C"),
|
|
57
|
+
Creator5PaletteColor(11, "Violet", "#A341F7"),
|
|
58
|
+
Creator5PaletteColor(12, "Magenta", "#F435F6"),
|
|
59
|
+
Creator5PaletteColor(13, "Pink", "#D5B4DE"),
|
|
60
|
+
Creator5PaletteColor(14, "Coral", "#FA6173"),
|
|
61
|
+
Creator5PaletteColor(15, "Red", "#F82D29"),
|
|
62
|
+
Creator5PaletteColor(16, "Brown", "#805003"),
|
|
63
|
+
Creator5PaletteColor(17, "Orange", "#F9903B"),
|
|
64
|
+
Creator5PaletteColor(18, "Cream", "#FCEBD7"),
|
|
65
|
+
Creator5PaletteColor(19, "Tan", "#D5C5A1"),
|
|
66
|
+
Creator5PaletteColor(20, "Dark Brown", "#B17C38"),
|
|
67
|
+
Creator5PaletteColor(21, "Gray", "#8C8C89"),
|
|
68
|
+
Creator5PaletteColor(22, "Light Gray", "#BEBEBE"),
|
|
69
|
+
Creator5PaletteColor(23, "Black", "#1B1B1B"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 25^7, the CIEDE2000 chroma weighting constant.
|
|
73
|
+
_25_POW_7 = 25.0 ** 7
|
|
74
|
+
|
|
75
|
+
# D65 reference white point used by the sRGB -> XYZ transform.
|
|
76
|
+
_D65_XN = 0.95047
|
|
77
|
+
_D65_YN = 1.0
|
|
78
|
+
_D65_ZN = 1.08883
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _srgb_to_linear(channel: float) -> float:
|
|
82
|
+
"""sRGB component (0-255) channel transfer function -> linear value (0-1)."""
|
|
83
|
+
c = channel / 255.0
|
|
84
|
+
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _rgb_to_lab(r: float, g: float, b: float) -> tuple[float, float, float]:
|
|
88
|
+
"""
|
|
89
|
+
Converts an sRGB color (0-255 channels) to CIE L*a*b* under a D65 illuminant.
|
|
90
|
+
Used as the perceptual basis for the CIEDE2000 nearest-color match.
|
|
91
|
+
"""
|
|
92
|
+
R = _srgb_to_linear(r)
|
|
93
|
+
G = _srgb_to_linear(g)
|
|
94
|
+
B = _srgb_to_linear(b)
|
|
95
|
+
|
|
96
|
+
x = R * 0.4124564 + G * 0.3575761 + B * 0.1804375
|
|
97
|
+
y = R * 0.2126729 + G * 0.7151522 + B * 0.072175
|
|
98
|
+
z = R * 0.0193339 + G * 0.119192 + B * 0.9503041
|
|
99
|
+
|
|
100
|
+
x /= _D65_XN
|
|
101
|
+
y /= _D65_YN
|
|
102
|
+
z /= _D65_ZN
|
|
103
|
+
|
|
104
|
+
def f(t: float) -> float:
|
|
105
|
+
return math.cbrt(t) if t > 0.008856 else 7.787 * t + 16.0 / 116.0
|
|
106
|
+
|
|
107
|
+
fx = f(x)
|
|
108
|
+
fy = f(y)
|
|
109
|
+
fz = f(z)
|
|
110
|
+
|
|
111
|
+
return (116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _atan2_deg(ordinate: float, abscissa: float) -> float:
|
|
115
|
+
"""atan2 -> hue in degrees, normalized to [0, 360)."""
|
|
116
|
+
h = math.degrees(math.atan2(ordinate, abscissa))
|
|
117
|
+
if h < 0:
|
|
118
|
+
h += 360.0
|
|
119
|
+
return h
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _delta_e_2000(c1: tuple[float, float, float], c2: tuple[float, float, float]) -> float:
|
|
123
|
+
"""
|
|
124
|
+
CIEDE2000 color difference between two L*a*b* colors (kL=kC=kH=1). This is the
|
|
125
|
+
most accurate standard delta-E metric and is preferred here because the
|
|
126
|
+
firmware renders only an exact palette match — snapping to the wrong
|
|
127
|
+
perceptual neighbor would display the wrong color on the printer.
|
|
128
|
+
"""
|
|
129
|
+
L1, a1, b1 = c1
|
|
130
|
+
L2, a2, b2 = c2
|
|
131
|
+
|
|
132
|
+
C1 = math.sqrt(a1 * a1 + b1 * b1)
|
|
133
|
+
C2 = math.sqrt(a2 * a2 + b2 * b2)
|
|
134
|
+
Cbar = (C1 + C2) / 2.0
|
|
135
|
+
Cbar7 = Cbar ** 7
|
|
136
|
+
G = 0.5 * (1 - math.sqrt(Cbar7 / (Cbar7 + _25_POW_7)))
|
|
137
|
+
|
|
138
|
+
a1p = (1 + G) * a1
|
|
139
|
+
a2p = (1 + G) * a2
|
|
140
|
+
C1p = math.sqrt(a1p * a1p + b1 * b1)
|
|
141
|
+
C2p = math.sqrt(a2p * a2p + b2 * b2)
|
|
142
|
+
h1p = _atan2_deg(b1, a1p)
|
|
143
|
+
h2p = _atan2_deg(b2, a2p)
|
|
144
|
+
|
|
145
|
+
dLp = L2 - L1
|
|
146
|
+
dCp = C2p - C1p
|
|
147
|
+
if C1p * C2p == 0:
|
|
148
|
+
dhp = 0.0
|
|
149
|
+
else:
|
|
150
|
+
diff = h2p - h1p
|
|
151
|
+
if abs(diff) <= 180:
|
|
152
|
+
dhp = diff
|
|
153
|
+
elif diff > 180:
|
|
154
|
+
dhp = diff - 360
|
|
155
|
+
else:
|
|
156
|
+
dhp = diff + 360
|
|
157
|
+
dHp = 2 * math.sqrt(C1p * C2p) * math.sin(math.radians(dhp) / 2.0)
|
|
158
|
+
|
|
159
|
+
Lbarp = (L1 + L2) / 2.0
|
|
160
|
+
Cbarp = (C1p + C2p) / 2.0
|
|
161
|
+
if C1p * C2p == 0:
|
|
162
|
+
hbarp = h1p + h2p
|
|
163
|
+
else:
|
|
164
|
+
diff = abs(h1p - h2p)
|
|
165
|
+
if diff <= 180:
|
|
166
|
+
hbarp = (h1p + h2p) / 2.0
|
|
167
|
+
elif h1p + h2p < 360:
|
|
168
|
+
hbarp = (h1p + h2p + 360) / 2.0
|
|
169
|
+
else:
|
|
170
|
+
hbarp = (h1p + h2p - 360) / 2.0
|
|
171
|
+
|
|
172
|
+
T = (
|
|
173
|
+
1
|
|
174
|
+
- 0.17 * math.cos(math.radians(hbarp - 30))
|
|
175
|
+
+ 0.24 * math.cos(math.radians(2 * hbarp))
|
|
176
|
+
+ 0.32 * math.cos(math.radians(3 * hbarp + 6))
|
|
177
|
+
- 0.20 * math.cos(math.radians(4 * hbarp - 63))
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
dTheta = 30 * math.exp(-(((hbarp - 275) / 25.0) ** 2))
|
|
181
|
+
Cbarp7 = Cbarp ** 7
|
|
182
|
+
RC = 2 * math.sqrt(Cbarp7 / (Cbarp7 + _25_POW_7))
|
|
183
|
+
SL = 1 + (0.015 * (Lbarp - 50) ** 2) / math.sqrt(20 + (Lbarp - 50) ** 2)
|
|
184
|
+
SC = 1 + 0.045 * Cbarp
|
|
185
|
+
SH = 1 + 0.015 * Cbarp * T
|
|
186
|
+
RT = -math.sin(math.radians(2 * dTheta)) * RC
|
|
187
|
+
|
|
188
|
+
termL = dLp / SL
|
|
189
|
+
termC = dCp / SC
|
|
190
|
+
termH = dHp / SH
|
|
191
|
+
|
|
192
|
+
return math.sqrt(termL * termL + termC * termC + termH * termH + RT * termC * termH)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_HEX_RE = re.compile(r"^[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _hex_to_rgb(hex_str: str) -> tuple[int, int, int] | None:
|
|
199
|
+
"""
|
|
200
|
+
Parses a hex color string (``#RRGGBB``, ``RRGGBB``, 3-digit shorthand, any
|
|
201
|
+
case) into its RGB channels. Returns None for unparseable input.
|
|
202
|
+
"""
|
|
203
|
+
clean = hex_str.strip()
|
|
204
|
+
# Remove a single leading "#" (matches the TS replace(/^#/, '')).
|
|
205
|
+
if clean.startswith("#"):
|
|
206
|
+
clean = clean[1:]
|
|
207
|
+
if not _HEX_RE.match(clean):
|
|
208
|
+
return None
|
|
209
|
+
if len(clean) == 3:
|
|
210
|
+
clean = "".join(ch + ch for ch in clean)
|
|
211
|
+
return (int(clean[0:2], 16), int(clean[2:4], 16), int(clean[4:6], 16))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# Palette entries with their L*a*b* values precomputed once at module load.
|
|
215
|
+
_PALETTE_LAB: list[tuple[Creator5PaletteColor, tuple[float, float, float]]] = []
|
|
216
|
+
for _color in CREATOR5_PALETTE:
|
|
217
|
+
_rgb = _hex_to_rgb(_color.hex)
|
|
218
|
+
if _rgb is None:
|
|
219
|
+
_rgb = (0, 0, 0)
|
|
220
|
+
_PALETTE_LAB.append((_color, _rgb_to_lab(_rgb[0], _rgb[1], _rgb[2])))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def snap_to_creator5_palette(hex_str: str) -> Creator5PaletteColor:
|
|
224
|
+
"""
|
|
225
|
+
Snaps an arbitrary hex color to the nearest entry in the Creator 5 firmware
|
|
226
|
+
palette using the CIEDE2000 perceptual distance in CIE L*a*b* space.
|
|
227
|
+
|
|
228
|
+
The returned :attr:`Creator5PaletteColor.hex` is always uppercase ``#RRGGBB``
|
|
229
|
+
and is guaranteed to be a byte-for-byte firmware match. Unparseable input
|
|
230
|
+
falls back to White (index 0, the firmware's own no-match fallback) with a
|
|
231
|
+
warning.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
hex_str: The caller's color as a hex string (leading ``#`` optional, any case).
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
The nearest Creator 5 palette entry.
|
|
238
|
+
"""
|
|
239
|
+
rgb = _hex_to_rgb(hex_str)
|
|
240
|
+
if rgb is None:
|
|
241
|
+
print(
|
|
242
|
+
f'snap_to_creator5_palette: could not parse "{hex_str}" as hex; '
|
|
243
|
+
"falling back to White."
|
|
244
|
+
)
|
|
245
|
+
return CREATOR5_PALETTE[0]
|
|
246
|
+
|
|
247
|
+
target = _rgb_to_lab(rgb[0], rgb[1], rgb[2])
|
|
248
|
+
best = _PALETTE_LAB[0]
|
|
249
|
+
best_delta = float("inf")
|
|
250
|
+
for entry in _PALETTE_LAB:
|
|
251
|
+
delta = _delta_e_2000(target, entry[1])
|
|
252
|
+
if delta < best_delta:
|
|
253
|
+
best_delta = delta
|
|
254
|
+
best = entry
|
|
255
|
+
return best[0]
|
flashforge/api/controls/files.py
CHANGED
|
@@ -35,10 +35,18 @@ class Files:
|
|
|
35
35
|
"""
|
|
36
36
|
Retrieves a list of files stored locally on the printer.
|
|
37
37
|
|
|
38
|
+
HTTP-only printers (Creator 5 / 5 Pro) have no TCP/8899 file-listing
|
|
39
|
+
channel, so this falls back to the HTTP ``/gcodeList`` recent-file list
|
|
40
|
+
(mirroring ``Files.ts``) instead of hanging on a dead TCP socket.
|
|
41
|
+
|
|
38
42
|
Returns:
|
|
39
43
|
A list of file names, or empty list if retrieval fails.
|
|
40
44
|
"""
|
|
41
|
-
|
|
45
|
+
if self.client.http_only:
|
|
46
|
+
entries = await self.get_recent_file_list()
|
|
47
|
+
return [e.gcode_file_name for e in entries if e.gcode_file_name]
|
|
48
|
+
|
|
49
|
+
# Legacy / 5M-family printers expose the file list over TCP.
|
|
42
50
|
if hasattr(self.client, "tcp_client") and self.client.tcp_client:
|
|
43
51
|
return await self.client.tcp_client.get_file_list_async()
|
|
44
52
|
return []
|
|
@@ -47,6 +55,9 @@ class Files:
|
|
|
47
55
|
"""
|
|
48
56
|
Retrieves a list of files stored locally on the printer.
|
|
49
57
|
|
|
58
|
+
HTTP-only printers (Creator 5 / 5 Pro) have no TCP/8899 file-listing
|
|
59
|
+
channel; see :meth:`get_file_list` for the HTTP fallback.
|
|
60
|
+
|
|
50
61
|
Returns:
|
|
51
62
|
A list of file names, or empty list if retrieval fails.
|
|
52
63
|
"""
|
flashforge/api/controls/info.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
FlashForge Python API - Info Module
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from datetime import datetime, timedelta
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
@@ -14,6 +15,32 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from ...client import FlashForgeClient
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
# Firmware-reported PIDs from FlashForge's /detail endpoint. These are stable
|
|
19
|
+
# identifiers set by firmware, unlike the user-mutable `name` field.
|
|
20
|
+
PID_5M = 35
|
|
21
|
+
PID_5M_PRO = 36
|
|
22
|
+
PID_AD5X = 38
|
|
23
|
+
PID_CREATOR5 = 40
|
|
24
|
+
PID_CREATOR5_PRO = 41
|
|
25
|
+
KNOWN_HTTP_PIDS = {
|
|
26
|
+
PID_5M,
|
|
27
|
+
PID_5M_PRO,
|
|
28
|
+
PID_AD5X,
|
|
29
|
+
PID_CREATOR5,
|
|
30
|
+
PID_CREATOR5_PRO,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Immutable model display names keyed by firmware PID. Used as a fallback for
|
|
34
|
+
# `model` when the printer doesn't report the `model` field (older firmware).
|
|
35
|
+
PID_MODEL_NAMES: dict[int, str] = {
|
|
36
|
+
PID_5M: "Adventurer 5M",
|
|
37
|
+
PID_5M_PRO: "Adventurer 5M Pro",
|
|
38
|
+
PID_AD5X: "AD5X",
|
|
39
|
+
PID_CREATOR5: "Creator 5",
|
|
40
|
+
PID_CREATOR5_PRO: "Creator 5 Pro",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
17
44
|
class MachineInfoParser:
|
|
18
45
|
"""
|
|
19
46
|
Transforms printer detail data from the API response format into a structured FFMachineInfo object.
|
|
@@ -68,8 +95,74 @@ class MachineInfoParser:
|
|
|
68
95
|
or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or []) > 0
|
|
69
96
|
)
|
|
70
97
|
printer_name = getattr(detail, "name", "") or ""
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
pid = getattr(detail, "pid", None)
|
|
99
|
+
detail_model = getattr(detail, "model", None) or ""
|
|
100
|
+
|
|
101
|
+
if pid in KNOWN_HTTP_PIDS:
|
|
102
|
+
is_ad5x = pid == PID_AD5X
|
|
103
|
+
is_pro = pid == PID_5M_PRO
|
|
104
|
+
is_creator5 = pid in (PID_CREATOR5, PID_CREATOR5_PRO)
|
|
105
|
+
is_creator5_pro = pid == PID_CREATOR5_PRO
|
|
106
|
+
else:
|
|
107
|
+
# Fallback for firmware that doesn't report pid: legacy
|
|
108
|
+
# name+capability heuristic. Vulnerable to user renames, which
|
|
109
|
+
# is why pid-based detection is preferred when available.
|
|
110
|
+
#
|
|
111
|
+
# IMPORTANT: detect the Creator 5 family *first*. A user-set
|
|
112
|
+
# name like "Creator 5 Pro" contains "Pro", so a naive
|
|
113
|
+
# `"Pro" in name` check would mis-classify it as a 5M Pro.
|
|
114
|
+
is_creator5 = "Creator 5" in printer_name
|
|
115
|
+
is_creator5_pro = is_creator5 and bool(
|
|
116
|
+
re.search(r"Creator 5 Pro", detail_model or printer_name, re.IGNORECASE)
|
|
117
|
+
)
|
|
118
|
+
is_ad5x = (
|
|
119
|
+
printer_name.upper() == "AD5X" or has_material_station
|
|
120
|
+
) and not is_creator5
|
|
121
|
+
is_pro = "Pro" in printer_name and not is_ad5x and not is_creator5
|
|
122
|
+
|
|
123
|
+
# Per-tool temperatures. Creator 5 series report nozzleTemps[] /
|
|
124
|
+
# nozzleTargetTemps[]; single-nozzle models don't, so fall back to a
|
|
125
|
+
# 1-element array mirroring the right/main extruder.
|
|
126
|
+
nozzle_temps = getattr(detail, "nozzle_temps", None)
|
|
127
|
+
nozzle_target_temps = getattr(detail, "nozzle_target_temps", None)
|
|
128
|
+
if nozzle_temps:
|
|
129
|
+
tool_temps = [
|
|
130
|
+
Temperature(
|
|
131
|
+
current=t or 0.0,
|
|
132
|
+
set=(
|
|
133
|
+
nozzle_target_temps[i]
|
|
134
|
+
if nozzle_target_temps and i < len(nozzle_target_temps)
|
|
135
|
+
else 0.0
|
|
136
|
+
)
|
|
137
|
+
or 0.0,
|
|
138
|
+
)
|
|
139
|
+
for i, t in enumerate(nozzle_temps)
|
|
140
|
+
]
|
|
141
|
+
else:
|
|
142
|
+
tool_temps = [
|
|
143
|
+
Temperature(
|
|
144
|
+
current=getattr(detail, "right_temp", 0) or 0.0,
|
|
145
|
+
set=getattr(detail, "right_target_temp", 0) or 0.0,
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# Capability flags. Derived from presence/value, never assumed from
|
|
150
|
+
# the model family alone. Only the Creator 5 Pro has a confirmed
|
|
151
|
+
# door sensor; on every other model `doorStatus` is cosmetic.
|
|
152
|
+
has_camera = getattr(detail, "camera", None) == 1 or bool(
|
|
153
|
+
getattr(detail, "camera_stream_url", "") or ""
|
|
154
|
+
)
|
|
155
|
+
has_lidar = getattr(detail, "lidar", None) == 1
|
|
156
|
+
has_door_sensor = is_creator5_pro
|
|
157
|
+
|
|
158
|
+
# Immutable model name resolution: prefer the firmware `model`
|
|
159
|
+
# field, then a PID-derived name, then the user-set name.
|
|
160
|
+
pid_model_name = PID_MODEL_NAMES.get(pid) if pid is not None else None
|
|
161
|
+
model_value = detail_model or pid_model_name or printer_name or ""
|
|
162
|
+
|
|
163
|
+
# Prefer the firmware-reported nozzle count; fall back to the parsed
|
|
164
|
+
# per-tool array length so single-nozzle models still report 1.
|
|
165
|
+
nozzle_count = getattr(detail, "nozzle_cnt", None) or len(tool_temps)
|
|
73
166
|
|
|
74
167
|
# Build the FFMachineInfo object
|
|
75
168
|
machine_info = FFMachineInfo(
|
|
@@ -107,9 +200,14 @@ class MachineInfoParser:
|
|
|
107
200
|
fill_amount=getattr(detail, "fill_amount", 0) or 0,
|
|
108
201
|
firmware_version=getattr(detail, "firmware_version", "") or "",
|
|
109
202
|
name=printer_name,
|
|
203
|
+
pid=pid,
|
|
110
204
|
is_pro=is_pro,
|
|
111
205
|
is_ad5x=is_ad5x,
|
|
206
|
+
is_creator5=is_creator5,
|
|
207
|
+
is_creator5_pro=is_creator5_pro,
|
|
208
|
+
model=model_value,
|
|
112
209
|
nozzle_size=getattr(detail, "nozzle_model", "") or "",
|
|
210
|
+
nozzle_count=nozzle_count,
|
|
113
211
|
# Temperatures
|
|
114
212
|
print_bed=Temperature(
|
|
115
213
|
current=getattr(detail, "plat_temp", 0) or 0,
|
|
@@ -119,6 +217,15 @@ class MachineInfoParser:
|
|
|
119
217
|
current=getattr(detail, "right_temp", 0) or 0,
|
|
120
218
|
set=getattr(detail, "right_target_temp", 0) or 0,
|
|
121
219
|
),
|
|
220
|
+
chamber=Temperature(
|
|
221
|
+
current=getattr(detail, "chamber_temp", 0) or 0,
|
|
222
|
+
set=getattr(detail, "chamber_target_temp", 0) or 0,
|
|
223
|
+
),
|
|
224
|
+
tool_temps=tool_temps,
|
|
225
|
+
# Capability flags (presence-derived)
|
|
226
|
+
has_camera=has_camera,
|
|
227
|
+
has_lidar=has_lidar,
|
|
228
|
+
has_door_sensor=has_door_sensor,
|
|
122
229
|
# Current print stats
|
|
123
230
|
print_duration=getattr(detail, "print_duration", 0) or 0,
|
|
124
231
|
print_file_name=getattr(detail, "print_file_name", "") or "",
|