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
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
"""
|
|
2
2
|
FlashForge Python API - Temperature Control Module
|
|
3
|
+
|
|
4
|
+
Sets and cancels extruder/bed/chamber temperatures. The 5M / 5M Pro / AD5X use
|
|
5
|
+
direct TCP G-code/M-code commands; HTTP-only printers (Creator 5 / 5 Pro, no TCP
|
|
6
|
+
channel) use the HTTP ``temperatureCtl_cmd`` instead.
|
|
3
7
|
"""
|
|
4
8
|
|
|
5
9
|
from typing import TYPE_CHECKING
|
|
6
10
|
|
|
11
|
+
from ..constants.commands import Commands
|
|
12
|
+
|
|
7
13
|
if TYPE_CHECKING:
|
|
8
14
|
from ...client import FlashForgeClient
|
|
9
15
|
from ...tcp.ff_client import FlashForgeClient as TcpClient
|
|
10
16
|
|
|
11
17
|
|
|
18
|
+
# Sentinel value for `temperatureCtl_cmd` meaning "leave this heater unchanged"
|
|
19
|
+
# (partial update). Sending a real 0 / -100 would turn the heater off.
|
|
20
|
+
TEMP_NO_CHANGE = -200
|
|
21
|
+
# `temperatureCtl_cmd` value that turns a SCALAR heater (platform / chamber /
|
|
22
|
+
# rightNozzle) off.
|
|
23
|
+
TEMP_OFF = -100
|
|
24
|
+
# Value that turns a tool/nozzle OFF inside the `nozzles` array. Unlike the scalar
|
|
25
|
+
# heater fields (which accept TEMP_OFF = -100), the Creator 5 firmware's
|
|
26
|
+
# per-nozzle parser only treats a literal 0 as "off" — it ignores -100 in the
|
|
27
|
+
# `nozzles` array and the tool keeps heating. (Firmware-confirmed via tester
|
|
28
|
+
# report; this is the v1.6.1 nozzle-off bugfix.)
|
|
29
|
+
NOZZLE_OFF = 0
|
|
30
|
+
# Number of tool/nozzle entries the Creator 5 firmware requires in the
|
|
31
|
+
# `nozzles` array. The firmware ignores the array unless its length is exactly
|
|
32
|
+
# this (confirmed via Ghidra: `size() == 4` check in the temp parser).
|
|
33
|
+
NOZZLE_COUNT = 4
|
|
34
|
+
|
|
35
|
+
|
|
12
36
|
class TempControl:
|
|
13
37
|
"""
|
|
14
38
|
Provides methods for controlling the temperatures of various printer components,
|
|
15
39
|
including extruders and the print bed.
|
|
40
|
+
|
|
41
|
+
Dual-API printers (5M / 5M Pro / AD5X) use TCP G-code commands; HTTP-only
|
|
42
|
+
printers (Creator 5 / 5 Pro) route through the HTTP ``temperatureCtl_cmd``.
|
|
16
43
|
"""
|
|
17
44
|
|
|
18
45
|
def __init__(self, client: "FlashForgeClient"):
|
|
@@ -32,61 +59,244 @@ class TempControl:
|
|
|
32
59
|
self._tcp_client = self.client.tcp_client
|
|
33
60
|
return self._tcp_client
|
|
34
61
|
|
|
62
|
+
async def _send_http_temp_command(
|
|
63
|
+
self,
|
|
64
|
+
right_nozzle: int | None = None,
|
|
65
|
+
left_nozzle: int | None = None,
|
|
66
|
+
platform: int | None = None,
|
|
67
|
+
chamber: int | None = None,
|
|
68
|
+
nozzles: list[int] | None = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Sends a ``temperatureCtl_cmd`` over HTTP with the given setpoints.
|
|
72
|
+
Unspecified heaters are left unchanged via the :data:`TEMP_NO_CHANGE`
|
|
73
|
+
sentinel.
|
|
74
|
+
|
|
75
|
+
The ``nozzles`` array is Creator 5-only (its 4-nozzle tool changer). The
|
|
76
|
+
confirmed C5 payload ALWAYS carries a 4-entry array, even on a bed- or
|
|
77
|
+
chamber-only command, so always include it on the C5 — defaulting
|
|
78
|
+
unspecified tools to :data:`TEMP_NO_CHANGE` so a bed/chamber adjustment
|
|
79
|
+
leaves the tools untouched.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if the command is acknowledged, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
payload: dict[str, int | list[int]] = {
|
|
85
|
+
"rightNozzle": right_nozzle if right_nozzle is not None else TEMP_NO_CHANGE,
|
|
86
|
+
"leftNozzle": left_nozzle if left_nozzle is not None else TEMP_NO_CHANGE,
|
|
87
|
+
"platform": platform if platform is not None else TEMP_NO_CHANGE,
|
|
88
|
+
"chamber": chamber if chamber is not None else TEMP_NO_CHANGE,
|
|
89
|
+
}
|
|
90
|
+
# The `nozzles` array is Creator 5-only. The confirmed C5 payload always
|
|
91
|
+
# carries a 4-entry array, even on a bed- or chamber-only command, so
|
|
92
|
+
# always include it on the C5 — defaulting unspecified tools to
|
|
93
|
+
# TEMP_NO_CHANGE so a bed/chamber adjustment leaves the tools untouched.
|
|
94
|
+
if nozzles is not None:
|
|
95
|
+
payload["nozzles"] = nozzles
|
|
96
|
+
elif self.client.is_creator5:
|
|
97
|
+
payload["nozzles"] = [TEMP_NO_CHANGE] * NOZZLE_COUNT
|
|
98
|
+
return await self.client.control.send_control_command(Commands.TEMP_CONTROL_CMD, payload)
|
|
99
|
+
|
|
100
|
+
def _build_nozzle_array(self, tool_index: int, value: int) -> list[int] | None:
|
|
101
|
+
"""
|
|
102
|
+
Builds a :data:`NOZZLE_COUNT`-length ``nozzles`` array of
|
|
103
|
+
:data:`TEMP_NO_CHANGE` placeholders with a single tool set to ``value``.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The array, or None if ``tool_index`` is out of range.
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(tool_index, int) or tool_index < 0 or tool_index >= NOZZLE_COUNT:
|
|
109
|
+
print(f"TempControl: toolIndex {tool_index} out of range (0-{NOZZLE_COUNT - 1}).")
|
|
110
|
+
return None
|
|
111
|
+
nozzles = [TEMP_NO_CHANGE] * NOZZLE_COUNT
|
|
112
|
+
nozzles[tool_index] = value
|
|
113
|
+
return nozzles
|
|
114
|
+
|
|
115
|
+
async def set_tool_temp(self, tool_index: int, temp: int) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Sets the target temperature for a single tool/nozzle on a Creator 5 series
|
|
118
|
+
tool-changer, leaving the other tools unchanged. Sent as a ``nozzles``
|
|
119
|
+
array (Creator 5 / 5 Pro, HTTP-only).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tool_index: Zero-based tool index (0-3 for T0-T3).
|
|
123
|
+
temp: Target temperature in Celsius.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if the command is acknowledged, False otherwise.
|
|
127
|
+
"""
|
|
128
|
+
nozzles = self._build_nozzle_array(tool_index, temp)
|
|
129
|
+
if nozzles is None:
|
|
130
|
+
return False
|
|
131
|
+
return await self._send_http_temp_command(nozzles=nozzles)
|
|
132
|
+
|
|
133
|
+
async def set_tool_temps(self, temps: list[int]) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Sets the target temperatures for all tools/nozzles on a Creator 5 series
|
|
136
|
+
tool-changer in one command. Use :data:`TEMP_NO_CHANGE` (-200) to leave a
|
|
137
|
+
tool unchanged or :data:`NOZZLE_OFF` (0) to turn one off (the firmware
|
|
138
|
+
ignores the -100 sentinel inside the ``nozzles`` array). Must contain
|
|
139
|
+
exactly :data:`NOZZLE_COUNT` entries.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
temps: Per-tool target temperatures, ordered T0..T3.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if the command is acknowledged, False otherwise.
|
|
146
|
+
"""
|
|
147
|
+
if len(temps) != NOZZLE_COUNT:
|
|
148
|
+
print(f"set_tool_temps: expected {NOZZLE_COUNT} temps, got {len(temps)}.")
|
|
149
|
+
return False
|
|
150
|
+
return await self._send_http_temp_command(nozzles=list(temps))
|
|
151
|
+
|
|
152
|
+
async def cancel_tool_temp(self, tool_index: int) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Cancels heating for a single tool/nozzle on a Creator 5 series tool-changer
|
|
155
|
+
(sets its target to 0 via :data:`NOZZLE_OFF`), leaving the other tools
|
|
156
|
+
unchanged.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
tool_index: Zero-based tool index (0-3 for T0-T3).
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if the command is acknowledged, False otherwise.
|
|
163
|
+
"""
|
|
164
|
+
nozzles = self._build_nozzle_array(tool_index, NOZZLE_OFF)
|
|
165
|
+
if nozzles is None:
|
|
166
|
+
return False
|
|
167
|
+
return await self._send_http_temp_command(nozzles=nozzles)
|
|
168
|
+
|
|
35
169
|
async def set_extruder_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
36
170
|
"""
|
|
37
|
-
Sets the target temperature for
|
|
171
|
+
Sets the target temperature for the printer's extruder.
|
|
172
|
+
|
|
173
|
+
On HTTP-only printers this routes to ``temperatureCtl_cmd``: the Creator 5
|
|
174
|
+
drives its tools only via the ``nozzles`` array (targeting T0), while a
|
|
175
|
+
forced-http_only AD5X / 5M keeps the ``rightNozzle`` scalar.
|
|
38
176
|
|
|
39
177
|
Args:
|
|
40
178
|
temperature: The target temperature in Celsius.
|
|
41
|
-
wait_for: Whether to wait for the heating operation to complete
|
|
179
|
+
wait_for: Whether to wait for the heating operation to complete (TCP only).
|
|
42
180
|
|
|
43
181
|
Returns:
|
|
44
182
|
True if the command is successful, False otherwise.
|
|
45
183
|
"""
|
|
184
|
+
if self.client.http_only:
|
|
185
|
+
# Creator 5 tools are driven ONLY via the `nozzles` array — the
|
|
186
|
+
# firmware's doTemperatureControl handler never reads
|
|
187
|
+
# rightNozzle/leftNozzle. Target the primary tool (T0): the active
|
|
188
|
+
# tool isn't reliably known over HTTP, so callers that need a
|
|
189
|
+
# specific tool should use set_tool_temp(index, temp).
|
|
190
|
+
if self.client.is_creator5:
|
|
191
|
+
nozzles = self._build_nozzle_array(0, temperature)
|
|
192
|
+
if nozzles is None:
|
|
193
|
+
return False
|
|
194
|
+
return await self._send_http_temp_command(nozzles=nozzles)
|
|
195
|
+
# Single-tool AD5X / 5M only reachable here if forced into http_only
|
|
196
|
+
# mode; they keep the rightNozzle field (unchanged).
|
|
197
|
+
return await self._send_http_temp_command(right_nozzle=temperature)
|
|
46
198
|
return await self.tcp_client.set_extruder_temp(temperature, wait_for)
|
|
47
199
|
|
|
48
200
|
async def set_bed_temp(self, temperature: int, wait_for: bool = False) -> bool:
|
|
49
201
|
"""
|
|
50
|
-
Sets the target temperature for the print bed.
|
|
202
|
+
Sets the target temperature for the printer's print bed.
|
|
51
203
|
|
|
52
204
|
Args:
|
|
53
205
|
temperature: The target bed temperature in Celsius.
|
|
54
|
-
wait_for: Whether to wait for the heating operation to complete
|
|
206
|
+
wait_for: Whether to wait for the heating operation to complete (TCP only).
|
|
55
207
|
|
|
56
208
|
Returns:
|
|
57
209
|
True if the command is successful, False otherwise.
|
|
58
210
|
"""
|
|
211
|
+
if self.client.http_only:
|
|
212
|
+
return await self._send_http_temp_command(platform=temperature)
|
|
59
213
|
return await self.tcp_client.set_bed_temp(temperature, wait_for)
|
|
60
214
|
|
|
61
215
|
async def cancel_extruder_temp(self) -> bool:
|
|
62
216
|
"""
|
|
63
|
-
Cancels
|
|
217
|
+
Cancels any ongoing extruder heating and sets its target temperature to 0.
|
|
64
218
|
|
|
65
219
|
Returns:
|
|
66
220
|
True if the command is successful, False otherwise.
|
|
67
221
|
"""
|
|
222
|
+
if self.client.http_only:
|
|
223
|
+
# See set_extruder_temp: Creator 5 tools are driven only via
|
|
224
|
+
# `nozzles`. Turn the primary tool (T0) off (target 0, not the -100
|
|
225
|
+
# sentinel the nozzles array ignores) while leaving the other tools
|
|
226
|
+
# unchanged.
|
|
227
|
+
if self.client.is_creator5:
|
|
228
|
+
nozzles = self._build_nozzle_array(0, NOZZLE_OFF)
|
|
229
|
+
if nozzles is None:
|
|
230
|
+
return False
|
|
231
|
+
return await self._send_http_temp_command(nozzles=nozzles)
|
|
232
|
+
# Single-tool AD5X / 5M (forced http_only) keep rightNozzle (unchanged).
|
|
233
|
+
return await self._send_http_temp_command(right_nozzle=TEMP_OFF)
|
|
68
234
|
return await self.tcp_client.cancel_extruder_temp()
|
|
69
235
|
|
|
70
236
|
async def cancel_bed_temp(self) -> bool:
|
|
71
237
|
"""
|
|
72
|
-
Cancels
|
|
238
|
+
Cancels any ongoing print bed heating and sets its target temperature to 0.
|
|
73
239
|
|
|
74
240
|
Returns:
|
|
75
241
|
True if the command is successful, False otherwise.
|
|
76
242
|
"""
|
|
243
|
+
if self.client.http_only:
|
|
244
|
+
return await self._send_http_temp_command(platform=TEMP_OFF)
|
|
77
245
|
return await self.tcp_client.cancel_bed_temp()
|
|
78
246
|
|
|
247
|
+
async def set_chamber_temp(self, temp: int) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Sets the target temperature for the printer's heated chamber. Only the
|
|
250
|
+
Creator 5 series has a chamber heater, and those printers are HTTP-only,
|
|
251
|
+
so this always goes over the HTTP ``temperatureCtl_cmd``. The firmware
|
|
252
|
+
caps the chamber at 80°C.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
temp: The target temperature in Celsius.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if the command is acknowledged, False otherwise.
|
|
259
|
+
"""
|
|
260
|
+
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
261
|
+
print("set_chamber_temp() error, chamber heater only available on Creator 5 series.")
|
|
262
|
+
return False
|
|
263
|
+
return await self._send_http_temp_command(chamber=temp)
|
|
264
|
+
|
|
265
|
+
async def cancel_chamber_temp(self) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Cancels any ongoing chamber heating and sets its target temperature to 0.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if the command is acknowledged, False otherwise.
|
|
271
|
+
"""
|
|
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
|
+
)
|
|
276
|
+
return False
|
|
277
|
+
return await self._send_http_temp_command(chamber=TEMP_OFF)
|
|
278
|
+
|
|
79
279
|
async def wait_for_part_cool(
|
|
80
280
|
self, target_temp: float = 50.0, timeout_seconds: int = 1800
|
|
81
281
|
) -> bool:
|
|
82
282
|
"""
|
|
83
283
|
Waits for printer components to cool down to a safe temperature.
|
|
84
284
|
|
|
285
|
+
Relies on TCP G-code polling and is therefore unavailable on HTTP-only
|
|
286
|
+
printers (Creator 5 / 5 Pro); callers should poll ``info.get()`` instead.
|
|
287
|
+
|
|
85
288
|
Args:
|
|
86
289
|
target_temp: The target temperature to wait for (default: 50°C).
|
|
87
290
|
timeout_seconds: Maximum time to wait in seconds (default: 30 minutes).
|
|
88
291
|
|
|
89
292
|
Returns:
|
|
90
|
-
True if components cooled to target temperature, False if timeout
|
|
293
|
+
True if components cooled to target temperature, False if timeout, error,
|
|
294
|
+
or HTTP-only (no TCP polling channel).
|
|
91
295
|
"""
|
|
296
|
+
if self.client.http_only:
|
|
297
|
+
print(
|
|
298
|
+
"wait_for_part_cool() unavailable over HTTP-only connection; "
|
|
299
|
+
"poll info.get() instead."
|
|
300
|
+
)
|
|
301
|
+
return False
|
|
92
302
|
return await self.tcp_client.wait_for_part_cool(target_temp, timeout_seconds)
|
flashforge/api/network/utils.py
CHANGED
|
@@ -19,7 +19,7 @@ async def json_from_response(response: aiohttp.ClientResponse) -> dict:
|
|
|
19
19
|
return await response.json() # type: ignore[no-any-return]
|
|
20
20
|
except aiohttp.ContentTypeError:
|
|
21
21
|
text = await response.text()
|
|
22
|
-
return json.loads(text)
|
|
22
|
+
return json.loads(text) # type: ignore[no-any-return]
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class NetworkUtils:
|
flashforge/client.py
CHANGED
|
@@ -27,6 +27,10 @@ class FiveMClientConnectionOptions:
|
|
|
27
27
|
http_port: int | None = None
|
|
28
28
|
tcp_port: int | None = None
|
|
29
29
|
led_control_override: bool | None = None
|
|
30
|
+
# Force HTTP-only transport (skip the TCP/8899 handshake). Set this for a
|
|
31
|
+
# Creator 5 / Creator 5 Pro, which exposes no TCP service; otherwise it is
|
|
32
|
+
# auto-detected from the firmware-reported model after `verify_connection`.
|
|
33
|
+
http_only: bool | None = None
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
class FlashForgeClient:
|
|
@@ -84,6 +88,8 @@ class FlashForgeClient:
|
|
|
84
88
|
self.printer_name: str = ""
|
|
85
89
|
self.is_pro: bool = False
|
|
86
90
|
self._is_ad5x: bool = False
|
|
91
|
+
self.is_creator5: bool = False
|
|
92
|
+
self.is_creator5_pro: bool = False
|
|
87
93
|
self.firmware_version: str = ""
|
|
88
94
|
self.firmware_ver: str = ""
|
|
89
95
|
self.mac_address: str = ""
|
|
@@ -106,6 +112,15 @@ class FlashForgeClient:
|
|
|
106
112
|
)
|
|
107
113
|
self._apply_feature_overrides()
|
|
108
114
|
|
|
115
|
+
# Transport selection. Creator 5 / Creator 5 Pro have no TCP/8899
|
|
116
|
+
# service, so they must run HTTP-only. An explicit override wins;
|
|
117
|
+
# otherwise http_only is auto-set from the detected model in
|
|
118
|
+
# `verify_connection` / `cache_details`.
|
|
119
|
+
self._http_only_override: bool | None = (
|
|
120
|
+
options.http_only if options and options.http_only is not None else None
|
|
121
|
+
)
|
|
122
|
+
self._http_only: bool = bool(self._http_only_override)
|
|
123
|
+
|
|
109
124
|
@property
|
|
110
125
|
def is_ad5x(self) -> bool:
|
|
111
126
|
"""
|
|
@@ -116,6 +131,46 @@ class FlashForgeClient:
|
|
|
116
131
|
"""
|
|
117
132
|
return self._is_ad5x
|
|
118
133
|
|
|
134
|
+
@property
|
|
135
|
+
def http_only(self) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Indicates whether the client should avoid the TCP transport.
|
|
138
|
+
|
|
139
|
+
True for Creator 5 / Creator 5 Pro (no TCP/8899 service) or when an
|
|
140
|
+
explicit ``http_only`` override was supplied at construction. When True,
|
|
141
|
+
TCP-only operations are unavailable (see :meth:`can_use_tcp`).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if the client must operate over HTTP only
|
|
145
|
+
"""
|
|
146
|
+
return self._http_only
|
|
147
|
+
|
|
148
|
+
def can_use_tcp(self, op: str = "") -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Report whether a TCP-backed operation may be attempted.
|
|
151
|
+
|
|
152
|
+
Returns False whenever this client is HTTP-only (e.g. a Creator 5 with
|
|
153
|
+
no TCP/8899 service). TCP-delegating control/temperature methods check
|
|
154
|
+
this and no-op (return False) instead of hanging on a dead socket.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
op: Optional operation name, included in the warning log.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if TCP operations are permitted, False otherwise
|
|
161
|
+
"""
|
|
162
|
+
if self._http_only:
|
|
163
|
+
print(f"{op}() unavailable: printer has no TCP control channel (HTTP-only).")
|
|
164
|
+
return False
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def _update_http_only_from_model(self) -> None:
|
|
168
|
+
"""Recompute ``http_only`` from the detected model unless overridden."""
|
|
169
|
+
if self._http_only_override is not None:
|
|
170
|
+
self._http_only = bool(self._http_only_override)
|
|
171
|
+
else:
|
|
172
|
+
self._http_only = self.is_creator5 or self.is_creator5_pro
|
|
173
|
+
|
|
119
174
|
async def __aenter__(self) -> "FlashForgeClient":
|
|
120
175
|
"""Async context manager entry."""
|
|
121
176
|
await self._ensure_http_session()
|
|
@@ -209,11 +264,15 @@ class FlashForgeClient:
|
|
|
209
264
|
Disposes of the FlashForgeClient instance, stopping keep-alive messages
|
|
210
265
|
and cleaning up resources.
|
|
211
266
|
"""
|
|
212
|
-
# Stop TCP keep-alive and dispose
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
# Stop TCP keep-alive and dispose. An HTTP-only client (e.g. Creator 5,
|
|
268
|
+
# which has no TCP/8899 service) never opened a TCP socket, so skip the
|
|
269
|
+
# cleanup handshake -- otherwise the logout send would attempt (and time
|
|
270
|
+
# out on) a connection that was never established.
|
|
271
|
+
if not self._http_only:
|
|
272
|
+
if hasattr(self.tcp_client, "stop_keep_alive"):
|
|
273
|
+
await self.tcp_client.stop_keep_alive(True)
|
|
274
|
+
if hasattr(self.tcp_client, "dispose"):
|
|
275
|
+
await self.tcp_client.dispose()
|
|
217
276
|
|
|
218
277
|
# Close HTTP session
|
|
219
278
|
if self._http_session and not self._http_session.closed:
|
|
@@ -298,6 +357,10 @@ class FlashForgeClient:
|
|
|
298
357
|
self.camera_stream_url = info.camera_stream_url or ""
|
|
299
358
|
self.lifetime_print_time = info.formatted_total_run_time or ""
|
|
300
359
|
self._is_ad5x = info.is_ad5x
|
|
360
|
+
self.is_creator5 = info.is_creator5
|
|
361
|
+
self.is_creator5_pro = info.is_creator5_pro
|
|
362
|
+
# Refresh transport selection now that the model flags are cached.
|
|
363
|
+
self._update_http_only_from_model()
|
|
301
364
|
|
|
302
365
|
# Format filament usage
|
|
303
366
|
filament_value = info.cumulative_filament if info.cumulative_filament is not None else 0.0
|
|
@@ -338,15 +401,31 @@ class FlashForgeClient:
|
|
|
338
401
|
print("Failed to parse machine info from detail response")
|
|
339
402
|
return False
|
|
340
403
|
|
|
341
|
-
#
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
404
|
+
# Detect http_only from the parsed model BEFORE touching TCP. The
|
|
405
|
+
# Creator 5 / Creator 5 Pro expose no TCP/8899 service, so the M115
|
|
406
|
+
# handshake would hang until the connect timeout. An explicit
|
|
407
|
+
# override always wins.
|
|
408
|
+
if self._http_only_override is not None:
|
|
409
|
+
self._http_only = bool(self._http_only_override)
|
|
346
410
|
else:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
411
|
+
self._http_only = machine_info.is_creator5
|
|
412
|
+
|
|
413
|
+
# Get TCP printer information to check for Pro model
|
|
414
|
+
tcp_info: PrinterInfo | None = None
|
|
415
|
+
if not self._http_only:
|
|
416
|
+
tcp_info = await self.tcp_client.get_printer_info()
|
|
417
|
+
if tcp_info:
|
|
418
|
+
if (
|
|
419
|
+
"Pro" in tcp_info.type_name
|
|
420
|
+
and not machine_info.is_pro
|
|
421
|
+
and not machine_info.is_ad5x
|
|
422
|
+
):
|
|
423
|
+
self.is_pro = True
|
|
424
|
+
else:
|
|
425
|
+
print(
|
|
426
|
+
"Warning: Unable to get PrinterInfo from TCP API, "
|
|
427
|
+
"some features might not work"
|
|
428
|
+
)
|
|
350
429
|
|
|
351
430
|
# Cache the details
|
|
352
431
|
return self.cache_details(machine_info)
|
|
@@ -67,9 +67,25 @@ class PrinterModel(StrEnum):
|
|
|
67
67
|
ADVENTURER_5M_PRO = "Adventurer5MPro"
|
|
68
68
|
ADVENTURER_4 = "Adventurer4"
|
|
69
69
|
ADVENTURER_3 = "Adventurer3"
|
|
70
|
+
CREATOR_5 = "Creator5"
|
|
71
|
+
CREATOR_5_PRO = "Creator5Pro"
|
|
70
72
|
UNKNOWN = "Unknown"
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
# Canonical USB Product IDs (discovery packet offset 0x88) for modern printers.
|
|
76
|
+
# Same value space as the firmware /detail `pid` field and the FlashForge
|
|
77
|
+
# update-checker keys, so it is the authoritative model discriminator -- unlike
|
|
78
|
+
# `product_type` (0x5A02), which only identifies the 5M *family* and cannot tell
|
|
79
|
+
# 5M / 5M Pro / AD5X / Creator 5 apart.
|
|
80
|
+
MODERN_PRODUCT_IDS = {
|
|
81
|
+
0x0023: PrinterModel.ADVENTURER_5M,
|
|
82
|
+
0x0024: PrinterModel.ADVENTURER_5M_PRO,
|
|
83
|
+
0x0026: PrinterModel.AD5X,
|
|
84
|
+
0x0028: PrinterModel.CREATOR_5,
|
|
85
|
+
0x0029: PrinterModel.CREATOR_5_PRO,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
73
89
|
class DiscoveryProtocol(StrEnum):
|
|
74
90
|
"""Discovery response wire formats."""
|
|
75
91
|
|
|
@@ -439,7 +455,7 @@ class PrinterDiscovery:
|
|
|
439
455
|
product_type = int.from_bytes(buffer[0x8C:0x8E], byteorder="big")
|
|
440
456
|
event_port = int.from_bytes(buffer[0x8E:0x90], byteorder="big")
|
|
441
457
|
serial_number = buffer[0x92 : 0x92 + 128].decode("utf-8", errors="ignore").split("\x00", 1)[0]
|
|
442
|
-
model = self.detect_modern_model(name, product_type)
|
|
458
|
+
model = self.detect_modern_model(name, product_type, product_id)
|
|
443
459
|
|
|
444
460
|
return DiscoveredPrinter(
|
|
445
461
|
model=model,
|
|
@@ -480,18 +496,39 @@ class PrinterDiscovery:
|
|
|
480
496
|
status=self.map_status_code(status_code),
|
|
481
497
|
)
|
|
482
498
|
|
|
483
|
-
def detect_modern_model(
|
|
484
|
-
|
|
485
|
-
|
|
499
|
+
def detect_modern_model(
|
|
500
|
+
self,
|
|
501
|
+
name: str,
|
|
502
|
+
product_type: int,
|
|
503
|
+
product_id: int | None = None,
|
|
504
|
+
) -> PrinterModel:
|
|
505
|
+
"""Detect a modern printer model from discovery metadata.
|
|
506
|
+
|
|
507
|
+
Resolution order:
|
|
508
|
+
1. USB product ID (offset 0x88) -- authoritative, user-immutable, and
|
|
509
|
+
the only signal that distinguishes 5M / 5M Pro / AD5X / Creator 5.
|
|
510
|
+
2. product_type 0x5A02 (5M *family*) + name -- fallback for firmware
|
|
511
|
+
reporting an unknown/zero product ID.
|
|
512
|
+
3. Name heuristics -- last resort.
|
|
513
|
+
"""
|
|
514
|
+
# Product ID is authoritative (firmware-set, not user-mutable).
|
|
515
|
+
if product_id is not None and product_id in MODERN_PRODUCT_IDS:
|
|
516
|
+
return MODERN_PRODUCT_IDS[product_id]
|
|
486
517
|
|
|
487
|
-
|
|
488
|
-
return PrinterModel.AD5X
|
|
518
|
+
upper_name = name.upper()
|
|
489
519
|
|
|
520
|
+
# 0x5A02 identifies the 5M family but not the specific model, so we still
|
|
521
|
+
# need the name to separate base from Pro.
|
|
490
522
|
if product_type == 0x5A02:
|
|
523
|
+
if upper_name == "AD5X":
|
|
524
|
+
return PrinterModel.AD5X
|
|
491
525
|
if "PRO" in upper_name:
|
|
492
526
|
return PrinterModel.ADVENTURER_5M_PRO
|
|
493
527
|
return PrinterModel.ADVENTURER_5M
|
|
494
528
|
|
|
529
|
+
if upper_name == "AD5X":
|
|
530
|
+
return PrinterModel.AD5X
|
|
531
|
+
|
|
495
532
|
if "ADVENTURER 5M" in upper_name or "AD5M" in upper_name:
|
|
496
533
|
if "PRO" in upper_name:
|
|
497
534
|
return PrinterModel.ADVENTURER_5M_PRO
|
flashforge/models/__init__.py
CHANGED
|
@@ -18,6 +18,8 @@ from .responses import (
|
|
|
18
18
|
AD5XMaterialMapping,
|
|
19
19
|
AD5XSingleColorJobParams,
|
|
20
20
|
AD5XUploadParams,
|
|
21
|
+
Creator5JobParams,
|
|
22
|
+
Creator5UploadParams,
|
|
21
23
|
DetailResponse,
|
|
22
24
|
FilamentArgs,
|
|
23
25
|
GCodeListResponse,
|
|
@@ -46,6 +48,8 @@ __all__ = [
|
|
|
46
48
|
"AD5XLocalJobParams",
|
|
47
49
|
"AD5XSingleColorJobParams",
|
|
48
50
|
"AD5XUploadParams",
|
|
51
|
+
"Creator5JobParams",
|
|
52
|
+
"Creator5UploadParams",
|
|
49
53
|
"GCodeListResponse",
|
|
50
54
|
"ThumbnailResponse",
|
|
51
55
|
]
|
|
@@ -250,6 +250,17 @@ class FFPrinterDetail(BaseModel):
|
|
|
250
250
|
nozzle_cnt: int | None = Field(default=None, ge=1, le=4, alias="nozzleCnt")
|
|
251
251
|
nozzle_model: str | None = Field(default=None, alias="nozzleModel")
|
|
252
252
|
nozzle_style: int | None = Field(default=None, ge=0, alias="nozzleStyle")
|
|
253
|
+
# --- Creator 5 series raw fields ---
|
|
254
|
+
# Immutable factory model name (e.g. "Creator 5 Pro"); unlike `name` this is
|
|
255
|
+
# not user-editable. May be absent on older firmware.
|
|
256
|
+
model: str | None = Field(default=None, alias="model")
|
|
257
|
+
# Per-tool current nozzle temperatures (one entry per nozzle). Multi-nozzle
|
|
258
|
+
# Creator 5 series report these; single-nozzle models use rightTemp/leftTemp.
|
|
259
|
+
nozzle_temps: list[float] | None = Field(default=None, alias="nozzleTemps")
|
|
260
|
+
# Per-tool target nozzle temperatures (one entry per nozzle).
|
|
261
|
+
nozzle_target_temps: list[float] | None = Field(default=None, alias="nozzleTargetTemps")
|
|
262
|
+
# Lidar / first-layer scanner presence flag (1 = present, 0 = absent).
|
|
263
|
+
lidar: int | None = Field(default=None, ge=0, alias="lidar")
|
|
253
264
|
pid: int | None = Field(default=None, ge=0, alias="pid")
|
|
254
265
|
plat_target_temp: float | None = Field(default=None, ge=-50, le=500, alias="platTargetTemp")
|
|
255
266
|
plat_temp: float | None = Field(default=None, ge=-50, le=500, alias="platTemp")
|
|
@@ -327,11 +338,35 @@ class FFMachineInfo(BaseModel):
|
|
|
327
338
|
pid: int | None = None
|
|
328
339
|
is_pro: bool = False
|
|
329
340
|
is_ad5x: bool = False
|
|
341
|
+
# Creator 5 / Creator 5 Pro (4-head tool-changer) detection. Drives the
|
|
342
|
+
# http_only transport decision and Pro-only capabilities (door sensor).
|
|
343
|
+
is_creator5: bool = False
|
|
344
|
+
is_creator5_pro: bool = False
|
|
345
|
+
# Immutable factory model name (e.g. "Creator 5 Pro"). Falls back to a
|
|
346
|
+
# PID-derived name, then the user-set `name`, when the printer doesn't
|
|
347
|
+
# report the `model` field.
|
|
348
|
+
model: str | None = None
|
|
330
349
|
nozzle_size: str = ""
|
|
350
|
+
# Number of tools/nozzles the printer reports. Creator 5 series = 4
|
|
351
|
+
# (tool-changer); single-nozzle models = 1. Mirrors len(tool_temps).
|
|
352
|
+
nozzle_count: int | None = None
|
|
331
353
|
|
|
332
354
|
# Temperatures
|
|
333
355
|
print_bed: Temperature = Field(default_factory=Temperature)
|
|
334
356
|
extruder: Temperature = Field(default_factory=Temperature)
|
|
357
|
+
# Heated chamber. Only the Creator 5 series has one; other models report 0/0.
|
|
358
|
+
chamber: Temperature | None = None
|
|
359
|
+
# Current/target temperatures for every tool/nozzle. Single-nozzle models
|
|
360
|
+
# report a 1-element array mirroring `extruder`; Creator 5 series report
|
|
361
|
+
# one entry per nozzle.
|
|
362
|
+
tool_temps: list[Temperature] = Field(default_factory=list)
|
|
363
|
+
|
|
364
|
+
# Capability flags (presence-derived, never assumed from model family)
|
|
365
|
+
has_camera: bool = False
|
|
366
|
+
has_lidar: bool = False
|
|
367
|
+
# Only true on models with confirmed hardware (Creator 5 Pro). When false,
|
|
368
|
+
# `door_open` is cosmetic and should not be surfaced.
|
|
369
|
+
has_door_sensor: bool = False
|
|
335
370
|
|
|
336
371
|
# Current print stats
|
|
337
372
|
print_duration: int = 0
|
flashforge/models/responses.py
CHANGED
|
@@ -164,6 +164,65 @@ class AD5XUploadParams(BaseModel):
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
class Creator5JobParams(BaseModel):
|
|
168
|
+
"""Parameters for starting a Creator 5 / Creator 5 Pro local print job.
|
|
169
|
+
|
|
170
|
+
Distinct from the AD5X job params: the Creator 5 maps materials at print-start
|
|
171
|
+
(POST /printGcode) rather than upload time, so the body carries NO
|
|
172
|
+
``useMatlStation`` / ``gcodeToolCnt`` / ``firstLayerInspection`` (the latter
|
|
173
|
+
doesn't exist on the C5). ``flowCalibration`` and ``timeLapseVideo`` are always
|
|
174
|
+
present (default False); ``material_mappings`` is optional for a single-tool
|
|
175
|
+
print.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
179
|
+
|
|
180
|
+
file_name: str = Field(description="Name of the file on the printer to start")
|
|
181
|
+
leveling_before_print: bool = Field(
|
|
182
|
+
description="Whether to perform bed leveling before printing"
|
|
183
|
+
)
|
|
184
|
+
flow_calibration: bool = Field(
|
|
185
|
+
default=False, description="Whether to enable flow calibration"
|
|
186
|
+
)
|
|
187
|
+
time_lapse_video: bool = Field(
|
|
188
|
+
default=False, description="Whether to enable time lapse video recording"
|
|
189
|
+
)
|
|
190
|
+
material_mappings: list[AD5XMaterialMapping] | None = Field(
|
|
191
|
+
default=None,
|
|
192
|
+
max_length=4,
|
|
193
|
+
description="Optional per-tool material mappings (1-4 items); omit for single-tool",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Creator5UploadParams(BaseModel):
|
|
198
|
+
"""Parameters for uploading a file to a Creator 5 / Creator 5 Pro.
|
|
199
|
+
|
|
200
|
+
Mirrors the AD5X upload but omits ``firstLayerInspection`` (absent on the C5)
|
|
201
|
+
and the ``materialMappings`` header (the C5 maps materials at print-start, not
|
|
202
|
+
upload). The C5 firmware checks the booleans as the string "true"/"false".
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
206
|
+
|
|
207
|
+
file_path: str = Field(description="Local file path to upload")
|
|
208
|
+
start_print: bool = Field(description="Whether to start printing immediately after upload")
|
|
209
|
+
leveling_before_print: bool = Field(
|
|
210
|
+
description="Whether to perform bed leveling before printing"
|
|
211
|
+
)
|
|
212
|
+
flow_calibration: bool = Field(
|
|
213
|
+
default=False, description="Whether to enable flow calibration"
|
|
214
|
+
)
|
|
215
|
+
time_lapse_video: bool = Field(
|
|
216
|
+
default=False, description="Whether to enable time lapse video recording"
|
|
217
|
+
)
|
|
218
|
+
use_matl_station: bool = Field(
|
|
219
|
+
description="Whether this is a multi-tool material-station job"
|
|
220
|
+
)
|
|
221
|
+
gcode_tool_cnt: int = Field(
|
|
222
|
+
ge=1, le=4, description="Number of tools in the G-code (1-4 for the C5)"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
167
226
|
class GCodeListResponse(GenericResponse):
|
|
168
227
|
"""Represents the response structure for a G-code file list request."""
|
|
169
228
|
|