diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 6a3a714d5..ff80116d1 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -539,10 +539,17 @@ async def scan_timelapse( base_name = Path(archive.filename).stem # Scan timelapse directory on printer - try: - files = await list_files_async(printer.ip_address, printer.access_code, "/timelapse/video") - except Exception: - raise HTTPException(500, "Failed to connect to printer") + # Try both /timelapse and /timelapse/video (different printer models use different paths) + files = [] + for timelapse_path in ["/timelapse", "/timelapse/video"]: + try: + files = await list_files_async(printer.ip_address, printer.access_code, timelapse_path) + if files: + break + except Exception: + continue + if not files: + raise HTTPException(500, "Failed to connect to printer or no timelapse directory found") # Look for matching timelapse matching_file = None @@ -574,7 +581,12 @@ async def scan_timelapse( # Timelapse is usually created at print end, so compare to completed_at or created_at compare_time = archive.completed_at or archive.created_at if compare_time: - diff = abs(file_time - compare_time) + # Bambu printers use China Standard Time (UTC+8) for filenames + # Try matching with CST offset adjustment + diff_direct = abs(file_time - compare_time) + # Also try with 8-hour offset (CST to UTC-ish local times) + diff_cst_adjusted = abs(file_time - timedelta(hours=8) - compare_time) + diff = min(diff_direct, diff_cst_adjusted) if diff < best_diff: best_diff = diff best_match = f @@ -587,8 +599,8 @@ async def scan_timelapse( if not matching_file: return {"status": "not_found", "message": "No matching timelapse found on printer"} - # Download the timelapse - remote_path = f"/timelapse/video/{matching_file['name']}" + # Download the timelapse - use the full path from the file listing + remote_path = matching_file.get('path') or f"/timelapse/{matching_file['name']}" timelapse_data = await download_file_bytes_async( printer.ip_address, printer.access_code, remote_path ) diff --git a/backend/app/services/bambu_mqtt.py b/backend/app/services/bambu_mqtt.py index c0f4a9f00..7980b8854 100644 --- a/backend/app/services/bambu_mqtt.py +++ b/backend/app/services/bambu_mqtt.py @@ -153,6 +153,11 @@ class BambuMQTTClient: # Temperature data temps = {} + # Log all temperature-related fields for debugging (only when we have temp data) + temp_fields = {k: v for k, v in data.items() if 'temp' in k.lower() or 'nozzle' in k.lower()} + if temp_fields and not hasattr(self, '_temp_fields_logged'): + logger.info(f"[{self.serial_number}] Temperature fields in MQTT data: {temp_fields}") + self._temp_fields_logged = True if "bed_temper" in data: temps["bed"] = float(data["bed_temper"]) if "bed_target_temper" in data: @@ -162,10 +167,20 @@ class BambuMQTTClient: if "nozzle_target_temper" in data: temps["nozzle_target"] = float(data["nozzle_target_temper"]) # Second nozzle for dual-extruder printers (H2 series) + # Try multiple possible field names used by different firmware versions if "nozzle_temper_2" in data: temps["nozzle_2"] = float(data["nozzle_temper_2"]) + elif "right_nozzle_temper" in data: + temps["nozzle_2"] = float(data["right_nozzle_temper"]) if "nozzle_target_temper_2" in data: temps["nozzle_2_target"] = float(data["nozzle_target_temper_2"]) + elif "right_nozzle_target_temper" in data: + temps["nozzle_2_target"] = float(data["right_nozzle_target_temper"]) + # Also check for left nozzle as primary (some H2 models) + if "left_nozzle_temper" in data and "nozzle" not in temps: + temps["nozzle"] = float(data["left_nozzle_temper"]) + if "left_nozzle_target_temper" in data and "nozzle_target" not in temps: + temps["nozzle_target"] = float(data["left_nozzle_target_temper"]) if "chamber_temper" in data: temps["chamber"] = float(data["chamber_temper"]) if temps: @@ -232,7 +247,7 @@ class BambuMQTTClient: "raw_data": data, }) - # Detect print completion + # Detect print completion (FINISH = success, FAILED = error) if ( self._previous_gcode_state == "RUNNING" and self.state.state in ("FINISH", "FAILED") diff --git a/backend/app/services/smart_plug_manager.py b/backend/app/services/smart_plug_manager.py index 3fb228dbd..e767ed78d 100644 --- a/backend/app/services/smart_plug_manager.py +++ b/backend/app/services/smart_plug_manager.py @@ -70,7 +70,11 @@ class SmartPlugManager: async def on_print_complete( self, printer_id: int, status: str, db: AsyncSession ): - """Called when a print completes - schedule turn off if configured.""" + """Called when a print completes - schedule turn off if configured. + + Only triggers auto-off on successful completion (status='completed'). + Failed prints keep the printer powered on for user investigation. + """ plug = await self._get_plug_for_printer(printer_id, db) if not plug: @@ -84,8 +88,17 @@ class SmartPlugManager: logger.debug(f"Smart plug '{plug.name}' auto_off is disabled") return + # Only auto-off on successful completion, not on failures + # This allows the user to investigate errors before power-off + if status != "completed": + logger.info( + f"Print on printer {printer_id} ended with status '{status}', " + f"skipping auto-off for plug '{plug.name}' to allow investigation" + ) + return + logger.info( - f"Print completed on printer {printer_id} (status: {status}), " + f"Print completed successfully on printer {printer_id}, " f"scheduling turn-off for plug '{plug.name}'" ) @@ -192,14 +205,14 @@ class SmartPlugManager: max_nozzle_temp = nozzle_temp if nozzle_2_temp is not None: max_nozzle_temp = max(nozzle_temp, nozzle_2_temp) - logger.debug( - f"Checking temp for plug {plug_id}: nozzle1={nozzle_temp}°C, " + logger.info( + f"Temp check plug {plug_id}: nozzle1={nozzle_temp}°C, " f"nozzle2={nozzle_2_temp}°C, max={max_nozzle_temp}°C, " f"threshold={temp_threshold}°C" ) else: - logger.debug( - f"Checking temp for plug {plug_id}: nozzle={nozzle_temp}°C, " + logger.info( + f"Temp check plug {plug_id}: nozzle={nozzle_temp}°C, " f"threshold={temp_threshold}°C" )