flashforge-python-api 1.1.0__py3-none-any.whl → 1.2.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.
@@ -22,7 +22,8 @@ LEGACY_PROTOCOL_SIZE = 140
22
22
 
23
23
  LEGACY_PRODUCT_IDS = {
24
24
  "Adventurer3": 0x0008,
25
- "Adventurer4": 0x001E,
25
+ "Adventurer4Lite": 0x0016,
26
+ "Adventurer4Pro": 0x001E,
26
27
  }
27
28
 
28
29
 
@@ -426,7 +427,7 @@ class PrinterDiscovery:
426
427
  return None
427
428
 
428
429
  def parse_modern_protocol(self, buffer: bytes, ip_address: str) -> DiscoveredPrinter:
429
- """Parse a modern 276-byte discovery response."""
430
+ """Parse a modern 276-byte (0x114) discovery response."""
430
431
  if len(buffer) < MODERN_PROTOCOL_SIZE:
431
432
  raise InvalidResponseError(len(buffer), ip_address)
432
433
 
@@ -434,10 +435,10 @@ class PrinterDiscovery:
434
435
  command_port = int.from_bytes(buffer[0x84:0x86], byteorder="big")
435
436
  vendor_id = int.from_bytes(buffer[0x86:0x88], byteorder="big")
436
437
  product_id = int.from_bytes(buffer[0x88:0x8A], byteorder="big")
438
+ status_code = int.from_bytes(buffer[0x8A:0x8C], byteorder="big")
437
439
  product_type = int.from_bytes(buffer[0x8C:0x8E], byteorder="big")
438
440
  event_port = int.from_bytes(buffer[0x8E:0x90], byteorder="big")
439
- status_code = int.from_bytes(buffer[0x90:0x92], byteorder="big")
440
- serial_number = buffer[0x92 : 0x92 + 130].decode("utf-8", errors="ignore").split("\x00", 1)[0]
441
+ serial_number = buffer[0x92 : 0x92 + 128].decode("utf-8", errors="ignore").split("\x00", 1)[0]
441
442
  model = self.detect_modern_model(name, product_type)
442
443
 
