Skip to content

Commit e02b13e

Browse files
committed
feat: cache printer-reported camera stream URLs on FlashForgeClient
Add Adventurer 3 (A3) full TCP client support, cache the OEM camera stream URL from machine-info responses on FlashForgeClient, and substantially improve discovery, TCP client, and parser reliability. - Add FlashForgeA3Client with dedicated G-code protocol support - Add A3GCodeController for Adventurer 3 command set - Add FlashForgeClient.camera_stream_url populated from machine-info - Major refactor of discovery.py for cross-printer reliability - tcp_client.py reworked with improved connection and parser coverage - All parsers updated for better type safety and correctness - Add thumbnail_info parser for printer thumbnail responses - Extend package exports to include A3 client and types - Add test coverage for A3 client, camera stream URL, and model parity - Bump version to 1.1.0
1 parent 99f8683 commit e02b13e

34 files changed

Lines changed: 2746 additions & 1301 deletions

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2026-03-08
9+
10+
### Added
11+
- `FlashForgeA3Client` — full Adventurer 3 TCP client with dedicated G-code protocol support
12+
- `A3GCodeController` — A3-specific G-code command controller
13+
- `FlashForgeClient.camera_stream_url` — caches the OEM camera stream URL reported by the printer in machine-info responses
14+
- `thumbnail_info` parser for handling printer thumbnail responses
15+
- New G-code commands in `gcodes.py` for broader printer compatibility
16+
- Test coverage for A3 client, camera stream URL caching, and machine-info model parity
17+
- mypy exclude configuration for tests and examples
18+
19+
### Changed
20+
- Major refactor of `discovery.py` for improved reliability and cross-printer compatibility
21+
- `tcp_client.py` substantially reworked for better connection handling and parser coverage
22+
- Parsers (`endstop_status`, `location_info`, `print_status`, `printer_info`, `temp_info`) updated for improved type safety and correctness
23+
- `client.py` extended with camera stream URL caching from machine-info responses
24+
- `control.py` updated with refined API surface
25+
- `info.py` refactored for cleaner response handling
26+
- Ruff linter configuration updated to use `[tool.ruff.lint]` table (ruff >=0.1.0 syntax)
27+
- Package exports in `__init__.py` updated to include new A3 types and client
28+
- TCP module exports updated to expose A3 client and controller
29+
30+
### Fixed
31+
- Discovery reliability improvements from `discovery.py` refactor
32+
- TCP client parser edge cases addressed across all response types
33+
834
## [1.0.2] - 2025-12-26
935

1036
### Added

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
| **File Operations** | List, upload, and download files • Extract print thumbnails • File metadata retrieval |
2929
| **Async Architecture** | Native async/await implementation • Non-blocking network operations • Concurrent operations support |
3030
| **Type Safety** | Full type hints for IDE autocomplete • Pydantic models for data validation • mypy strict mode compatible |
31-
| **Model Detection** | Automatic capability detection • Feature flags for model-specific functions • Graceful degradation for older models |
31+
| **Model Detection** | Automatic capability detection • Runtime camera stream detection via `camera_stream_url` • Graceful degradation for older models |
3232

3333
</div>
3434

@@ -156,6 +156,31 @@ async def monitor_printer():
156156
asyncio.run(monitor_printer())
157157
```
158158

159+
### Camera Stream Detection
160+
161+
Use the printer-reported runtime camera stream URL as the OEM camera source of truth:
162+
163+
```python
164+
from flashforge import FlashForgeClient
165+
import asyncio
166+
167+
async def check_camera():
168+
async with FlashForgeClient("192.168.1.100", "SERIAL", "CODE") as client:
169+
if not await client.initialize():
170+
return
171+
172+
if client.camera_stream_url:
173+
print(f"OEM camera stream: {client.camera_stream_url}")
174+
else:
175+
print("Printer is not reporting an active OEM camera stream")
176+
177+
# Camera power control remains Pro-only.
178+
if client.is_pro:
179+
await client.control.turn_camera_on()
180+
181+
asyncio.run(check_camera())
182+
```
183+
159184
### File Operations and Thumbnails
160185

161186
List files and extract G-code thumbnails:

docs/api_reference.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ FlashForgeClient(ip_address: str, serial_number: str, check_code: str)
6464
- `is_pro: bool` - True if Pro model
6565
- `firmware_version: str` - Firmware version string
6666
- `mac_address: str` - MAC address
67+
- `camera_stream_url: str` - Runtime OEM camera stream URL reported by the printer
6768
- `led_control: bool` - True if LED control available
6869
- `filtration_control: bool` - True if filtration control available
6970

@@ -149,6 +150,14 @@ General machine control operations.
149150
- `async set_filtration_off() -> bool`
150151
Turns off all filtration systems.
151152

153+
#### Camera Control
154+
155+
- `async turn_camera_on() -> bool`
156+
Sends the OEM camera enable command for Pro models.
157+
158+
- `async turn_camera_off() -> bool`
159+
Sends the OEM camera disable command for Pro models.
160+
152161
#### Fan Control
153162

154163
- `async set_chamber_fan_speed(speed: int) -> bool`
@@ -411,6 +420,7 @@ Structured printer status and information.
411420
- `firmware_version: str` - Firmware version
412421
- `ip_address: str` - IP address
413422
- `mac_address: str` - MAC address
423+
- `camera_stream_url: str` - OEM camera stream URL, or empty when the printer is not reporting one
414424
- `print_file_name: str` - Current print file
415425
- `print_progress: float` - Print progress (0.0-100.0)
416426
- `current_print_layer: int` - Current layer number

docs/client.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,22 @@ async with FlashForgeClient(ip, serial, check_code) as client:
103103
await client.job_control.cancel_print_job()
104104
```
105105

