From 5da769be316f9543e7536b61e0bbba079dbca265 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 5 Feb 2026 17:06:15 +0100 Subject: [PATCH] Fix A1 FTP uploads by replacing storbinary with manual transfer The A1 printer's FTP server hangs when Python's storbinary() calls voidresp() to wait for the server's completion response. This caused upload timeouts on A1 and A1 Mini printers. Fix contributed by an A1 user - replaces storbinary() with manual chunked transfer using transfercmd() + sendall(): - Uses 1MB chunks (CHUNK_SIZE constant) for better throughput - Sets explicit 120s socket timeout on data connection - Manually closes connection after transfer, avoiding voidresp() hang Applied to all printer models since the manual approach is compatible with X1C/P1S/P1P as well (transfercmd is what storbinary uses internally). --- backend/app/services/bambu_ftp.py | 59 ++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/backend/app/services/bambu_ftp.py b/backend/app/services/bambu_ftp.py index 66f4b548f..f3dfd09b4 100644 --- a/backend/app/services/bambu_ftp.py +++ b/backend/app/services/bambu_ftp.py @@ -80,6 +80,9 @@ class BambuFTPClient: # 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") + # Chunk size for manual upload transfer (1MB) + # Larger chunks reduce overhead and work better with A1 printers + CHUNK_SIZE = 1024 * 1024 # Cache for working FTP modes per printer IP # Maps IP -> "prot_p" or "prot_c" @@ -363,15 +366,37 @@ class BambuFTPClient: uploaded = 0 - def on_block(block: bytes): - nonlocal uploaded - uploaded += len(block) - if progress_callback: - progress_callback(uploaded, file_size) - + # Use manual transfer instead of storbinary() for A1 compatibility + # A1 printers have issues with storbinary's voidresp() hanging after transfer with open(local_path, "rb") as f: logger.debug(f"FTP STOR command starting for {remote_path}") - self._ftp.storbinary(f"STOR {remote_path}", f, callback=on_block) + conn = self._ftp.transfercmd(f"STOR {remote_path}") + + # Set explicit socket options for reliable transfer + conn.setblocking(True) + conn.settimeout(120) # 2 minute timeout per chunk + + try: + while True: + chunk = f.read(self.CHUNK_SIZE) + if not chunk: + logger.debug("FTP upload: final chunk reached") + break + + conn.sendall(chunk) + uploaded += len(chunk) + logger.debug(f"FTP upload progress: {uploaded}/{file_size} bytes") + + if progress_callback: + progress_callback(uploaded, file_size) + + except OSError as e: + logger.error(f"FTP connection lost during upload: {e}") + conn.close() + raise + + conn.close() + logger.info(f"FTP upload complete: {remote_path}") return True except ftplib.error_perm as e: @@ -399,8 +424,24 @@ class BambuFTPClient: return False try: - buffer = BytesIO(data) - self._ftp.storbinary(f"STOR {remote_path}", buffer) + # Use manual transfer instead of storbinary() for A1 compatibility + conn = self._ftp.transfercmd(f"STOR {remote_path}") + conn.setblocking(True) + conn.settimeout(120) + + try: + # Send data in chunks + offset = 0 + while offset < len(data): + chunk = data[offset : offset + self.CHUNK_SIZE] + conn.sendall(chunk) + offset += len(chunk) + except OSError as e: + logger.error(f"FTP connection lost during upload_bytes: {e}") + conn.close() + raise + + conn.close() return True except Exception: return False