ソースを参照

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 ヶ月 前
コミット
5da769be31
1 ファイル変更50 行追加9 行削除
  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)
     # 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