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.
Files changed (43) hide show
  1. flashforge/__init__.py +188 -0
  2. flashforge/api/__init__.py +17 -0
  3. flashforge/api/constants/__init__.py +10 -0
  4. flashforge/api/constants/commands.py +10 -0
  5. flashforge/api/constants/endpoints.py +11 -0
  6. flashforge/api/controls/__init__.py +16 -0
  7. flashforge/api/controls/control.py +357 -0
  8. flashforge/api/controls/files.py +171 -0
  9. flashforge/api/controls/info.py +292 -0
  10. flashforge/api/controls/job_control.py +561 -0
  11. flashforge/api/controls/temp_control.py +89 -0
  12. flashforge/api/filament/__init__.py +7 -0
  13. flashforge/api/filament/filament.py +39 -0
  14. flashforge/api/misc/__init__.py +8 -0
  15. flashforge/api/misc/scientific_notation.py +28 -0
  16. flashforge/api/misc/temperature.py +42 -0
  17. flashforge/api/network/__init__.py +10 -0
  18. flashforge/api/network/fnet_code.py +15 -0
  19. flashforge/api/network/utils.py +55 -0
  20. flashforge/client.py +381 -0
  21. flashforge/discovery/__init__.py +12 -0
  22. flashforge/discovery/discovery.py +388 -0
  23. flashforge/models/__init__.py +50 -0
  24. flashforge/models/machine_info.py +247 -0
  25. flashforge/models/responses.py +123 -0
  26. flashforge/tcp/__init__.py +41 -0
  27. flashforge/tcp/ff_client.py +587 -0
  28. flashforge/tcp/gcode/__init__.py +11 -0
  29. flashforge/tcp/gcode/gcode_controller.py +296 -0
  30. flashforge/tcp/gcode/gcodes.py +93 -0
  31. flashforge/tcp/parsers/__init__.py +27 -0
  32. flashforge/tcp/parsers/endstop_status.py +273 -0
  33. flashforge/tcp/parsers/location_info.py +68 -0
  34. flashforge/tcp/parsers/print_status.py +182 -0
  35. flashforge/tcp/parsers/printer_info.py +154 -0
  36. flashforge/tcp/parsers/temp_info.py +217 -0
  37. flashforge/tcp/parsers/thumbnail_info.py +242 -0
  38. flashforge/tcp/tcp_client.py +448 -0
  39. flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
  40. flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
  41. flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
  42. flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
  43. 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