flashforge-python-api 1.0.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 +188 -0
- flashforge/api/__init__.py +17 -0
- flashforge/api/constants/__init__.py +10 -0
- flashforge/api/constants/commands.py +10 -0
- flashforge/api/constants/endpoints.py +11 -0
- flashforge/api/controls/__init__.py +16 -0
- flashforge/api/controls/control.py +357 -0
- flashforge/api/controls/files.py +171 -0
- flashforge/api/controls/info.py +292 -0
- flashforge/api/controls/job_control.py +561 -0
- flashforge/api/controls/temp_control.py +89 -0
- flashforge/api/filament/__init__.py +7 -0
- flashforge/api/filament/filament.py +39 -0
- flashforge/api/misc/__init__.py +8 -0
- flashforge/api/misc/scientific_notation.py +28 -0
- flashforge/api/misc/temperature.py +42 -0
- flashforge/api/network/__init__.py +10 -0
- flashforge/api/network/fnet_code.py +15 -0
- flashforge/api/network/utils.py +55 -0
- flashforge/client.py +381 -0
- flashforge/discovery/__init__.py +12 -0
- flashforge/discovery/discovery.py +388 -0
- flashforge/models/__init__.py +50 -0
- flashforge/models/machine_info.py +247 -0
- flashforge/models/responses.py +123 -0
- flashforge/tcp/__init__.py +41 -0
- flashforge/tcp/ff_client.py +587 -0
- flashforge/tcp/gcode/__init__.py +11 -0
- flashforge/tcp/gcode/gcode_controller.py +296 -0
- flashforge/tcp/gcode/gcodes.py +93 -0
- flashforge/tcp/parsers/__init__.py +27 -0
- flashforge/tcp/parsers/endstop_status.py +273 -0
- flashforge/tcp/parsers/location_info.py +68 -0
- flashforge/tcp/parsers/print_status.py +182 -0
- flashforge/tcp/parsers/printer_info.py +154 -0
- flashforge/tcp/parsers/temp_info.py +217 -0
- flashforge/tcp/parsers/thumbnail_info.py +242 -0
- flashforge/tcp/tcp_client.py +448 -0
- flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
- flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
- flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
- flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
- flashforge_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Printer Discovery (FIXED VERSION)
|
|
3
|
+
|
|
4
|
+
UDP-based printer discovery implementation that finds FlashForge printers on the local network.
|
|
5
|
+
Fixed to match the original TypeScript implementation behavior.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
import netifaces
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FlashForgePrinter:
|
|
20
|
+
"""
|
|
21
|
+
Represents a discovered FlashForge 3D printer.
|
|
22
|
+
Stores information such as name, serial number, and IP address.
|
|
23
|
+
"""
|
|
24
|
+
name: str = ""
|
|
25
|
+
serial_number: str = ""
|
|
26
|
+
ip_address: str = ""
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
"""Returns a string representation of the FlashForgePrinter object."""
|
|
30
|
+
return f"Name: {self.name}, Serial: {self.serial_number}, IP: {self.ip_address}"
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
"""Returns a detailed string representation for debugging."""
|
|
34
|
+
return f"FlashForgePrinter(name='{self.name}', serial_number='{self.serial_number}', ip_address='{self.ip_address}')"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FlashForgePrinterDiscovery:
|
|
38
|
+
"""
|
|
39
|
+
Handles the discovery of FlashForge printers on the local network.
|
|
40
|
+
Uses UDP broadcast messages to find printers and parses their responses.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# The UDP port used for sending discovery messages to FlashForge printers
|
|
44
|
+
DISCOVERY_PORT = 48899
|
|
45
|
+
# The port we listen on for responses (must match TypeScript implementation)
|
|
46
|
+
LISTEN_PORT = 18007
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
"""Initialize the printer discovery client."""
|
|
50
|
+
self.discovery_port = self.DISCOVERY_PORT
|
|
51
|
+
self.listen_port = self.LISTEN_PORT
|
|
52
|
+
|
|
53
|
+
# The discovery UDP packet is a 20-byte message.
|
|
54
|
+
# It starts with "www.usr" followed by specific bytes.
|
|
55
|
+
# This packet structure is based on observations from FlashPrint software.
|
|
56
|
+
self.discovery_message = bytes([
|
|
57
|
+
0x77, 0x77, 0x77, 0x2e, 0x75, 0x73, 0x72, 0x22, # "www.usr"
|
|
58
|
+
0x65, 0x36, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
59
|
+
0x00, 0x00, 0x00, 0x00
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
async def discover_printers_async(
|
|
63
|
+
self,
|
|
64
|
+
timeout_ms: int = 10000,
|
|
65
|
+
idle_timeout_ms: int = 1500,
|
|
66
|
+
max_retries: int = 3
|
|
67
|
+
) -> List[FlashForgePrinter]:
|
|
68
|
+
"""
|
|
69
|
+
Discovers FlashForge printers on the network asynchronously.
|
|
70
|
+
|
|
71
|
+
FIXED VERSION - Uses proper asyncio UDP socket handling that matches the TypeScript implementation.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
timeout_ms: The total time (in milliseconds) to wait for printer responses
|
|
75
|
+
idle_timeout_ms: The time (in milliseconds) to wait for additional responses
|
|
76
|
+
after the last received one
|
|
77
|
+
max_retries: The maximum number of discovery attempts
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A list of FlashForgePrinter objects found on the network
|
|
81
|
+
"""
|
|
82
|
+
printers: List[FlashForgePrinter] = []
|
|
83
|
+
broadcast_addresses = self._get_broadcast_addresses()
|
|
84
|
+
attempt = 0
|
|
85
|
+
|
|
86
|
+
logger.info(f"Starting printer discovery with {len(broadcast_addresses)} broadcast addresses")
|
|
87
|
+
|
|
88
|
+
while attempt < max_retries:
|
|
89
|
+
attempt += 1
|
|
90
|
+
logger.debug(f"Discovery attempt {attempt}/{max_retries}")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Use asyncio datagram endpoint for proper UDP handling
|
|
94
|
+
transport, protocol = await asyncio.get_event_loop().create_datagram_endpoint(
|
|
95
|
+
lambda: DiscoveryProtocol(self),
|
|
96
|
+
local_addr=('0.0.0.0', self.listen_port),
|
|
97
|
+
allow_broadcast=True
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Send discovery messages to all broadcast addresses
|
|
102
|
+
for broadcast_address in broadcast_addresses:
|
|
103
|
+
try:
|
|
104
|
+
transport.sendto(self.discovery_message, (broadcast_address, self.discovery_port))
|
|
105
|
+
logger.debug(f"Sent discovery message to {broadcast_address}:{self.discovery_port}")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(f"Failed to send to {broadcast_address}: {e}")
|
|
108
|
+
|
|
109
|
+
# Wait for responses using the protocol
|
|
110
|
+
found_printers = await protocol.wait_for_responses(
|
|
111
|
+
timeout_ms / 1000.0, idle_timeout_ms / 1000.0
|
|
112
|
+
)
|
|
113
|
+
printers.extend(found_printers)
|
|
114
|
+
|
|
115
|
+
finally:
|
|
116
|
+
transport.close()
|
|
117
|
+
|
|
118
|
+
if printers:
|
|
119
|
+
break # Printers found, exit retry loop
|
|
120
|
+
|
|
121
|
+
if attempt < max_retries:
|
|
122
|
+
logger.debug(f"No printers found, waiting before retry {attempt + 1}")
|
|
123
|
+
await asyncio.sleep(1.0) # Wait before retrying
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Error during discovery attempt {attempt}: {e}")
|
|
127
|
+
if attempt < max_retries:
|
|
128
|
+
await asyncio.sleep(1.0)
|
|
129
|
+
|
|
130
|
+
# Remove duplicates based on IP address
|
|
131
|
+
unique_printers = {}
|
|
132
|
+
for printer in printers:
|
|
133
|
+
if printer.ip_address not in unique_printers:
|
|
134
|
+
unique_printers[printer.ip_address] = printer
|
|
135
|
+
|
|
136
|
+
final_printers = list(unique_printers.values())
|
|
137
|
+
logger.info(f"Discovery completed. Found {len(final_printers)} unique printers")
|
|
138
|
+
|
|
139
|
+
return final_printers
|
|
140
|
+
|
|
141
|
+
def _parse_printer_response(self, response: bytes, ip_address: str) -> Optional[FlashForgePrinter]:
|
|
142
|
+
"""
|
|
143
|
+
Parses the UDP response received from a FlashForge printer.
|
|
144
|
+
|
|
145
|
+
The response is a buffer containing printer information at specific offsets:
|
|
146
|
+
- Printer Name: ASCII string at offset 0x00 (32 bytes)
|
|
147
|
+
- Serial Number: ASCII string at offset 0x92 (32 bytes)
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
response: The bytes containing the printer's response
|
|
151
|
+
ip_address: The IP address from which the response was received
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A FlashForgePrinter object if parsing is successful, otherwise None
|
|
155
|
+
"""
|
|
156
|
+
# Expected response length is at least 0xC4 (196 bytes) to contain name and serial
|
|
157
|
+
if not response:
|
|
158
|
+
logger.warning(f"Invalid response from {ip_address}: response is None or empty")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
if len(response) < 0xC4:
|
|
162
|
+
logger.warning(f"Invalid response from {ip_address}, length: {len(response)}")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Printer name is at offset 0x00, padded with null characters
|
|
167
|
+
name = response[0:32].decode('ascii', errors='ignore').rstrip('\x00')
|
|
168
|
+
|
|
169
|
+
# Serial number is at offset 0x92, padded with null characters
|
|
170
|
+
serial_number = response[0x92:0x92 + 32].decode('ascii', errors='ignore').rstrip('\x00')
|
|
171
|
+
|
|
172
|
+
if not name and not serial_number:
|
|
173
|
+
logger.warning(f"Empty name and serial from {ip_address}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
printer = FlashForgePrinter(
|
|
177
|
+
name=name,
|
|
178
|
+
serial_number=serial_number,
|
|
179
|
+
ip_address=ip_address
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return printer
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Error parsing response from {ip_address}: {e}")
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def _get_broadcast_addresses(self) -> List[str]:
|
|
189
|
+
"""
|
|
190
|
+
Retrieves a list of broadcast addresses for all active IPv4 network interfaces.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A list of broadcast address strings
|
|
194
|
+
"""
|
|
195
|
+
broadcast_addresses: List[str] = []
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Get all network interfaces
|
|
199
|
+
for interface_name in netifaces.interfaces():
|
|
200
|
+
try:
|
|
201
|
+
# Get IPv4 addresses for this interface
|
|
202
|
+
addresses = netifaces.ifaddresses(interface_name)
|
|
203
|
+
if netifaces.AF_INET not in addresses:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
for addr_info in addresses[netifaces.AF_INET]:
|
|
207
|
+
# Skip loopback interfaces
|
|
208
|
+
if addr_info.get('addr', '').startswith('127.'):
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# Calculate broadcast address if netmask is available
|
|
212
|
+
ip_addr = addr_info.get('addr')
|
|
213
|
+
netmask = addr_info.get('netmask')
|
|
214
|
+
|
|
215
|
+
if ip_addr and netmask:
|
|
216
|
+
broadcast = self._calculate_broadcast_address(ip_addr, netmask)
|
|
217
|
+
if broadcast and broadcast not in broadcast_addresses:
|
|
218
|
+
broadcast_addresses.append(broadcast)
|
|
219
|
+
logger.debug(f"Added broadcast address: {broadcast} (interface: {interface_name})")
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.warning(f"Error processing interface {interface_name}: {e}")
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error getting network interfaces: {e}")
|
|
227
|
+
# Fallback to common broadcast addresses
|
|
228
|
+
broadcast_addresses = ['255.255.255.255', '192.168.1.255', '192.168.0.255']
|
|
229
|
+
logger.info("Using fallback broadcast addresses")
|
|
230
|
+
|
|
231
|
+
# Always include the general broadcast address
|
|
232
|
+
if '255.255.255.255' not in broadcast_addresses:
|
|
233
|
+
broadcast_addresses.append('255.255.255.255')
|
|
234
|
+
|
|
235
|
+
logger.debug(f"Using broadcast addresses: {broadcast_addresses}")
|
|
236
|
+
return broadcast_addresses
|
|
237
|
+
|
|
238
|
+
def _calculate_broadcast_address(self, ip_address: str, subnet_mask: str) -> Optional[str]:
|
|
239
|
+
"""
|
|
240
|
+
Calculates the broadcast address for a given IP address and subnet mask.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
ip_address: The IPv4 address string (e.g., "192.168.1.10")
|
|
244
|
+
subnet_mask: The IPv4 subnet mask string (e.g., "255.255.255.0")
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The calculated broadcast address string, or None if input is invalid
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
# Convert IP and subnet to arrays of numbers
|
|
251
|
+
ip_parts = [int(x) for x in ip_address.split('.')]
|
|
252
|
+
mask_parts = [int(x) for x in subnet_mask.split('.')]
|
|
253
|
+
|
|
254
|
+
if len(ip_parts) != 4 or len(mask_parts) != 4:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
# Calculate broadcast address: IP | (~MASK)
|
|
258
|
+
broadcast_parts = [ip_parts[i] | (~mask_parts[i] & 255) for i in range(4)]
|
|
259
|
+
return '.'.join(map(str, broadcast_parts))
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.warning(f"Error calculating broadcast address for {ip_address}/{subnet_mask}: {e}")
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def print_debug_info(self, response: bytes, ip_address: str) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Prints detailed debugging information about a received UDP response.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
response: The bytes containing the response data
|
|
271
|
+
ip_address: The IP address from which the response was received
|
|
272
|
+
"""
|
|
273
|
+
print(f"Received response from {ip_address}:")
|
|
274
|
+
print(f"Response length: {len(response)} bytes")
|
|
275
|
+
|
|
276
|
+
# Hex dump
|
|
277
|
+
print("Hex dump:")
|
|
278
|
+
for i in range(0, len(response), 16):
|
|
279
|
+
line = f"{i:04x} "
|
|
280
|
+
|
|
281
|
+
# Hex values
|
|
282
|
+
for j in range(16):
|
|
283
|
+
if i + j < len(response):
|
|
284
|
+
line += f"{response[i + j]:02x} "
|
|
285
|
+
else:
|
|
286
|
+
line += " "
|
|
287
|
+
|
|
288
|
+
if j == 7:
|
|
289
|
+
line += " "
|
|
290
|
+
|
|
291
|
+
# ASCII representation
|
|
292
|
+
line += " "
|
|
293
|
+
for j in range(16):
|
|
294
|
+
if i + j < len(response):
|
|
295
|
+
c = response[i + j]
|
|
296
|
+
line += chr(c) if 32 <= c <= 126 else '.'
|
|
297
|
+
|
|
298
|
+
print(line)
|
|
299
|
+
|
|
300
|
+
# ASCII dump
|
|
301
|
+
print("ASCII dump:")
|
|
302
|
+
try:
|
|
303
|
+
ascii_content = response.decode('ascii', errors='replace')
|
|
304
|
+
print(repr(ascii_content))
|
|
305
|
+
except Exception as e:
|
|
306
|
+
print(f"Error decoding ASCII: {e}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class DiscoveryProtocol(asyncio.DatagramProtocol):
|
|
310
|
+
"""
|
|
311
|
+
Asyncio datagram protocol for handling UDP discovery responses.
|
|
312
|
+
This matches the event-driven approach used in the TypeScript implementation.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(self, discovery: FlashForgePrinterDiscovery):
|
|
316
|
+
self.discovery = discovery
|
|
317
|
+
self.printers: List[FlashForgePrinter] = []
|
|
318
|
+
self.response_event = asyncio.Event()
|
|
319
|
+
self.last_response_time = 0.0
|
|
320
|
+
|
|
321
|
+
def connection_made(self, transport):
|
|
322
|
+
self.transport = transport
|
|
323
|
+
logger.debug("Discovery protocol connection established")
|
|
324
|
+
|
|
325
|
+
def datagram_received(self, data: bytes, addr):
|
|
326
|
+
"""Handle incoming UDP datagram (printer response)."""
|
|
327
|
+
ip_address = addr[0]
|
|
328
|
+
logger.debug(f"Received {len(data)} bytes from {ip_address}")
|
|
329
|
+
|
|
330
|
+
# Parse the response
|
|
331
|
+
printer = self.discovery._parse_printer_response(data, ip_address)
|
|
332
|
+
if printer:
|
|
333
|
+
self.printers.append(printer)
|
|
334
|
+
logger.info(f"Discovered printer: {printer}")
|
|
335
|
+
|
|
336
|
+
# Update last response time and signal that we got a response
|
|
337
|
+
self.last_response_time = asyncio.get_event_loop().time()
|
|
338
|
+
self.response_event.set()
|
|
339
|
+
self.response_event.clear() # Reset for next response
|
|
340
|
+
|
|
341
|
+
def error_received(self, exc):
|
|
342
|
+
logger.error(f"Discovery protocol error: {exc}")
|
|
343
|
+
|
|
344
|
+
async def wait_for_responses(self, total_timeout: float, idle_timeout: float) -> List[FlashForgePrinter]:
|
|
345
|
+
"""
|
|
346
|
+
Wait for printer responses with total and idle timeouts.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
total_timeout: Maximum total time to wait for responses
|
|
350
|
+
idle_timeout: Maximum time to wait between responses
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of discovered printers
|
|
354
|
+
"""
|
|
355
|
+
start_time = asyncio.get_event_loop().time()
|
|
356
|
+
self.last_response_time = start_time
|
|
357
|
+
|
|
358
|
+
logger.debug(f"Waiting for responses (total timeout: {total_timeout}s, idle timeout: {idle_timeout}s)")
|
|
359
|
+
|
|
360
|
+
while True:
|
|
361
|
+
current_time = asyncio.get_event_loop().time()
|
|
362
|
+
|
|
363
|
+
# Check total timeout
|
|
364
|
+
if current_time - start_time >= total_timeout:
|
|
365
|
+
logger.debug("Total timeout reached")
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
# Check idle timeout
|
|
369
|
+
if current_time - self.last_response_time >= idle_timeout:
|
|
370
|
+
logger.debug("Idle timeout reached")
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
# Wait for next response or timeout
|
|
374
|
+
try:
|
|
375
|
+
remaining_total = total_timeout - (current_time - start_time)
|
|
376
|
+
remaining_idle = idle_timeout - (current_time - self.last_response_time)
|
|
377
|
+
wait_time = min(remaining_total, remaining_idle, 0.1) # Max 100ms wait
|
|
378
|
+
|
|
379
|
+
if wait_time <= 0:
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
await asyncio.wait_for(self.response_event.wait(), timeout=wait_time)
|
|
383
|
+
except asyncio.TimeoutError:
|
|
384
|
+
# Continue to check timeouts
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
logger.debug(f"Finished waiting, found {len(self.printers)} printers")
|
|
388
|
+
return self.printers
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Models Package
|
|
3
|
+
"""
|
|
4
|
+
from .machine_info import (
|
|
5
|
+
FFMachineInfo,
|
|
6
|
+
FFPrinterDetail,
|
|
7
|
+
FFGcodeFileEntry,
|
|
8
|
+
FFGcodeToolData,
|
|
9
|
+
MachineState,
|
|
10
|
+
Temperature,
|
|
11
|
+
SlotInfo,
|
|
12
|
+
MatlStationInfo,
|
|
13
|
+
IndepMatlInfo,
|
|
14
|
+
)
|
|
15
|
+
from .responses import (
|
|
16
|
+
DetailResponse,
|
|
17
|
+
FilamentArgs,
|
|
18
|
+
GenericResponse,
|
|
19
|
+
Product,
|
|
20
|
+
ProductResponse,
|
|
21
|
+
AD5XMaterialMapping,
|
|
22
|
+
AD5XLocalJobParams,
|
|
23
|
+
AD5XSingleColorJobParams,
|
|
24
|
+
AD5XUploadParams,
|
|
25
|
+
GCodeListResponse,
|
|
26
|
+
ThumbnailResponse,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"FFMachineInfo",
|
|
31
|
+
"FFPrinterDetail",
|
|
32
|
+
"FFGcodeFileEntry",
|
|
33
|
+
"FFGcodeToolData",
|
|
34
|
+
"MachineState",
|
|
35
|
+
"Temperature",
|
|
36
|
+
"SlotInfo",
|
|
37
|
+
"MatlStationInfo",
|
|
38
|
+
"IndepMatlInfo",
|
|
39
|
+
"DetailResponse",
|
|
40
|
+
"FilamentArgs",
|
|
41
|
+
"GenericResponse",
|
|
42
|
+
"Product",
|
|
43
|
+
"ProductResponse",
|
|
44
|
+
"AD5XMaterialMapping",
|
|
45
|
+
"AD5XLocalJobParams",
|
|
46
|
+
"AD5XSingleColorJobParams",
|
|
47
|
+
"AD5XUploadParams",
|
|
48
|
+
"GCodeListResponse",
|
|
49
|
+
"ThumbnailResponse",
|
|
50
|
+
]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Data Models
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MachineState(Enum):
|
|
14
|
+
"""Enumerates the possible operational states of the FlashForge 3D printer."""
|
|
15
|
+
READY = "ready"
|
|
16
|
+
BUSY = "busy"
|
|
17
|
+
CALIBRATING = "calibrating"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
HEATING = "heating"
|
|
20
|
+
PRINTING = "printing"
|
|
21
|
+
PAUSING = "pausing"
|
|
22
|
+
PAUSED = "paused"
|
|
23
|
+
CANCELLED = "cancelled"
|
|
24
|
+
COMPLETED = "completed"
|
|
25
|
+
UNKNOWN = "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Temperature(BaseModel):
|
|
29
|
+
"""Represents a pair of current and target temperatures for a component like an extruder or print bed."""
|
|
30
|
+
current: float = Field(default=0.0, description="The current temperature in Celsius")
|
|
31
|
+
set: float = Field(default=0.0, description="The target (set) temperature in Celsius")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SlotInfo(BaseModel):
|
|
35
|
+
"""Information about a single slot in the material station."""
|
|
36
|
+
has_filament: bool = Field(alias="hasFilament", description="Indicates if filament is present in this slot")
|
|
37
|
+
material_color: str = Field(alias="materialColor", description="Color of the material in this slot (e.g., '#FFFFFF')")
|
|
38
|
+
material_name: str = Field(alias="materialName", description="Name of the material in this slot (e.g., 'PLA')")
|
|
39
|
+
slot_id: int = Field(alias="slotId", description="Identifier for this slot")
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
populate_by_name = True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MatlStationInfo(BaseModel):
|
|
46
|
+
"""Detailed information about the material station."""
|
|
47
|
+
current_load_slot: int = Field(alias="currentLoadSlot", description="Currently loading slot ID (0 if none)")
|
|
48
|
+
current_slot: int = Field(alias="currentSlot", description="Currently active/printing slot ID (0 if none)")
|
|
49
|
+
slot_cnt: int = Field(alias="slotCnt", description="Total number of slots in the station")
|
|
50
|
+
slot_infos: list[SlotInfo] = Field(default_factory=list, alias="slotInfos", description="Array of information for each slot")
|
|
51
|
+
state_action: int = Field(alias="stateAction", description="Current action state of the material station")
|
|
52
|
+
state_step: int = Field(alias="stateStep", description="Current step within the state action")
|
|
53
|
+
|
|
54
|
+
class Config:
|
|
55
|
+
populate_by_name = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IndepMatlInfo(BaseModel):
|
|
59
|
+
"""Information related to independent material loading, often used when a single extruder printer has a material station."""
|
|
60
|
+
material_color: str = Field(alias="materialColor", description="Color of the material")
|
|
61
|
+
material_name: str = Field(alias="materialName", description="Name of the material (can be '?' if unknown)")
|
|
62
|
+
state_action: int = Field(alias="stateAction", description="Current action state")
|
|
63
|
+
state_step: int = Field(alias="stateStep", description="Current step within the state action")
|
|
64
|
+
|
|
65
|
+
class Config:
|
|
66
|
+
populate_by_name = True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FFGcodeToolData(BaseModel):
|
|
70
|
+
"""Represents data for a single tool/material used in a G-code file, typically part of a multi-material print."""
|
|
71
|
+
filament_weight: float = Field(alias="filamentWeight", description="Calculated filament weight for this tool/material in the print")
|
|
72
|
+
material_color: str = Field(alias="materialColor", description="Material color hex string (e.g., '#FFFF00')")
|
|
73
|
+
material_name: str = Field(alias="materialName", description="Name of the material (e.g., 'PLA')")
|
|
74
|
+
slot_id: int = Field(alias="slotId", description="Slot ID from the material station, if applicable (0 if not or direct)")
|
|
75
|
+
tool_id: int = Field(alias="toolId", description="Tool ID or extruder number")
|
|
76
|
+
|
|
77
|
+
class Config:
|
|
78
|
+
populate_by_name = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FFGcodeFileEntry(BaseModel):
|
|
82
|
+
"""Represents a single G-code file entry as returned by the /gcodeList endpoint, especially for printers like AD5X that provide detailed material info."""
|
|
83
|
+
gcode_file_name: str = Field(alias="gcodeFileName", description="The name of the G-code file (e.g., 'FISH_PLA.3mf')")
|
|
84
|
+
gcode_tool_cnt: Optional[int] = Field(default=None, alias="gcodeToolCnt", description="Number of tools/materials used in this G-code file")
|
|
85
|
+
gcode_tool_datas: Optional[list[FFGcodeToolData]] = Field(default=None, alias="gcodeToolDatas", description="Array of detailed information for each tool/material")
|
|
86
|
+
printing_time: int = Field(alias="printingTime", description="Estimated printing time in seconds")
|
|
87
|
+
total_filament_weight: Optional[float] = Field(default=None, alias="totalFilamentWeight", description="Total estimated filament weight for the print")
|
|
88
|
+
use_matl_station: Optional[bool] = Field(default=None, alias="useMatlStation", description="Indicates if the G-code file is intended for use with a material station")
|
|
89
|
+
|
|
90
|
+
class Config:
|
|
91
|
+
populate_by_name = True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FFPrinterDetail(BaseModel):
|
|
95
|
+
"""
|
|
96
|
+
Represents the raw detailed information about a FlashForge 3D printer as obtained from its API.
|
|
97
|
+
Properties are often in the printer's native naming format and may include string representations
|
|
98
|
+
of boolean states (e.g., "open", "close").
|
|
99
|
+
"""
|
|
100
|
+
auto_shutdown: Optional[str] = Field(default=None, alias="autoShutdown")
|
|
101
|
+
auto_shutdown_time: Optional[int] = Field(default=None, alias="autoShutdownTime")
|
|
102
|
+
camera_stream_url: Optional[str] = Field(default=None, alias="cameraStreamUrl")
|
|
103
|
+
chamber_fan_speed: Optional[int] = Field(default=None, alias="chamberFanSpeed")
|
|
104
|
+
chamber_target_temp: Optional[float] = Field(default=None, alias="chamberTargetTemp")
|
|
105
|
+
chamber_temp: Optional[float] = Field(default=None, alias="chamberTemp")
|
|
106
|
+
cooling_fan_speed: Optional[int] = Field(default=None, alias="coolingFanSpeed")
|
|
107
|
+
cooling_fan_left_speed: Optional[int] = Field(default=None, alias="coolingFanLeftSpeed")
|
|
108
|
+
cumulative_filament: Optional[float] = Field(default=None, alias="cumulativeFilament")
|
|
109
|
+
cumulative_print_time: Optional[int] = Field(default=None, alias="cumulativePrintTime")
|
|
110
|
+
current_print_speed: Optional[int] = Field(default=None, alias="currentPrintSpeed")
|
|
111
|
+
door_status: Optional[str] = Field(default=None, alias="doorStatus")
|
|
112
|
+
error_code: Optional[str] = Field(default=None, alias="errorCode")
|
|
113
|
+
estimated_left_len: Optional[float] = Field(default=None, alias="estimatedLeftLen")
|
|
114
|
+
estimated_left_weight: Optional[float] = Field(default=None, alias="estimatedLeftWeight")
|
|
115
|
+
estimated_right_len: Optional[float] = Field(default=None, alias="estimatedRightLen")
|
|
116
|
+
estimated_right_weight: Optional[float] = Field(default=None, alias="estimatedRightWeight")
|
|
117
|
+
estimated_time: Optional[int] = Field(default=None, alias="estimatedTime")
|
|
118
|
+
external_fan_status: Optional[str] = Field(default=None, alias="externalFanStatus")
|
|
119
|
+
fill_amount: Optional[float] = Field(default=None, alias="fillAmount")
|
|
120
|
+
firmware_version: Optional[str] = Field(default=None, alias="firmwareVersion")
|
|
121
|
+
flash_register_code: Optional[str] = Field(default=None, alias="flashRegisterCode")
|
|
122
|
+
has_matl_station: Optional[bool] = Field(default=None, alias="hasMatlStation")
|
|
123
|
+
matl_station_info: Optional[MatlStationInfo] = Field(default=None, alias="matlStationInfo")
|
|
124
|
+
indep_matl_info: Optional[IndepMatlInfo] = Field(default=None, alias="indepMatlInfo")
|
|
125
|
+
has_left_filament: Optional[bool] = Field(default=None, alias="hasLeftFilament")
|
|
126
|
+
has_right_filament: Optional[bool] = Field(default=None, alias="hasRightFilament")
|
|
127
|
+
internal_fan_status: Optional[str] = Field(default=None, alias="internalFanStatus")
|
|
128
|
+
ip_addr: Optional[str] = Field(default=None, alias="ipAddr")
|
|
129
|
+
left_filament_type: Optional[str] = Field(default=None, alias="leftFilamentType")
|
|
130
|
+
left_target_temp: Optional[float] = Field(default=None, alias="leftTargetTemp")
|
|
131
|
+
left_temp: Optional[float] = Field(default=None, alias="leftTemp")
|
|
132
|
+
light_status: Optional[str] = Field(default=None, alias="lightStatus")
|
|
133
|
+
location: Optional[str] = Field(default=None, alias="location")
|
|
134
|
+
mac_addr: Optional[str] = Field(default=None, alias="macAddr")
|
|
135
|
+
measure: Optional[str] = Field(default=None, alias="measure")
|
|
136
|
+
name: Optional[str] = Field(default=None, alias="name")
|
|
137
|
+
nozzle_cnt: Optional[int] = Field(default=None, alias="nozzleCnt")
|
|
138
|
+
nozzle_model: Optional[str] = Field(default=None, alias="nozzleModel")
|
|
139
|
+
nozzle_style: Optional[int] = Field(default=None, alias="nozzleStyle")
|
|
140
|
+
pid: Optional[int] = Field(default=None, alias="pid")
|
|
141
|
+
plat_target_temp: Optional[float] = Field(default=None, alias="platTargetTemp")
|
|
142
|
+
plat_temp: Optional[float] = Field(default=None, alias="platTemp")
|
|
143
|
+
polar_register_code: Optional[str] = Field(default=None, alias="polarRegisterCode")
|
|
144
|
+
print_duration: Optional[int] = Field(default=None, alias="printDuration")
|
|
145
|
+
print_file_name: Optional[str] = Field(default=None, alias="printFileName")
|
|
146
|
+
print_file_thumb_url: Optional[str] = Field(default=None, alias="printFileThumbUrl")
|
|
147
|
+
print_layer: Optional[int] = Field(default=None, alias="printLayer")
|
|
148
|
+
print_progress: Optional[float] = Field(default=None, alias="printProgress")
|
|
149
|
+
print_speed_adjust: Optional[int] = Field(default=None, alias="printSpeedAdjust")
|
|
150
|
+
remaining_disk_space: Optional[float] = Field(default=None, alias="remainingDiskSpace")
|
|
151
|
+
right_filament_type: Optional[str] = Field(default=None, alias="rightFilamentType")
|
|
152
|
+
right_target_temp: Optional[float] = Field(default=None, alias="rightTargetTemp")
|
|
153
|
+
right_temp: Optional[float] = Field(default=None, alias="rightTemp")
|
|
154
|
+
status: Optional[str] = Field(default=None, alias="status")
|
|
155
|
+
target_print_layer: Optional[int] = Field(default=None, alias="targetPrintLayer")
|
|
156
|
+
tvoc: Optional[float] = Field(default=None, alias="tvoc")
|
|
157
|
+
z_axis_compensation: Optional[float] = Field(default=None, alias="zAxisCompensation")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class FFMachineInfo(BaseModel):
|
|
161
|
+
"""
|
|
162
|
+
Represents a structured and user-friendly model of the printer's information and state.
|
|
163
|
+
This interface is populated by transforming data from FFPrinterDetail.
|
|
164
|
+
"""
|
|
165
|
+
# Auto-shutdown settings
|
|
166
|
+
auto_shutdown: bool = False
|
|
167
|
+
auto_shutdown_time: int = 0
|
|
168
|
+
|
|
169
|
+
# Camera
|
|
170
|
+
camera_stream_url: str = ""
|
|
171
|
+
|
|
172
|
+
# Fan speeds
|
|
173
|
+
chamber_fan_speed: int = 0
|
|
174
|
+
cooling_fan_speed: int = 0
|
|
175
|
+
cooling_fan_left_speed: Optional[int] = None
|
|
176
|
+
|
|
177
|
+
# Cumulative stats
|
|
178
|
+
cumulative_filament: float = 0.0
|
|
179
|
+
cumulative_print_time: int = 0
|
|
180
|
+
|
|
181
|
+
# Current print speed
|
|
182
|
+
current_print_speed: int = 0
|
|
183
|
+
|
|
184
|
+
# Disk space
|
|
185
|
+
free_disk_space: str = "0.00"
|
|
186
|
+
|
|
187
|
+
# Door and error status
|
|
188
|
+
door_open: bool = False
|
|
189
|
+
error_code: str = ""
|
|
190
|
+
|
|
191
|
+
# Current print estimates
|
|
192
|
+
est_length: float = 0.0
|
|
193
|
+
est_weight: float = 0.0
|
|
194
|
+
estimated_time: int = 0
|
|
195
|
+
|
|
196
|
+
# Fans & LED status
|
|
197
|
+
external_fan_on: bool = False
|
|
198
|
+
internal_fan_on: bool = False
|
|
199
|
+
lights_on: bool = False
|
|
200
|
+
|
|
201
|
+
# Network
|
|
202
|
+
ip_address: str = ""
|
|
203
|
+
mac_address: str = ""
|
|
204
|
+
|
|
205
|
+
# Print settings
|
|
206
|
+
fill_amount: float = 0.0
|
|
207
|
+
firmware_version: str = ""
|
|
208
|
+
name: str = ""
|
|
209
|
+
is_pro: bool = False
|
|
210
|
+
is_ad5x: bool = False
|
|
211
|
+
nozzle_size: str = ""
|
|
212
|
+
|
|
213
|
+
# Temperatures
|
|
214
|
+
print_bed: Temperature = Field(default_factory=Temperature)
|
|
215
|
+
extruder: Temperature = Field(default_factory=Temperature)
|
|
216
|
+
|
|
217
|
+
# Current print stats
|
|
218
|
+
print_duration: int = 0
|
|
219
|
+
print_file_name: str = ""
|
|
220
|
+
print_file_thumb_url: str = ""
|
|
221
|
+
current_print_layer: int = 0
|
|
222
|
+
print_progress: float = 0.0
|
|
223
|
+
print_progress_int: int = 0
|
|
224
|
+
print_speed_adjust: int = 0
|
|
225
|
+
filament_type: str = ""
|
|
226
|
+
|
|
227
|
+
# Machine state
|
|
228
|
+
machine_state: MachineState = MachineState.UNKNOWN
|
|
229
|
+
status: str = ""
|
|
230
|
+
total_print_layers: int = 0
|
|
231
|
+
tvoc: float = 0.0
|
|
232
|
+
z_axis_compensation: float = 0.0
|
|
233
|
+
|
|
234
|
+
# Cloud codes
|
|
235
|
+
flash_cloud_register_code: str = ""
|
|
236
|
+
polar_cloud_register_code: str = ""
|
|
237
|
+
|
|
238
|
+
# Extras
|
|
239
|
+
print_eta: str = "00:00"
|
|
240
|
+
completion_time: datetime = Field(default_factory=datetime.now)
|
|
241
|
+
formatted_run_time: str = "00:00"
|
|
242
|
+
formatted_total_run_time: str = "0h:0m"
|
|
243
|
+
|
|
244
|
+
# AD5X Material Station
|
|
245
|
+
has_matl_station: Optional[bool] = None
|
|
246
|
+
matl_station_info: Optional[MatlStationInfo] = None
|
|
247
|
+
indep_matl_info: Optional[IndepMatlInfo] = None
|