mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
FTP auto-detection: try prot_p first, fall back to prot_c for A1
User feedback indicated A1 Mini with current firmware works with prot_p (protected/SSL data channel), not prot_c as previously assumed. Different A1 firmware versions have different FTP SSL behavior. Changes: - Remove hardcoded assumption that A1 models need prot_c - Try prot_p first for all models (including A1/A1 Mini) - If upload/download fails on A1 models, automatically retry with prot_c - Cache working mode per printer IP for subsequent operations - Add force_prot_c parameter for explicit mode control This makes FTP work across A1 firmware versions: - New firmware: prot_p succeeds, cached - Old firmware: prot_p fails → prot_c fallback succeeds, cached
This commit is contained in:
@@ -77,9 +77,13 @@ class BambuFTPClient:
|
||||
|
||||
FTP_PORT = 990
|
||||
DEFAULT_TIMEOUT = 30 # Default timeout in seconds (increased for A1 printers)
|
||||
# Models that need SSL session reuse disabled (A1 series has FTP issues with session reuse)
|
||||
# X1C/X1E/P1S/P1P/P2S use vsFTPd which requires SSL session reuse - do NOT add them here
|
||||
SKIP_SESSION_REUSE_MODELS = ("A1", "A1 Mini")
|
||||
# Models that may need SSL mode fallback (try prot_p first, fall back to prot_c)
|
||||
# These models have varying FTP SSL behavior depending on firmware version
|
||||
A1_MODELS = ("A1", "A1 Mini")
|
||||
|
||||
# Cache for working FTP modes per printer IP
|
||||
# Maps IP -> "prot_p" or "prot_c"
|
||||
_mode_cache: dict[str, str] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -87,45 +91,70 @@ class BambuFTPClient:
|
||||
access_code: str,
|
||||
timeout: float | None = None,
|
||||
printer_model: str | None = None,
|
||||
force_prot_c: bool = False,
|
||||
):
|
||||
self.ip_address = ip_address
|
||||
self.access_code = access_code
|
||||
self.timeout = timeout if timeout is not None else self.DEFAULT_TIMEOUT
|
||||
self.printer_model = printer_model
|
||||
self.force_prot_c = force_prot_c
|
||||
self._ftp: ImplicitFTP_TLS | None = None
|
||||
|
||||
def _should_skip_session_reuse(self) -> bool:
|
||||
"""Check if this printer model needs SSL session reuse disabled."""
|
||||
def _is_a1_model(self) -> bool:
|
||||
"""Check if this is an A1 series printer."""
|
||||
if not self.printer_model:
|
||||
return False
|
||||
return self.printer_model in self.SKIP_SESSION_REUSE_MODELS
|
||||
return self.printer_model in self.A1_MODELS
|
||||
|
||||
def _get_cached_mode(self) -> str | None:
|
||||
"""Get cached FTP mode for this printer."""
|
||||
return self._mode_cache.get(self.ip_address)
|
||||
|
||||
@classmethod
|
||||
def cache_mode(cls, ip_address: str, mode: str):
|
||||
"""Cache the working FTP mode for a printer."""
|
||||
cls._mode_cache[ip_address] = mode
|
||||
logger.info(f"FTP mode cached for {ip_address}: {mode}")
|
||||
|
||||
def _should_use_prot_c(self) -> bool:
|
||||
"""Determine if we should use prot_c (clear) mode."""
|
||||
# If explicitly forced, use prot_c
|
||||
if self.force_prot_c:
|
||||
return True
|
||||
# Check cache first
|
||||
cached = self._get_cached_mode()
|
||||
if cached:
|
||||
return cached == "prot_c"
|
||||
# Default: try prot_p first (will fall back if needed)
|
||||
return False
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to the printer FTP server (implicit FTPS on port 990)."""
|
||||
try:
|
||||
skip_reuse = self._should_skip_session_reuse()
|
||||
use_prot_c = self._should_use_prot_c()
|
||||
logger.debug(
|
||||
f"FTP connecting to {self.ip_address}:{self.FTP_PORT} "
|
||||
f"(timeout={self.timeout}s, model={self.printer_model}, skip_session_reuse={skip_reuse})"
|
||||
f"(timeout={self.timeout}s, model={self.printer_model}, prot_c={use_prot_c})"
|
||||
)
|
||||
self._ftp = ImplicitFTP_TLS(skip_session_reuse=skip_reuse)
|
||||
self._ftp = ImplicitFTP_TLS(skip_session_reuse=use_prot_c)
|
||||
self._ftp.connect(self.ip_address, self.FTP_PORT, timeout=self.timeout)
|
||||
logger.debug("FTP connected, logging in as bblp")
|
||||
self._ftp.login("bblp", self.access_code)
|
||||
if skip_reuse:
|
||||
# A1/A1 Mini: Use clear (unencrypted) data channel
|
||||
# These printers have issues with SSL on the data channel
|
||||
logger.debug("FTP logged in, setting prot_c (clear) and passive mode for A1")
|
||||
if use_prot_c:
|
||||
# Use clear (unencrypted) data channel
|
||||
logger.debug("FTP logged in, setting prot_c (clear) and passive mode")
|
||||
self._ftp.prot_c()
|
||||
else:
|
||||
# X1C/P1S/etc: Use protected (encrypted) data channel with session reuse
|
||||
# Use protected (encrypted) data channel with session reuse
|
||||
logger.debug("FTP logged in, setting prot_p (protected) and passive mode")
|
||||
self._ftp.prot_p()
|
||||
self._ftp.set_pasv(True)
|
||||
# Log welcome message for debugging
|
||||
if hasattr(self._ftp, "welcome") and self._ftp.welcome:
|
||||
logger.debug(f"FTP server welcome: {self._ftp.welcome}")
|
||||
logger.info(f"FTP connected successfully to {self.ip_address} (model={self.printer_model})")
|
||||
logger.info(
|
||||
f"FTP connected successfully to {self.ip_address} (model={self.printer_model}, prot_c={use_prot_c})"
|
||||
)
|
||||
return True
|
||||
except ftplib.error_perm as e:
|
||||
logger.warning(f"FTP connection permission error to {self.ip_address}: {e}")
|
||||
@@ -462,6 +491,9 @@ async def download_file_async(
|
||||
) -> bool:
|
||||
"""Async wrapper for downloading a file with timeout.
|
||||
|
||||
For A1/A1 Mini printers, automatically tries prot_p first, then falls back
|
||||
to prot_c if the download fails. The working mode is cached for future operations.
|
||||
|
||||
Args:
|
||||
ip_address: Printer IP address
|
||||
access_code: Printer access code
|
||||
@@ -472,18 +504,47 @@ async def download_file_async(
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
is_a1 = printer_model in BambuFTPClient.A1_MODELS if printer_model else False
|
||||
|
||||
def _download():
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
def _download(force_prot_c: bool = False) -> bool:
|
||||
mode_str = "prot_c" if force_prot_c else "prot_p"
|
||||
client = BambuFTPClient(
|
||||
ip_address, access_code, timeout=socket_timeout, printer_model=printer_model, force_prot_c=force_prot_c
|
||||
)
|
||||
if client.connect():
|
||||
try:
|
||||
return client.download_to_file(remote_path, local_path)
|
||||
result = client.download_to_file(remote_path, local_path)
|
||||
if result:
|
||||
# Cache the working mode
|
||||
BambuFTPClient.cache_mode(ip_address, mode_str)
|
||||
return result
|
||||
finally:
|
||||
client.disconnect()
|
||||
return False
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(loop.run_in_executor(None, _download), timeout=timeout)
|
||||
# Check if we have a cached mode for this printer
|
||||
cached_mode = BambuFTPClient._mode_cache.get(ip_address)
|
||||
|
||||
if cached_mode:
|
||||
# Use cached mode
|
||||
force_prot_c = cached_mode == "prot_c"
|
||||
return await asyncio.wait_for(loop.run_in_executor(None, lambda: _download(force_prot_c)), timeout=timeout)
|
||||
|
||||
# No cached mode - try prot_p first
|
||||
result = await asyncio.wait_for(loop.run_in_executor(None, lambda: _download(False)), timeout=timeout)
|
||||
|
||||
if result:
|
||||
return True
|
||||
|
||||
# Download failed - for A1 models, try prot_c fallback
|
||||
if is_a1:
|
||||
logger.info("FTP download failed with prot_p for A1 model, trying prot_c fallback...")
|
||||
result = await asyncio.wait_for(loop.run_in_executor(None, lambda: _download(True)), timeout=timeout)
|
||||
return result
|
||||
|
||||
return False
|
||||
|
||||
except TimeoutError:
|
||||
logger.warning(f"FTP download timed out after {timeout}s for {remote_path}")
|
||||
return False
|
||||
@@ -530,6 +591,9 @@ async def upload_file_async(
|
||||
) -> bool:
|
||||
"""Async wrapper for uploading a file with timeout and progress callback.
|
||||
|
||||
For A1/A1 Mini printers, automatically tries prot_p first, then falls back
|
||||
to prot_c if the upload fails. The working mode is cached for future uploads.
|
||||
|
||||
Args:
|
||||
ip_address: Printer IP address
|
||||
access_code: Printer access code
|
||||
@@ -541,23 +605,53 @@ async def upload_file_async(
|
||||
printer_model: Printer model for A1-specific workarounds
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
is_a1 = printer_model in BambuFTPClient.A1_MODELS if printer_model else False
|
||||
|
||||
def _upload():
|
||||
def _upload(force_prot_c: bool = False) -> bool:
|
||||
mode_str = "prot_c" if force_prot_c else "prot_p"
|
||||
logger.info(
|
||||
f"FTP connecting to {ip_address} for upload (model={printer_model}, socket_timeout={socket_timeout}s)..."
|
||||
f"FTP connecting to {ip_address} for upload (model={printer_model}, "
|
||||
f"mode={mode_str}, socket_timeout={socket_timeout}s)..."
|
||||
)
|
||||
client = BambuFTPClient(
|
||||
ip_address, access_code, timeout=socket_timeout, printer_model=printer_model, force_prot_c=force_prot_c
|
||||
)
|
||||
client = BambuFTPClient(ip_address, access_code, timeout=socket_timeout, printer_model=printer_model)
|
||||
if client.connect():
|
||||
logger.info(f"FTP connected to {ip_address}")
|
||||
try:
|
||||
return client.upload_file(local_path, remote_path, progress_callback)
|
||||
result = client.upload_file(local_path, remote_path, progress_callback)
|
||||
if result:
|
||||
# Cache the working mode
|
||||
BambuFTPClient.cache_mode(ip_address, mode_str)
|
||||
return result
|
||||
finally:
|
||||
client.disconnect()
|
||||
logger.warning(f"FTP connection failed to {ip_address}")
|
||||
return False
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(loop.run_in_executor(None, _upload), timeout=timeout)
|
||||
# Check if we have a cached mode for this printer
|
||||
cached_mode = BambuFTPClient._mode_cache.get(ip_address)
|
||||
|
||||
if cached_mode:
|
||||
# Use cached mode
|
||||
force_prot_c = cached_mode == "prot_c"
|
||||
return await asyncio.wait_for(loop.run_in_executor(None, lambda: _upload(force_prot_c)), timeout=timeout)
|
||||
|
||||
# No cached mode - try prot_p first
|
||||
result = await asyncio.wait_for(loop.run_in_executor(None, lambda: _upload(False)), timeout=timeout)
|
||||
|
||||
if result:
|
||||
return True
|
||||
|
||||
# Upload failed - for A1 models, try prot_c fallback
|
||||
if is_a1:
|
||||
logger.info("FTP upload failed with prot_p for A1 model, trying prot_c fallback...")
|
||||
result = await asyncio.wait_for(loop.run_in_executor(None, lambda: _upload(True)), timeout=timeout)
|
||||
return result
|
||||
|
||||
return False
|
||||
|
||||
except TimeoutError:
|
||||
logger.warning(f"FTP upload timed out after {timeout}s for {remote_path}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user