443
444
  return DiscoveredPrinter(
@@ -508,7 +509,10 @@ class PrinterDiscovery:
508
509
  if "ADVENTURER 3" in upper_name or "ADVENTURER3" in upper_name or "AD3" in upper_name:
509
510
  return PrinterModel.ADVENTURER_3
510
511
 
511
- if product_id == LEGACY_PRODUCT_IDS["Adventurer4"]:
512
+ if product_id in {
513
+ LEGACY_PRODUCT_IDS["Adventurer4Lite"],
514
+ LEGACY_PRODUCT_IDS["Adventurer4Pro"],
515
+ }:
512
516
  return PrinterModel.ADVENTURER_4
513
517
 
514
518
  if product_id == LEGACY_PRODUCT_IDS["Adventurer3"]:
@@ -199,23 +199,26 @@ class FFPrinterDetail(BaseModel):
199
199
  of boolean states (e.g., "open", "close").
200
200
  """
201
201
 
202
- model_config = ConfigDict(extra="forbid")
202
+ model_config = ConfigDict(extra="allow")
203
203
 
204
204
  auto_shutdown: str | None = Field(default=None, alias="autoShutdown")
205
205
  auto_shutdown_time: int | None = Field(default=None, ge=0, alias="autoShutdownTime")
206
+ camera: int | None = Field(default=None, ge=0, alias="camera")
206
207
  camera_stream_url: str | None = Field(default=None, alias="cameraStreamUrl")
207
208
  chamber_fan_speed: int | None = Field(default=None, ge=0, le=100, alias="chamberFanSpeed")
208
209
  chamber_target_temp: float | None = Field(
209
210
  default=None, ge=-50, le=500, alias="chamberTargetTemp"
210
211
  )
211
212
  chamber_temp: float | None = Field(default=None, ge=-50, le=500, alias="chamberTemp")
213
+ clear_fan_status: str | None = Field(default=None, alias="clearFanStatus")
212
214
  cooling_fan_speed: int | None = Field(default=None, ge=0, le=100, alias="coolingFanSpeed")
213
215
  cooling_fan_left_speed: int | None = Field(
214
216
  default=None, ge=0, le=100, alias="coolingFanLeftSpeed"
215
217
  )
218
+ coordinate: list[float] | None = Field(default=None, alias="coordinate")
216
219
  cumulative_filament: float | None = Field(default=None, ge=0, alias="cumulativeFilament")
217
220
  cumulative_print_time: int | None = Field(default=None, ge=0, alias="cumulativePrintTime")
218
- current_print_speed: int | None = Field(default=None, ge=0, le=200, alias="currentPrintSpeed")
221
+ current_print_speed: int | None = Field(default=None, ge=0, le=1000, alias="currentPrintSpeed")
219
222
  door_status: str | None = Field(default=None, alias="doorStatus")
220
223
  error_code: str | None = Field(default=None, alias="errorCode")
221
224
  estimated_left_len: float | None = Field(default=None, ge=0, alias="estimatedLeftLen")
@@ -223,6 +226,7 @@ class FFPrinterDetail(BaseModel):
223
226
  estimated_right_len: float | None = Field(default=None, ge=0, alias="estimatedRightLen")
224
227
  estimated_right_weight: float | None = Field(default=None, ge=0, alias="estimatedRightWeight")
225
228
  estimated_time: float | None = Field(default=None, ge=0, alias="estimatedTime")
229
+ extrude_ctrl: int | None = Field(default=None, ge=0, alias="extrudeCtrl")
226
230
  external_fan_status: str | None = Field(default=None, alias="externalFanStatus")
227
231
  fill_amount: float | None = Field(default=None, ge=0, le=100, alias="fillAmount")
228
232
  firmware_version: str | None = Field(default=None, alias="firmwareVersion")
@@ -241,6 +245,7 @@ class FFPrinterDetail(BaseModel):
241
245
  location: str | None = Field(default=None, alias="location")
242
246
  mac_addr: str | None = Field(default=None, alias="macAddr")
243
247
  measure: str | None = Field(default=None, alias="measure")
248
+ move_ctrl: int | None = Field(default=None, ge=0, alias="moveCtrl")
244
249
  name: str | None = Field(default=None, alias="name")
245
250
  nozzle_cnt: int | None = Field(default=None, ge=1, le=4, alias="nozzleCnt")
246
251
  nozzle_model: str | None = Field(default=None, alias="nozzleModel")
@@ -254,7 +259,7 @@ class FFPrinterDetail(BaseModel):
254
259
  print_file_thumb_url: str | None = Field(default=None, alias="printFileThumbUrl")
255
260
  print_layer: int | None = Field(default=None, ge=0, alias="printLayer")
256
261
  print_progress: float | None = Field(default=None, ge=0, le=100, alias="printProgress")
257
- print_speed_adjust: int | None = Field(default=None, ge=0, le=200, alias="printSpeedAdjust")
262
+ print_speed_adjust: int | None = Field(default=None, ge=0, le=1000, alias="printSpeedAdjust")
258
263
  remaining_disk_space: float | None = Field(default=None, ge=0, alias="remainingDiskSpace")
259
264
  right_filament_type: str | None = Field(default=None, alias="rightFilamentType")
260
265
  right_target_temp: float | None = Field(default=None, ge=-50, le=500, alias="rightTargetTemp")
@@ -6,6 +6,7 @@ for controlling FlashForge printers via their TCP API.
6
6
  """
7
7
 
8
8
  from .a3_client import A3BuildVolume, A3FileEntry, A3PrinterInfo, A3Thumbnail, FlashForgeA3Client
9
+ from .a4_client import A4BuildVolume, A4FileEntry, A4PrinterInfo, FlashForgeA4Client
9
10
  from .ff_client import FlashForgeClient
10
11
  from .gcode import A3GCodeController, GCodeController, GCodes
11
12
  from .parsers import (
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "FlashForgeTcpClientOptions",
29
30
  "FlashForgeClient",
30
31
  "FlashForgeA3Client",
32
+ "FlashForgeA4Client",
31
33
  "GCodes",
32
34
  "GCodeController",
33
35
  "A3GCodeController",
@@ -35,6 +37,9 @@ __all__ = [
35
37
  "A3PrinterInfo",
36
38
  "A3FileEntry",
37
39
  "A3Thumbnail",
40
+ "A4BuildVolume",
41
+ "A4PrinterInfo",
42
+ "A4FileEntry",
38
43
  "PrinterInfo",
39
44
  "TempInfo",
40
45
  "TempData",
@@ -9,6 +9,7 @@ import re
9
9
  from dataclasses import dataclass
10
10
  from typing import TYPE_CHECKING
11
11
 
12
+ from .gcode.gcodes import GCodes
12
13
  from .parsers import EndstopStatus, LocationInfo, PrintStatus, TempInfo
13
14
  from .tcp_client import FlashForgeTcpClient, FlashForgeTcpClientOptions
14
15
 
@@ -112,15 +113,15 @@ class FlashForgeA3Client(FlashForgeTcpClient):
112
113
  return self._normalize_a3_text_response(response)
113
114
 
114
115
  async def init_control(self) -> bool:
115
- """Initialize control by sending M601."""
116
+ """Initialize control by sending the legacy M601 S1 login command."""
116
117
  try:
117
118
  await asyncio.sleep(0.5)
118
- response = await self.send_command_async("~M601")
119
+ response = await self.send_command_async(GCodes.CMD_LOGIN)
119
120
  if response is None:
120
121
  return False
121
122
  if "Error: have been connected" in response:
122
123
  return True
123
- return self._is_successful_command_response("~M601", response)
124
+ return self._is_successful_command_response(GCodes.CMD_LOGIN, response)
124
125
  except Exception:
125
126
  return False
126
127
 
@@ -150,6 +151,8 @@ class FlashForgeA3Client(FlashForgeTcpClient):
150
151
  info.machine_name = line.replace("Machine Name:", "", 1).strip()
151
152
  elif line.startswith("Firmware:"):
152
153
  info.firmware = line.replace("Firmware:", "", 1).strip()
154
+ elif line.startswith("SN:"):
155
+ info.serial_number = line.replace("SN:", "", 1).strip()
153
156
  elif line.startswith("Serial Number:"):
154
157
  info.serial_number = line.replace("Serial Number:", "", 1).strip()
155
158
  elif line.startswith("Tool Count:"):
@@ -0,0 +1,300 @@
1
+ """
2
+ Adventurer 4 TCP client and typed helpers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Literal
11
+
12
+ from .gcode import GCodeController, GCodes
13
+ from .parsers import EndstopStatus, LocationInfo, PrintStatus, TempInfo, ThumbnailInfo
14
+ from .tcp_client import FlashForgeTcpClient, FlashForgeTcpClientOptions
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ A4PrinterVariant = Literal["lite", "pro", "unknown"]
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class A4BuildVolume:
23
+ """Build volume metadata reported by Adventurer 4 M115 replies."""
24
+
25
+ x: int
26
+ y: int
27
+ z: int
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class A4PrinterInfo:
32
+ """Structured Adventurer 4 printer info parsed from M115."""
33
+
34
+ machine_type: str
35
+ machine_name: str
36
+ firmware: str
37
+ serial_number: str | None
38
+ build_volume: A4BuildVolume
39
+ tool_count: int
40
+ mac_address: str
41
+ variant: A4PrinterVariant
42
+ raw: str
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class A4FileEntry:
47
+ """Normalized file entry returned by the Adventurer 4 TCP client."""
48
+
49
+ name: str
50
+ path: str
51
+
52
+
53
+ class FlashForgeA4Client(FlashForgeTcpClient):
54
+ """TCP client for FlashForge Adventurer 4 Lite and Pro printers."""
55
+
56
+ def __init__(self, hostname: str, options: FlashForgeTcpClientOptions | None = None) -> None:
57
+ super().__init__(hostname, options)
58
+ self._control = GCodeController(self)
59
+
60
+ def get_ip(self) -> str:
61
+ """Return the configured printer hostname or IP address."""
62
+ return self.hostname
63
+
64
+ def gcode(self) -> GCodeController:
65
+ """Return the shared G-code controller."""
66
+ return self._control
67
+
68
+ async def init_control(self) -> bool:
69
+ """Initialize control using the legacy M601 S1 login flow."""
70
+ try:
71
+ response = await self.send_command_async(GCodes.CMD_LOGIN)
72
+ if response is None:
73
+ logger.error("A4: failed to send M601 S1 login command")
74
+ return False
75
+
76
+ if "Error: have been connected" in response:
77
+ logger.warning("A4: already connected to printer")
78
+ return True
79
+
80
+ if not self._is_successful_command_response(GCodes.CMD_LOGIN, response):
81
+ return False
82
+
83
+ await asyncio.sleep(0.1)
84
+ info = await self.get_printer_info()
85
+ if not info:
86
+ logger.error("A4: failed to retrieve printer info after M601 S1")
87
+ return False
88
+
89
+ await self.start_keep_alive()
90
+ return True
91
+ except Exception as error:
92
+ logger.error("A4: init_control error: %s", error)
93
+ return False
94
+
95
+ async def led_on(self) -> bool:
96
+ return await self._control.led_on()
97
+
98
+ async def led_off(self) -> bool:
99
+ return await self._control.led_off()
100
+
101
+ async def pause_job(self) -> bool:
102
+ return await self._control.pause_job()
103
+
104
+ async def resume_job(self) -> bool:
105
+ return await self._control.resume_job()
106
+
107
+ async def stop_job(self) -> bool:
108
+ return await self._control.stop_job()
109
+
110
+ async def start_job(self, name: str) -> bool:
111
+ return await self._control.start_job(name)
112
+
113
+ async def home_axes(self) -> bool:
114
+ return await self._control.home()
115
+
116
+ async def rapid_home(self) -> bool:
117
+ return await self._control.rapid_home()
118
+
119
+ async def set_extruder_temp(self, temp: int, wait_for: bool = False) -> bool:
120
+ return await self._control.set_extruder_temp(temp, wait_for)
121
+
122
+ async def cancel_extruder_temp(self) -> bool:
123
+ return await self._control.cancel_extruder_temp()
124
+
125
+ async def set_bed_temp(self, temp: int, wait_for: bool = False) -> bool:
126
+ return await self._control.set_bed_temp(temp, wait_for)
127
+
128
+ async def cancel_bed_temp(self, wait_for_cool: bool = False) -> bool:
129
+ return await self._control.cancel_bed_temp(wait_for_cool)
130
+
131
+ async def extrude(self, length: float, feedrate: int = 450) -> bool:
132
+ return await self.send_cmd_ok(f"~G1 E{length} F{feedrate}")
133
+
134
+ async def move_extruder(self, x: float, y: float, feedrate: int) -> bool:
135
+ return await self.send_cmd_ok(f"~G1 X{x} Y{y} F{feedrate}")
136
+
137
+ async def move(self, x: float, y: float, z: float, feedrate: int) -> bool:
138
+ return await self.send_cmd_ok(f"~G1 X{x} Y{y} Z{z} F{feedrate}")
139
+
140
+ async def send_cmd_ok(self, cmd: str) -> bool:
141
+ """Send a command and apply Adventurer 4 success semantics."""
142
+ try:
143
+ response = await self.send_command_async(cmd)
144
+ if response is None:
145
+ return False
146
+ return self._is_successful_command_response(cmd, response)
147
+ except Exception as error:
148
+ logger.error("A4: send_cmd_ok failed for %s: %s", cmd, error)
149
+ return False
150
+
151
+ async def send_raw_cmd(self, cmd: str) -> str:
152
+ """Send a raw command and return the raw string response."""
153
+ if "M661" not in cmd:
154
+ response = await self.send_command_async(cmd)
155
+ return response or ""
156
+
157
+ files = await self.get_file_list_async()
158
+ return "\n".join(files)
159
+
160
+ async def get_printer_info(self) -> A4PrinterInfo | None:
161
+ """Get printer information using the documented Adventurer 4 M115 response."""
162
+ response = await self.send_command_async(GCodes.CMD_INFO_STATUS)
163
+ if response is None:
164
+ return None
165
+
166
+ normalized = self._normalize_a4_text_response(response)
167
+ lines = self._get_normalized_lines(normalized)
168
+ info = A4PrinterInfo(
169
+ machine_type="",
170
+ machine_name="",
171
+ firmware="",
172
+ serial_number=None,
173
+ build_volume=A4BuildVolume(x=220, y=200, z=250),
174
+ tool_count=1,
175
+ mac_address="",
176
+ variant="unknown",
177
+ raw=normalized,
178
+ )
179
+
180
+ for line in lines:
181
+ if line.startswith("Machine Type:"):
182
+ info.machine_type = line.replace("Machine Type:", "", 1).strip()
183
+ info.variant = self._detect_variant(info.machine_type)
184
+ elif line.startswith("Machine Name:"):
185
+ info.machine_name = line.replace("Machine Name:", "", 1).strip()
186
+ elif line.startswith("Firmware:"):
187
+ info.firmware = line.replace("Firmware:", "", 1).strip()
188
+ elif line.startswith("SN:"):
189
+ info.serial_number = line.replace("SN:", "", 1).strip()
190
+ elif line.startswith("Serial Number:"):
191
+ info.serial_number = line.replace("Serial Number:", "", 1).strip()
192
+ elif line.startswith("Tool Count:") or line.startswith("Tool count:"):
193
+ try:
194
+ info.tool_count = int(line.split(":", 1)[1].strip())
195
+ except (IndexError, ValueError):
196
+ info.tool_count = 1
197
+ elif line.startswith("Mac Address:"):
198
+ info.mac_address = line.replace("Mac Address:", "", 1).strip()
199
+ elif "X:" in line and "Y:" in line and "Z:" in line:
200
+ volume_tokens = line.replace(":", "").split()
201
+ try:
202
+ info.build_volume = A4BuildVolume(
203
+ x=int(volume_tokens[1]),
204
+ y=int(volume_tokens[3]),
205
+ z=int(volume_tokens[5]),
206
+ )
207
+ except (IndexError, ValueError):
208
+ pass
209
+
210
+ if not info.machine_type or not info.firmware:
211
+ return None
212
+
213
+ return info
214
+
215
+ async def get_temp_info(self) -> TempInfo | None:
216
+ response = await self.send_command_async(GCodes.CMD_TEMP)
217
+ if response:
218
+ return TempInfo().from_replay(self._normalize_a4_text_response(response))
219
+ return None
220
+
221
+ async def get_endstop_status(self) -> EndstopStatus | None:
222
+ response = await self.send_command_async(GCodes.CMD_ENDSTOP_INFO)
223
+ if response:
224
+ return EndstopStatus().from_replay(self._normalize_a4_text_response(response))
225
+ return None
226
+
227
+ async def get_endstop_info(self) -> EndstopStatus | None:
228
+ return await self.get_endstop_status()
229
+
230
+ async def get_print_status(self) -> PrintStatus | None:
231
+ response = await self.send_command_async(GCodes.CMD_PRINT_STATUS)
232
+ if response:
233
+ return PrintStatus().from_replay(self._normalize_a4_text_response(response))
234
+ return None
235
+
236
+ async def get_location_info(self) -> LocationInfo | None:
237
+ response = await self.send_command_async(GCodes.CMD_INFO_XYZAB)
238
+ if response:
239
+ return LocationInfo().from_replay(self._normalize_a4_text_response(response))
240
+ return None
241
+
242
+ async def list_files(self) -> list[A4FileEntry]:
243
+ files = await self.get_file_list_async()
244
+ entries: list[A4FileEntry] = []
245
+ for relative_path in files:
246
+ normalized_path = (
247
+ relative_path if relative_path.startswith("/data/") else f"/data/{relative_path}"
248
+ )
249
+ entries.append(
250
+ A4FileEntry(
251
+ name=normalized_path.split("/")[-1] or relative_path,
252
+ path=normalized_path,
253
+ )
254
+ )
255
+ return entries
256
+
257
+ async def get_thumbnail(self, file_name: str) -> ThumbnailInfo | None:
258
+ file_path = file_name if file_name.startswith("/data/") else f"/data/{file_name}"
259
+ response = await self.send_command_async(f"{GCodes.CMD_GET_THUMBNAIL} {file_path}")
260
+ if response:
261
+ return ThumbnailInfo().from_replay(response, file_name)
262
+ return None
263
+
264
+ def _normalize_a4_text_response(self, response: str) -> str:
265
+ normalized = response.replace("\r\n", "\n").replace("\r", "\n").rstrip()
266
+ lines = normalized.split("\n")
267
+ if lines:
268
+ lines[0] = lines[0].removeprefix("ack: ").removeprefix("echo: ")
269
+ normalized = "\n".join(lines).rstrip()
270
+
271
+ if normalized.startswith('"') and normalized.endswith('"'):
272
+ normalized = normalized[1:-1].rstrip()
273
+
274
+ return normalized
275
+
276
+ def _get_normalized_lines(self, response: str) -> list[str]:
277
+ return [line.strip() for line in self._normalize_a4_text_response(response).split("\n") if line.strip()]
278
+
279
+ def _detect_variant(self, machine_type: str) -> A4PrinterVariant:
280
+ normalized_type = machine_type.upper()
281
+ if "PRO" in normalized_type:
282
+ return "pro"
283
+ if "ADVENTURER 4" in normalized_type or "ADVENTURER4" in normalized_type:
284
+ return "lite"
285
+ return "unknown"
286
+
287
+ def _is_successful_command_response(self, cmd: str, response: str) -> bool:
288
+ normalized = self._normalize_a4_text_response(response).strip()
289
+ if not normalized:
290
+ return False
291
+
292
+ lowered = normalized.lower()
293
+ if "error:" in lowered or "control failed." in lowered:
294
+ return False
295
+
296
+ bare_cmd = cmd.strip().removeprefix("~").split(maxsplit=1)[0]
297
+ if bare_cmd == "M23":
298
+ return "File opened" in normalized or "ok" in lowered
299
+
300
+ return "ok" in lowered or "Received." in normalized or normalized.startswith(bare_cmd)
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: flashforge-python-api
3
+ Version: 1.2.1
4
+ Summary: A comprehensive Python library for controlling FlashForge 3D printers
5
+ Project-URL: Homepage, https://github.com/GhostTypes/ff-5mp-api-py
6
+ Project-URL: Documentation, https://github.com/GhostTypes/ff-5mp-api-py#readme
7
+ Project-URL: Repository, https://github.com/GhostTypes/ff-5mp-api-py.git
8
+ Project-URL: Issues, https://github.com/GhostTypes/ff-5mp-api-py/issues
9
+ Author-email: GhostTypes <notghosttypes@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: 3d-printer,api,async,control,flashforge,python
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: aiohttp>=3.8.0
24
+ Requires-Dist: netifaces>=0.11.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: requests>=2.31.0
27
+ Provides-Extra: all
28
+ Requires-Dist: black>=23.0.0; extra == 'all'
29
+ Requires-Dist: mypy>=1.0.0; extra == 'all'
30
+ Requires-Dist: pillow>=10.0.0; extra == 'all'
31
+ Requires-Dist: pre-commit>=3.0.0; extra == 'all'
32
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'all'
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'all'
34
+ Requires-Dist: pytest>=7.0.0; extra == 'all'
35
+ Requires-Dist: ruff>=0.1.0; extra == 'all'
36
+ Provides-Extra: dev
37
+ Requires-Dist: black>=23.0.0; extra == 'dev'
38
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
39
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
40
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
41
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
42
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
43
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
44
+ Provides-Extra: imaging
45
+ Requires-Dist: pillow>=10.0.0; extra == 'imaging'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # FlashForge Python API
49
+
50
+ Python library for controlling FlashForge 3D printers with async support for the modern HTTP API and the legacy TCP protocol.
51
+
52
+ ## Supported Printers
53
+
54
+ | Printer | Support |
55
+ | --- | --- |
56
+ | Adventurer 5M | Full |
57
+ | Adventurer 5M Pro | Full |
58
+ | AD5X | Full |
59
+ | Adventurer 3 / 4 | Dedicated TCP clients |
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install flashforge-python-api
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ Modern LAN-mode HTTP printers require:
70
+
71
+ - printer IP address
72
+ - serial number
73
+ - check code
74
+
75
+ ```python
76
+ import asyncio
77
+ from flashforge import FlashForgeClient, PrinterDiscovery
78
+
79
+
80
+ async def main():
81
+ discovery = PrinterDiscovery()
82
+ printers = await discovery.discover()
83
+
84
+ if not printers:
85
+ print("No printers found")
86
+ return
87
+
88
+ printer = printers[0]
89
+
90
+ async with FlashForgeClient(
91
+ printer.ip_address,
92
+ printer.serial_number or "SERIAL_NUMBER",
93
+ "CHECK_CODE",
94
+ ) as client:
95
+ if not await client.initialize():
96
+ return
97
+
98
+ await client.init_control()
99
+
100
+ status = await client.get_printer_status()
101
+ print(f"Printer: {client.printer_name}")
102
+ print(f"State: {status.machine_state if status else 'unknown'}")
103
+
104
+ await client.control.home_axes()
105
+
106
+
107
+ asyncio.run(main())
108
+ ```
109
+
110
+ ## Main Entry Points
111
+
112
+ - `FlashForgeClient`: primary client for modern printers
113
+ - `PrinterDiscovery`: recommended discovery API
114
+ - `FlashForgeA4Client`: documented TCP client for Adventurer 4 Lite / Pro printers
115
+ - `FlashForgeA3Client`: documented TCP client for Adventurer 3 printers
116
+ - `FlashForgeTcpClient` and `client.tcp_client`: lower-level TCP access for direct commands and generic legacy workflows
117
+
118
+ ## Capabilities
119
+
120
+ - printer discovery
121
+ - printer status and machine information
122
+ - job control
123
+ - file listing, uploads, and thumbnails
124
+ - temperature and motion control
125
+ - LED, camera, and filtration control where supported
126
+ - AD5X-specific job and material-station support
127
+
128
+ ## Documentation
129
+
130
+ - [docs/README.md](docs/README.md)
131
+ - [docs/client.md](docs/client.md)
132
+ - [docs/protocols.md](docs/protocols.md)
133
+ - [docs/api_reference.md](docs/api_reference.md)
@@ -1,14 +1,14 @@
1
- flashforge/__init__.py,sha256=k67UUBpSPMCRlexqn43RRHIh8XyNb9Bq5yXOpwChsFo,5170
2
- flashforge/client.py,sha256=XP_l5FsaUXKWjkRMWxbz7F1d7_8Xdh7oD5zpThtBFhM,15462
1
+ flashforge/__init__.py,sha256=3fmBeEJ4vFJDSsOQV0Yixb1yLyUw8nHDZKQHbXIbtmo,5336
2
+ flashforge/client.py,sha256=lplKJxNOLhUJMusm025cj7rZvWRAiv2j5qENdBIctuA,16920
3
3
  flashforge/api/__init__.py,sha256=vQz-DkG6LTH39bI4fyiUc0D9jQzemLh45pORLptSOlg,335
4
4
  flashforge/api/constants/__init__.py,sha256=Q0HL2tqSBYPd4Oz49VHLS3qUvRuv__GCvTGecaLrQ-Y,163
5
5
  flashforge/api/constants/commands.py,sha256=S9cBgU3pBXQm-dp3Bv4795NL059pmTY1NtxWzPA5gWU,329
6
- flashforge/api/constants/endpoints.py,sha256=3CSCvilBJ1uAM_dZiY5NgbYl4rub1RsZZRpCBRZCqH4,271
6
+ flashforge/api/constants/endpoints.py,sha256=zU_ONPyLUATr1KPlk0x8SLrK4ojs4QiiWr1-WwLVwTY,338
7
7
  flashforge/api/controls/__init__.py,sha256=53s-H25Pjwr0kvPk8MZ2Twy8VHGqGcO6Q6uBphhEOh4,293
8
- flashforge/api/controls/control.py,sha256=hifFWRPs2fRvNaJMSLgDJ4ytkHrEMGqzgCazuAC6ewA,11787
9
- flashforge/api/controls/files.py,sha256=fFjxOCVbK7q1P4GM9RcgB2tsJsywT_HiTBcL3-U0_Yo,7749
10
- flashforge/api/controls/info.py,sha256=r_gi20ZlgpdtCJVqtoyncV714-MQcDjg4Qg4xeTWHNA,12969
11
- flashforge/api/controls/job_control.py,sha256=uhuQE3weXJPvWBER0P7lvqWEzAVHXFNXWWBFfnZwtR8,23656
8
+ flashforge/api/controls/control.py,sha256=8wK2YOGf9NphWXsjIDapyQEfx-qGZgytXZFsjKeWvSA,11291
9
+ flashforge/api/controls/files.py,sha256=8T9kzgss_2DYFL4WVuEFj8W9sOPPPMIMq3dTq1d551w,6560
10
+ flashforge/api/controls/info.py,sha256=KVExwaeDd1SYiSOBZVVtif3Rf_cJkw4Bizhgm6dbAS8,12398
11
+ flashforge/api/controls/job_control.py,sha256=-DglR97DWBzl45mmqGkauYpUM_I-C0tXrbp9SKPYOVs,21814
12
12
  flashforge/api/controls/temp_control.py,sha256=TGkX63WHygVA9DeYfLARbktZ0o-bYX6MNyYOX6V_RB4,3034
13
13
  flashforge/api/filament/__init__.py,sha256=isT2dl0hzUS0xMTXMm4Tip0GTG1gCsm8LKWa9xPu4Y8,104
14
14
  flashforge/api/filament/filament.py,sha256=TtcC2G2nD9Sq4cOrCZzFLZ6Q0Ve-zfrfuEF9uQ8M3eE,1214
@@ -17,14 +17,15 @@ flashforge/api/misc/scientific_notation.py,sha256=KSMTrrYhPWLv0gbcEAD6u-jEOcIwUj
17
17
  flashforge/api/misc/temperature.py,sha256=qeHlCdPzLyqGDEB874Ib5AgYHMR9Qtw61shnw4G095E,1066
18
18
  flashforge/api/network/__init__.py,sha256=G5fBoAlq-NQPDkWp579mFIqsj0v7B1Lb2TAzO6IZA0Q,164
19
19
  flashforge/api/network/fnet_code.py,sha256=BR2niVKKk4ZfXtizUDIMXzDhlCxPHzqWg5AQQIcoj3g,402
20
- flashforge/api/network/utils.py,sha256=jwqyJaByr4q80_MJOTwS0nX8I8FZ54T_CHBwkKTXptA,1519
20
+ flashforge/api/network/utils.py,sha256=hh-9SGI61ii0CRkLVnAp-Eq2E0a9PAKPyNLMGaYPNsI,2065
21
21
  flashforge/discovery/__init__.py,sha256=G0WiP70EhfHdjXR1RNlv6xjhyHOdo-HWyOm9J0ds4SM,828
22
- flashforge/discovery/discovery.py,sha256=qlyLxITY9JMKY_quONdWeZ5sbsF_pXfqRXqk7DwDGfs,26042
22
+ flashforge/discovery/discovery.py,sha256=lNyD-nDDdYKV23kUOQpuA5ZX4r9T7_Zk3SzXoUwgMPI,26163
23
23
  flashforge/models/__init__.py,sha256=MNpnXdS6Yah5kSwBB7cbA2Hd-8azffE62pgUrrKSZk4,984
24
- flashforge/models/machine_info.py,sha256=tMec0qSq9iTVfUvLl72vL5jlDrBW2mn132dzL8842mA,14602
24
+ flashforge/models/machine_info.py,sha256=t7TJk03PG8kf-g6XnWd3NiWuCVLY1Wuod-aI4zdXu4Y,14976
25
25
  flashforge/models/responses.py,sha256=befMeXAc7XXiK7emF8Ep2cd1GuhZ-zyYPbbPqS2lcQI,7214
26
- flashforge/tcp/__init__.py,sha256=g0xAZf0ZDKnH2g79tK5FeAATRo90GfDB7Lkws1LsZxU,1145
27
- flashforge/tcp/a3_client.py,sha256=n7Kw0Dn9MYjjj0r2GPEiQFBKZfzuE9lboziuRt2bKgM,15411
26
+ flashforge/tcp/__init__.py,sha256=hpnqoWHeRtTwJPzdsVlwGt1njKbAkrgIHKADkLSIRec,1317
27
+ flashforge/tcp/a3_client.py,sha256=a1jdDcBnTSNrzJFKTuVV13fSLbZmxKEZ94n3cGf3wOA,15603
28
+ flashforge/tcp/a4_client.py,sha256=0KH5zfsbWaPRd7xSKb7YA3oZTkCVdsdcTA9LPl3S55Q,11167
28
29
  flashforge/tcp/ff_client.py,sha256=CJXk0Qei2FVSpbXxZhiEQgAEcTJaBab1ouooqyC4OL4,20235
29
30
  flashforge/tcp/tcp_client.py,sha256=KjymxUXiXgEp9y7mjr5tNO_Y2-bsGEyP-oG5en7rCCs,16996
30
31
  flashforge/tcp/gcode/__init__.py,sha256=tFGlqCPFxVJNizXsIG4pMOK7IPFNnc_vIEMciULYkMY,271
@@ -38,8 +39,8 @@ flashforge/tcp/parsers/print_status.py,sha256=4q9ZlJfO0c7_t1o15tgxbguMxpDjEW9pMy
38
39
  flashforge/tcp/parsers/printer_info.py,sha256=QY6LECfcOhVHSIdsvQZwJago0OjXroUwiX0H-uQaH2A,5411
39
40
  flashforge/tcp/parsers/temp_info.py,sha256=9Waf68tt-4QBcChyUwfMHDwWdIzM4mmDpZAKeAzklgs,7908
40
41
  flashforge/tcp/parsers/thumbnail_info.py,sha256=ZmxdEbVFOzqR1AfhXXZyjENVI66BdRdE7Q8LQ8an9FY,10201
41
- flashforge_python_api-1.1.0.dist-info/METADATA,sha256=H9Lv3ThMilNw6H-BMtsi2-vdsL0u6f4u7QtGkh-6vlQ,11038
42
- flashforge_python_api-1.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
43
- flashforge_python_api-1.1.0.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
44
- flashforge_python_api-1.1.0.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
45
- flashforge_python_api-1.1.0.dist-info/RECORD,,
42
+ flashforge_python_api-1.2.1.dist-info/METADATA,sha256=eMFS4ENMi13-7DADIKwTNTSQyBoFjHrGrLMb_gw4dZw,4194
43
+ flashforge_python_api-1.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
44
+ flashforge_python_api-1.2.1.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
45
+ flashforge_python_api-1.2.1.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
46
+ flashforge_python_api-1.2.1.dist-info/RECORD,,