106+
#### Detect OEM Camera Stream
107+
108+
```python
109+
async with FlashForgeClient(ip, serial, check_code) as client:
110+
await client.initialize()
111+
112+
if client.camera_stream_url:
113+
print(f"OEM camera stream: {client.camera_stream_url}")
114+
else:
115+
print("No active OEM camera stream reported by the printer")
116+
117+
# Camera power control remains Pro-only.
118+
if client.is_pro:
119+
await client.control.turn_camera_on()
120+
```
121+
106122
#### List Files
107123

108124
```python

docs/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The primary model for printer status, aggregating data from various sources into
2222
| `firmware_version` | `str` | Firmware version string |
2323
| `ip_address` | `str` | Printer IP address |
2424
| `mac_address` | `str` | MAC address |
25+
| `camera_stream_url` | `str` | OEM camera stream URL reported by the printer, or empty when inactive |
2526

2627
#### Temperature Properties
2728

flashforge/__init__.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,23 @@ async def main():
5151

5252
# Import utility classes
5353
from .api.network.utils import NetworkUtils
54-
from .client import FlashForgeClient
54+
from .client import FiveMClientConnectionOptions, FlashForgeClient
5555

5656
# Import discovery classes
5757
from .discovery import (
58+
DiscoveredPrinter,
59+
DiscoveryError,
60+
DiscoveryMonitor,
61+
DiscoveryOptions,
62+
DiscoveryProtocol,
63+
DiscoveryTimeoutError,
5864
FlashForgePrinter,
5965
FlashForgePrinterDiscovery,
66+
InvalidResponseError,
67+
PrinterDiscovery,
68+
PrinterModel,
69+
PrinterStatus,
70+
SocketCreationError,
6071
)
6172

6273
# Import key models for convenience
@@ -83,9 +94,16 @@ async def main():
8394
ThumbnailResponse,
8495
)
8596
from .tcp import (
97+
A3BuildVolume,
98+
A3FileEntry,
99+
A3GCodeController,
100+
A3PrinterInfo,
101+
A3Thumbnail,
86102
Endstop,
87103
EndstopStatus,
104+
FlashForgeA3Client,
88105
FlashForgeTcpClient,
106+
FlashForgeTcpClientOptions,
89107
GCodeController,
90108
GCodes,
91109
LocationInfo,
@@ -104,7 +122,8 @@ async def main():
104122
FlashForgeTcpClient as TcpClient,
105123
)
106124

107-
__version__ = "1.0.0"
125+
FiveMClient = FlashForgeClient
126+
__version__ = "1.1.0"
108127
__author__ = "FlashForge Python API Contributors"
109128
__email__ = "notghosttypes@gmail.com"
110129
__description__ = "Python library for controlling FlashForge 3D printers"
@@ -113,6 +132,8 @@ async def main():
113132
__all__ = [
114133
# Main client class
115134
"FlashForgeClient",
135+
"FiveMClient",
136+
"FiveMClientConnectionOptions",
116137
# Data models
117138
"FFMachineInfo",
118139
"FFPrinterDetail",
@@ -144,6 +165,8 @@ async def main():
144165
# TCP classes for low-level operations
145166
"TcpClient",
146167
"FlashForgeTcpClient",
168+
"FlashForgeTcpClientOptions",
169+
"FlashForgeA3Client",
147170
"PrinterInfo",
148171
"TempInfo",
149172
"TempData",
@@ -157,7 +180,23 @@ async def main():
157180
"ThumbnailInfo",
158181
"GCodes",
159182
"GCodeController",
183+
"A3GCodeController",
184+
"A3BuildVolume",
185+
"A3PrinterInfo",
186+
"A3FileEntry",
187+
"A3Thumbnail",
160188
# Discovery classes
189+
"PrinterDiscovery",
190+
"DiscoveredPrinter",
191+
"DiscoveryOptions",
192+
"PrinterModel",
193+
"DiscoveryProtocol",
194+
"PrinterStatus",
195+
"DiscoveryMonitor",
196+
"DiscoveryError",
197+
"InvalidResponseError",
198+
"SocketCreationError",
199+
"DiscoveryTimeoutError",
161200
"FlashForgePrinter",
162201
"FlashForgePrinterDiscovery",
163202
# Utilities
@@ -176,7 +215,7 @@ async def main():
176215
]
177216

178217
# Convenience imports for common patterns
179-
from .models.machine_info import MachineState as State
218+
State = MachineState
180219

181220
# Add version info accessible as flashforge.version
182221
version = __version__

flashforge/api/controls/control.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,28 +178,26 @@ async def set_cooling_fan_speed(self, speed: int) -> bool:
178178
async def set_led_on(self) -> bool:
179179
"""
180180
Turns on the printer's LED lights.
181-
Requires the printer to have LED control.
182181
183182
Returns:
184183
True if the command is successful, False otherwise.
185184
"""
186-
if self.client.led_control:
187-
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "open"})
188-
print("SetLedOn() error, LEDs not equipped.")
189-
return False
185+
if not self.client.led_control:
186+
print("SetLedOn() error, LED control not equipped.")
187+
return False
188+
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "open"})
190189

191190
async def set_led_off(self) -> bool:
192191
"""
193192
Turns off the printer's LED lights.
194-
Requires the printer to have LED control.
195193
196194
Returns:
197195
True if the command is successful, False otherwise.
198196
"""
199-
if self.client.led_control:
200-
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "close"})
201-
print("SetLedOff() error, LEDs not equipped.")
202-
return False
197+
if not self.client.led_control:
198+
print("SetLedOff() error, LED control not equipped.")
199+
return False
200+
return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "close"})
203201

204202
async def turn_runout_sensor_on(self) -> bool:
205203
"""

0 commit comments

Comments
 (0)