Browse Source

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).
maziggy 3 months ago
parent
commit
5da769be31
1 changed files with 50 additions and 9 deletions
  1. 50 9
      backend/app/services/bambu_ftp.py

+ 50 - 9
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)
     # 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
     # These models have varying FTP SSL behavior depending on firmware version
     A1_MODELS = ("A1", "A1 Mini")
     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
     # Cache for working FTP modes per printer IP
     # Maps IP -> "prot_p" or "prot_c"
     # Maps IP -> "prot_p" or "prot_c"
@@ -363,15 +366,37 @@ class BambuFTPClient:
 
 
             uploaded = 0
             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:
             with open(local_path, "rb") as f:
                 logger.debug(f"FTP STOR command starting for {remote_path}")
                 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}")
             logger.info(f"FTP upload complete: {remote_path}")
             return True
             return True
         except ftplib.error_perm as e:
         except ftplib.error_perm as e:
@@ -399,8 +424,24 @@ class BambuFTPClient:
             return False
             return False
 
 
         try:
         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
             return True
         except Exception:
         except Exception:
             return False
             return False