flashforge-python-api 1.1.0__py3-none-any.whl → 1.2.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 CHANGED
@@ -99,9 +99,13 @@ from .tcp import (
99
99
  A3GCodeController,
100
100
  A3PrinterInfo,
101
101
  A3Thumbnail,
102
+ A4BuildVolume,
103
+ A4FileEntry,
104
+ A4PrinterInfo,
102
105
  Endstop,
103
106
  EndstopStatus,
104
107
  FlashForgeA3Client,
108
+ FlashForgeA4Client,
105
109
  FlashForgeTcpClient,
106
110
  FlashForgeTcpClientOptions,
107
111
  GCodeController,
@@ -123,7 +127,7 @@ from .tcp import (
123
127
  )
124
128
 
125
129
  FiveMClient = FlashForgeClient
126
- __version__ = "1.1.0"
130
+ __version__ = "1.1.1"
127
131
  __author__ = "FlashForge Python API Contributors"
128
132
  __email__ = "notghosttypes@gmail.com"
129
133
  __description__ = "Python library for controlling FlashForge 3D printers"
@@ -167,6 +171,7 @@ __all__ = [
167
171
  "FlashForgeTcpClient",
168
172
  "FlashForgeTcpClientOptions",
169
173
  "FlashForgeA3Client",
174
+ "FlashForgeA4Client",
170
175
  "PrinterInfo",
171
176
  "TempInfo",
172
177
  "TempData",
@@ -185,6 +190,9 @@ __all__ = [
185
190
  "A3PrinterInfo",
186
191
  "A3FileEntry",
187
192
  "A3Thumbnail",
193
+ "A4BuildVolume",
194
+ "A4PrinterInfo",
195
+ "A4FileEntry",
188
196
  # Discovery classes
189
197
  "PrinterDiscovery",
190
198
  "DiscoveredPrinter",
@@ -1,3 +1,9 @@
1
+ from typing import Final
2
+
3
+
4
+ CAMERA_STREAM_PORT: Final[int] = 8080
5
+
6
+
1
7
  class Endpoints:
2
8
  """
3
9
  Endpoints for the "new" HTTP API
flashforge/client.py CHANGED
@@ -10,7 +10,7 @@ from dataclasses import dataclass
10
10
 
11
11
  import aiohttp
12
12
 
13
- from .api.constants.endpoints import Endpoints
13
+ from .api.constants.endpoints import CAMERA_STREAM_PORT, Endpoints
14
14
  from .api.controls import Control, Files, Info, JobControl, TempControl
15
15
  from .api.controls.info import MachineInfoParser
16
16
  from .api.network.utils import NetworkUtils
@@ -221,6 +221,59 @@ class FlashForgeClient:
221
221
 
222
222
  self.camera_stream_url = ""
223
223
 
224
+ async def detect_camera_stream(self, timeout_ms: int = 3000) -> str:
225
+ """
226
+ Probes the printer's known OEM camera endpoint.
227
+
228
+ Falls back to a short GET when HEAD is unsupported so MJPEG servers
229
+ that reject HEAD requests are still detected.
230
+
231
+ Args:
232
+ timeout_ms: Timeout for each probe attempt in milliseconds.
233
+
234
+ Returns:
235
+ The working camera stream URL, or an empty string if not detected.
236
+ """
237
+ probe_url = f"http://{self.ip_address}:{CAMERA_STREAM_PORT}/?action=stream"
238
+ timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000)
239
+
240
+ if await self._probe_camera_stream_with_method("head", probe_url, timeout):
241
+ return probe_url
242
+
243
+ if await self._probe_camera_stream_with_method("get", probe_url, timeout):
244
+ return probe_url
245
+
246
+ return ""
247
+
248
+ async def _probe_camera_stream_with_method(
249
+ self,
250
+ method: str,
251
+ probe_url: str,
252
+ timeout: aiohttp.ClientTimeout,
253
+ ) -> bool:
254
+ try:
255
+ session = await self._ensure_http_session()
256
+ request = session.head if method == "head" else session.get
257
+
258
+ async with request(probe_url, timeout=timeout) as response:
259
+ return self._is_valid_camera_probe_response(
260
+ response.status, response.headers.get("Content-Type", "")
261
+ )
262
+ except Exception:
263
+ return False
264
+
265
+ @staticmethod
266
+ def _is_valid_camera_probe_response(status: int, content_type: str | None) -> bool:
267
+ if status != 200:
268
+ return False
269
+
270
+ normalized_content_type = (content_type or "").lower()
271
+ return (
272
+ normalized_content_type == ""
273
+ or "multipart" in normalized_content_type
274
+ or "video/x-mjpeg" in normalized_content_type
275
+ )
276
+
224
277
  def cache_details(self, info: FFMachineInfo | None) -> bool:
225
278
  """
226
279
  Caches machine details from the provided FFMachineInfo object.
@@ -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,20 +199,23 @@ 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
221
  current_print_speed: int | None = Field(default=None, ge=0, le=200, alias="currentPrintSpeed")
@@ -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")
@@ -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.0
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,9 +1,9 @@
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=lmkPfkhcfDslbROv6XouwTIHshz4rHS0wNNJeicBHiA,17334
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
8
  flashforge/api/controls/control.py,sha256=hifFWRPs2fRvNaJMSLgDJ4ytkHrEMGqzgCazuAC6ewA,11787
9
9
  flashforge/api/controls/files.py,sha256=fFjxOCVbK7q1P4GM9RcgB2tsJsywT_HiTBcL3-U0_Yo,7749
@@ -19,12 +19,13 @@ flashforge/api/network/__init__.py,sha256=G5fBoAlq-NQPDkWp579mFIqsj0v7B1Lb2TAzO6
19
19
  flashforge/api/network/fnet_code.py,sha256=BR2niVKKk4ZfXtizUDIMXzDhlCxPHzqWg5AQQIcoj3g,402
20
20
  flashforge/api/network/utils.py,sha256=jwqyJaByr4q80_MJOTwS0nX8I8FZ54T_CHBwkKTXptA,1519
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=BvIaefjpApsbSVQA8eOG3NSm2L1ltZ1kcHH1118frZc,14974
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.0.dist-info/METADATA,sha256=UYZ0LVlLhqxWzMWv0-IjO35VefbUEBBqN3NZhK21uD4,4194
43
+ flashforge_python_api-1.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
44
+ flashforge_python_api-1.2.0.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
45
+ flashforge_python_api-1.2.0.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
46
+ flashforge_python_api-1.2.0.dist-info/RECORD,,
@@ -1,305 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: flashforge-python-api
3
- Version: 1.1.0
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
- <div align="center">
49
-
50
- # FlashForge Python API
51
-
52
- **A comprehensive Python library for controlling FlashForge 3D printers**
53
-
54
- ![PyPI](https://img.shields.io/pypi/v/flashforge-python-api?style=flat&color=3776ab) ![Python](https://img.shields.io/badge/Python-3.11%2B-3776ab?style=flat) ![License](https://img.shields.io/badge/License-MIT-brightgreen?style=flat) ![Status](https://img.shields.io/badge/Status-Stable-brightgreen?style=flat)
55
-
56
- ![aiohttp](https://img.shields.io/badge/aiohttp-%E2%89%A53.8.0-blue?style=flat) ![pydantic](https://img.shields.io/badge/pydantic-%E2%89%A52.0.0-e92063?style=flat) ![netifaces](https://img.shields.io/badge/netifaces-%E2%89%A50.11.0-orange?style=flat) ![requests](https://img.shields.io/badge/requests-%E2%89%A52.31.0-blue?style=flat) ![pillow](https://img.shields.io/badge/pillow-%E2%89%A510.0.0-yellow?style=flat)
57
-
58
- **Dual-protocol support with modern async/await architecture for seamless printer control and monitoring**
59
-
60
- </div>
61
-
62
- ---
63
-
64
- <div align="center">
65
-
66
- ## Features
67
-
68
- | Capability | Details |
69
- | --- | --- |
70
- | **Dual Protocol Support** | Modern HTTP REST API for Adventurer 5M/5X series • Legacy TCP G-code protocol for all networked FlashForge printers |
71
- | **Printer Discovery** | Automatic UDP broadcast discovery • Returns printer name, serial number, and IP address |
72
- | **Full Control** | Movement and homing • Temperature control • Fan speed adjustment • LED lighting • Camera control • Air filtration system |
73
- | **Real-time Monitoring** | Printer status and machine state • Current and target temperatures • Print progress and layer tracking • Estimated time remaining |
74
- | **Job Management** | Start, pause, resume, and cancel print jobs • Progress monitoring |
75
- | **File Operations** | List, upload, and download files • Extract print thumbnails • File metadata retrieval |
76
- | **Async Architecture** | Native async/await implementation • Non-blocking network operations • Concurrent operations support |
77
- | **Type Safety** | Full type hints for IDE autocomplete • Pydantic models for data validation • mypy strict mode compatible |
78
- | **Model Detection** | Automatic capability detection • Runtime camera stream detection via `camera_stream_url` • Graceful degradation for older models |
79
-
80
- </div>
81
-
82
- ---
83
-
84
- <div align="center">
85
-
86
- ## Printer Testing & Coverage
87
-
88
- | Printer Model | Support Status | Testing Status | API Type |
89
- | --- | --- | --- | --- |
90
- | **AD5X** | Full | Tested | HTTP (New) + TCP |
91
- | **Adventurer 5M/Pro** | Full | Tested | HTTP (New) + TCP |
92
- | **Adventurer 3/4** | Partial | Partial | TCP (Legacy) |
93
-
94
- </div>
95
-
96
- ---
97
-
98
- <div align="center">
99
-
100
- ## Installation
101
-
102
- | Method | Command |
103
- | --- | --- |
104
- | **PyPI (Recommended)** | `pip install flashforge-python-api` |
105
- | **Development Install** | `pip install -e ".[dev]"` |
106
- | **With Imaging Support** | `pip install flashforge-python-api[imaging]` |
107
- | **All Optional Dependencies** | `pip install flashforge-python-api[all]` |
108
-
109
- </div>
110
-
111
- ---
112
-
113
- ## Quick Start
114
-
115
- <div align="center">
116
-
117
- **Important: LAN-Only Mode Required**
118
-
119
- Your printer must be in **LAN-only mode** to communicate with this library. See the [official FlashForge guide](https://wiki.flashforge.com/en/Orca-Flashforge-and-Flashmaker/orca-flashforge-quick-start-guide#connect-via-lan-only-mode) for setup instructions and to obtain your check code.
120
-
121
- </div>
122
-
123
- ### Printer Discovery
124
-
125
- Discover FlashForge printers on your local network automatically:
126
-
127
- ```python
128
- from flashforge import FlashForgePrinterDiscovery
129
- import asyncio
130
-
131
- async def discover():
132
- discovery = FlashForgePrinterDiscovery()
133
- printers = await discovery.discover_printers_async()
134
-
135
- for printer in printers:
136
- print(f"Found: {printer.name} at {printer.ip_address}")
137
- print(f"Serial: {printer.serial_number}")
138
-
139
- asyncio.run(discover())
140
- ```
141
-
142
- ### Basic Printer Control
143
-
144
- Connect to a printer and perform basic operations:
145
-
146
- ```python
147
- from flashforge import FlashForgeClient
148
- import asyncio
149
-
150
- async def control_printer():
151
- # Initialize client with printer credentials
152
- client = FlashForgeClient("192.168.1.100", "SERIAL_NUMBER", "CHECK_CODE")
153
-
154
- # Always initialize before operations
155
- if await client.initialize():
156
- print(f"Connected to {client.printer_name}")
157
- print(f"Firmware: {client.firmware_version}")
158
-
159
- # Set temperatures
160
- await client.temp_control.set_bed_temp(60)
161
- await client.temp_control.set_extruder_temp(220)
162
-
163
- # Home all axes
164
- await client.control.home_xyz()
165
-
166
- # Turn on LED lights (AD5M/5X only)
167
- if client.is_ad5x:
168
- await client.control.set_led_on()
169
-
170
- # Clean up
171
- await client.dispose()
172
-
173
- asyncio.run(control_printer())
174
- ```
175
-
176
- ### Real-time Status Monitoring
177
-
178
- Monitor printer status, temperatures, and print progress:
179
-
180
- ```python
181
- from flashforge import FlashForgeClient
182
- import asyncio
183
-
184
- async def monitor_printer():
185
- async with FlashForgeClient("192.168.1.100", "SERIAL", "CODE") as client:
186
- # Get comprehensive status via HTTP
187
- status = await client.get_printer_status()
188
- print(f"State: {status.machine_state}")
189
- print(f"Progress: {status.print_progress}%")
190
-
191
- # Get real-time temperatures via TCP
192
- temps = await client.tcp_client.get_temp_info()
193
- if temps:
194
- bed = temps.get_bed_temp()
195
- extruder = temps.get_extruder_temp()
196
- print(f"Bed: {bed.get_current()}°C / {bed.get_target()}°C")
197
- print(f"Extruder: {extruder.get_current()}°C / {extruder.get_target()}°C")
198
-
199
- # Check print progress via TCP
200
- layer_p, sd_p, current_layer = await client.tcp_client.get_print_progress()
201
- print(f"Layer Progress: {layer_p}% (Layer {current_layer})")
202
-
203
- asyncio.run(monitor_printer())
204
- ```
205
-
206
- ### Camera Stream Detection
207
-
208
- Use the printer-reported runtime camera stream URL as the OEM camera source of truth:
209
-
210
- ```python
211
- from flashforge import FlashForgeClient
212
- import asyncio
213
-
214
- async def check_camera():
215
- async with FlashForgeClient("192.168.1.100", "SERIAL", "CODE") as client:
216
- if not await client.initialize():
217
- return
218
-
219
- if client.camera_stream_url:
220
- print(f"OEM camera stream: {client.camera_stream_url}")
221
- else:
222
- print("Printer is not reporting an active OEM camera stream")
223
-
224
- # Camera power control remains Pro-only.
225
- if client.is_pro:
226
- await client.control.turn_camera_on()
227
-
228
- asyncio.run(check_camera())
229
- ```
230
-
231
- ### File Operations and Thumbnails
232
-
233
- List files and extract G-code thumbnails:
234
-
235
- ```python
236
- from flashforge import FlashForgeClient
237
- import asyncio
238
-
239
- async def file_operations():
240
- async with FlashForgeClient("192.168.1.100", "SERIAL", "CODE") as client:
241
- # List all files on printer
242
- files = await client.files.get_file_list()
243
- print(f"Found {len(files)} files")
244
-
245
- for filename in files:
246
- print(f"\nFile: {filename}")
247
-
248
- # Extract thumbnail image
249
- thumb = await client.tcp_client.get_thumbnail(filename)
250
- if thumb and thumb.has_image_data():
251
- print(f"Thumbnail: {len(thumb.get_image_bytes())} bytes")
252
-
253
- # Save thumbnail to disk
254
- thumb.save_to_file_sync(f"{filename}.png")
255
- print(f"Saved thumbnail as {filename}.png")
256
-
257
- asyncio.run(file_operations())
258
- ```
259
-
260
- ---
261
-
262
- <div align="center">
263
-
264
- ## Documentation
265
-
266
- | Resource | Description |
267
- | --- | --- |
268
- | **[Client API Reference](docs/client.md)** | Complete API reference for `FlashForgeClient` and all control modules |
269
- | **[Data Models](docs/models.md)** | Pydantic model documentation for status objects and responses |
270
- | **[Protocols (HTTP/TCP)](docs/protocols.md)** | Understanding the dual-protocol architecture and when to use each |
271
- | **[Advanced Usage](docs/advanced.md)** | Async patterns, error handling, concurrent operations, and best practices |
272
- | **[Complete API Reference](docs/api_reference.md)** | Full class hierarchy and method listing |
273
-
274
- </div>
275
-
276
- ---
277
-
278
- <div align="center">
279
-
280
- ## Development
281
-
282
- | Task | Command | Description |
283
- | --- | --- | --- |
284
- | **Setup Environment** | `python -m venv .venv && .venv\Scripts\activate` | Create and activate virtual environment |
285
- | **Install Dependencies** | `pip install -e ".[dev]"` | Install package with development tools |
286
- | **Run Tests** | `pytest` | Execute test suite |
287
- | **Type Check** | `mypy flashforge/` | Run strict type checking with mypy |
288
- | **Format Code** | `black flashforge/ tests/` | Format code with Black (line length: 100) |
289
- | **Lint** | `ruff check flashforge/ tests/` | Lint code with Ruff |
290
- | **Coverage Report** | `pytest --cov=flashforge --cov-report=html` | Generate test coverage report |
291
- | **Build Package** | `python -m build` | Build distribution packages |
292
- | **Run Pre-commit** | `pre-commit run --all-files` | Execute all pre-commit hooks |
293
-
294
- </div>
295
-
296
- ---
297
-
298
- <div align="center">
299
-
300
- ## License
301
-
302
- **MIT License** - See [LICENSE](LICENSE) for details
303
-
304
- </div>
305
-