소스 검색

fix: preserve original firmware filename on SD card upload (#385)

The firmware update cache stored downloaded files under a mangled name
(e.g. X1C_01_09_00_10.bin) instead of the original filename from
Bambu Lab's CDN. On cache hit the wrong filename was uploaded to the
printer's SD card, so the printer couldn't find the firmware file.
Now caches with the original filename so it's always correct.
maziggy 3 달 전
부모
커밋
c52a67a5c3
2개의 변경된 파일11개의 추가작업 그리고 26개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 10 26
      backend/app/services/firmware_check.py

+ 1 - 0
CHANGELOG.md

@@ -18,6 +18,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Print Log** — New view mode on the Archives page showing a chronological table of all print activity. Columns include date/time, print name, printer, user, status, duration, and filament. Supports filtering by search text, printer, user, status, and date range. Pagination with configurable page size. A dedicated clear button deletes only log entries without affecting archives. Data is stored in a separate `print_log_entries` database table.
 
 ### Fixed
+- **Firmware Upload Uses Wrong Filename on Cache Hit** — The firmware update uploader cached downloaded firmware files under a mangled name (e.g., `X1C_01_09_00_10.bin`) instead of the original filename from Bambu Lab's CDN. On the first download the correct filename was uploaded to the SD card, but on subsequent attempts the cached file with the wrong name was used — causing the printer to not recognize the firmware file. Now caches using the original filename so the SD card always receives the correct file.
 - **Update Check Runs When Disabled** ([#367](https://github.com/maziggy/bambuddy/issues/367)) — The Settings page triggered an update check on every visit even when "Check for updates" was disabled, causing error popups on air-gapped systems with no internet. The backend `/updates/check` endpoint also ignored the setting entirely. Now the backend returns early without making GitHub API calls when the setting is disabled, the Settings page respects the `check_updates` flag before auto-fetching, and the printer card firmware badge shows a neutral version-only display instead of disappearing when firmware update checks are off.
 - **Stale Inventory Assignments Persist After Switching to Spoolman Mode** — When switching from built-in inventory to Spoolman mode, existing spool-to-AMS-slot assignments were not cleaned up. The printer card hover cards continued showing "Assign Spool" buttons that opened the internal inventory modal, and any prior assignments remained visible. Now bulk-deletes all `SpoolAssignment` records when enabling Spoolman, invalidates the frontend cache so printer cards update immediately, and hides the inventory assign/unassign UI on printer cards while in Spoolman mode.
 - **Bulk Archive Delete Leaves Orphaned Database Records** — When bulk-deleting archives, the files were removed from disk before the database commit. If concurrent SQLite writes caused a lock timeout, the commit failed and rolled back — leaving database records pointing to deleted files (broken thumbnails, 404 errors). Fixed by deleting the database record first and only removing files after a successful commit.

+ 10 - 26
backend/app/services/firmware_check.py

@@ -324,14 +324,6 @@ class FirmwareCheckService:
         cache_dir.mkdir(parents=True, exist_ok=True)
         return cache_dir
 
-    def _get_cached_firmware_path(self, model: str, version: str) -> Path:
-        """Get the path where a firmware file would be cached."""
-        # Normalize model name for filename
-        model_safe = model.upper().replace(" ", "-").replace("/", "-")
-        version_safe = version.replace(".", "_")
-        filename = f"{model_safe}_{version_safe}.bin"
-        return self._get_firmware_cache_dir() / filename
-
     async def get_firmware_file_info(self, model: str) -> dict | None:
         """
         Get information about the firmware file for a model.
@@ -374,16 +366,16 @@ class FirmwareCheckService:
             logger.warning("No firmware download URL available for model: %s", model)
             return None
 
-        # Check if already cached
-        cached_path = self._get_cached_firmware_path(model, latest.version)
-        if cached_path.exists():
-            logger.info("Using cached firmware: %s", cached_path)
-            return cached_path
-
         # Extract original filename from URL (must preserve for SD card update)
         url_parts = latest.download_url.split("/")
         original_filename = url_parts[-1] if url_parts else f"firmware_{model}.bin"
 
+        # Check if already cached (using original filename so SD card gets the right name)
+        cached_path = self._get_firmware_cache_dir() / original_filename
+        if cached_path.exists():
+            logger.info("Using cached firmware: %s", cached_path)
+            return cached_path
+
         # Download to temp file first
         temp_path = self._get_firmware_cache_dir() / f".downloading_{original_filename}"
 
@@ -407,22 +399,14 @@ class FirmwareCheckService:
                         if progress_callback:
                             progress_callback(downloaded, total_size, "Downloading firmware...")
 
-            # Also save a copy with the original filename for SD card
-            original_path = self._get_firmware_cache_dir() / original_filename
-            if original_path.exists():
-                original_path.unlink()
+            # Move temp to final path, preserving original filename
+            temp_path.rename(cached_path)
 
-            # Move temp to both cached path and original filename path
-            import shutil
-
-            shutil.copy2(temp_path, cached_path)
-            temp_path.rename(original_path)
-
-            logger.info("Firmware downloaded successfully: %s", original_path)
+            logger.info("Firmware downloaded successfully: %s", cached_path)
             if progress_callback:
                 progress_callback(downloaded, total_size, "Download complete")
 
-            return original_path
+            return cached_path
 
         except Exception as e:
             logger.error("Firmware download failed: %s", e)