|
@@ -69,7 +69,7 @@ from backend.app.core.config import APP_VERSION, settings as app_settings
|
|
|
from backend.app.core.database import async_session, engine, init_db
|
|
from backend.app.core.database import async_session, engine, init_db
|
|
|
from backend.app.core.websocket import ws_manager
|
|
from backend.app.core.websocket import ws_manager
|
|
|
from backend.app.models.smart_plug import SmartPlug
|
|
from backend.app.models.smart_plug import SmartPlug
|
|
|
-from backend.app.services.archive import ArchiveService
|
|
|
|
|
|
|
+from backend.app.services.archive import ArchiveService, peek_plate_index_in_3mf, swap_plate_suffix
|
|
|
from backend.app.services.archive_purge import archive_purge_service
|
|
from backend.app.services.archive_purge import archive_purge_service
|
|
|
from backend.app.services.background_dispatch import background_dispatch
|
|
from backend.app.services.background_dispatch import background_dispatch
|
|
|
from backend.app.services.bambu_ftp import (
|
|
from backend.app.services.bambu_ftp import (
|
|
@@ -93,6 +93,7 @@ from backend.app.services.obico_detection import obico_detection_service
|
|
|
from backend.app.services.print_scheduler import scheduler as print_scheduler
|
|
from backend.app.services.print_scheduler import scheduler as print_scheduler
|
|
|
from backend.app.services.printer_manager import (
|
|
from backend.app.services.printer_manager import (
|
|
|
init_printer_connections,
|
|
init_printer_connections,
|
|
|
|
|
+ parse_plate_id,
|
|
|
printer_manager,
|
|
printer_manager,
|
|
|
printer_state_to_dict,
|
|
printer_state_to_dict,
|
|
|
)
|
|
)
|
|
@@ -2151,6 +2152,114 @@ async def on_print_start(printer_id: int, data: dict):
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.debug("Failed to list %s: %s", search_dir, e)
|
|
logger.debug("Failed to list %s: %s", search_dir, e)
|
|
|
|
|
|
|
|
|
|
+ # Validate the downloaded 3MF actually matches the plate that's running
|
|
|
|
|
+ # (#1204): subtask_name lags across consecutive plates of the same model,
|
|
|
|
|
+ # so the first FTP candidate (built from subtask_name) can land on the
|
|
|
|
|
+ # previous plate's still-resident upload. Cross-check the slice_info
|
|
|
|
|
+ # plate index against the plate parsed from gcode_file (always fresh —
|
|
|
|
|
+ # it's the field whose change triggered this callback).
|
|
|
|
|
+ if downloaded_filename and temp_path:
|
|
|
|
|
+ expected_plate = parse_plate_id(filename)
|
|
|
|
|
+ actual_plate = peek_plate_index_in_3mf(temp_path) if expected_plate is not None else None
|
|
|
|
|
+ if expected_plate is not None and actual_plate is not None and actual_plate != expected_plate:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "[CALLBACK] 3MF plate mismatch: downloaded %s reports plate %s but printer is "
|
|
|
|
|
+ "running plate %s — subtask_name=%r appears stale, retrying with corrected name",
|
|
|
|
|
+ downloaded_filename,
|
|
|
|
|
+ actual_plate,
|
|
|
|
|
+ expected_plate,
|
|
|
|
|
+ subtask_name,
|
|
|
|
|
+ )
|
|
|
|
|
+ corrected_subtask = swap_plate_suffix(subtask_name, expected_plate)
|
|
|
|
|
+ retry_succeeded = False
|
|
|
|
|
+ if corrected_subtask and corrected_subtask != subtask_name:
|
|
|
|
|
+ for try_filename in (f"{corrected_subtask}.gcode.3mf", f"{corrected_subtask}.3mf"):
|
|
|
|
|
+ retry_temp_path = app_settings.archive_dir / "temp" / try_filename
|
|
|
|
|
+ retry_temp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ for remote_path in (
|
|
|
|
|
+ f"/{try_filename}",
|
|
|
|
|
+ f"/cache/{try_filename}",
|
|
|
|
|
+ f"/model/{try_filename}",
|
|
|
|
|
+ f"/data/{try_filename}",
|
|
|
|
|
+ f"/data/Metadata/{try_filename}",
|
|
|
|
|
+ ):
|
|
|
|
|
+ try:
|
|
|
|
|
+ if ftp_retry_enabled:
|
|
|
|
|
+ downloaded = await with_ftp_retry(
|
|
|
|
|
+ download_file_async,
|
|
|
|
|
+ printer.ip_address,
|
|
|
|
|
+ printer.access_code,
|
|
|
|
|
+ remote_path,
|
|
|
|
|
+ retry_temp_path,
|
|
|
|
|
+ timeout=ftp_timeout,
|
|
|
|
|
+ socket_timeout=ftp_timeout,
|
|
|
|
|
+ printer_model=printer.model,
|
|
|
|
|
+ max_retries=ftp_retry_count,
|
|
|
|
|
+ retry_delay=ftp_retry_delay,
|
|
|
|
|
+ operation_name=f"Re-download 3MF from {remote_path}",
|
|
|
|
|
+ non_retry_exceptions=(FileNotOnPrinterError,),
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ downloaded = await download_file_async(
|
|
|
|
|
+ printer.ip_address,
|
|
|
|
|
+ printer.access_code,
|
|
|
|
|
+ remote_path,
|
|
|
|
|
+ retry_temp_path,
|
|
|
|
|
+ timeout=ftp_timeout,
|
|
|
|
|
+ socket_timeout=ftp_timeout,
|
|
|
|
|
+ printer_model=printer.model,
|
|
|
|
|
+ )
|
|
|
|
|
+ if downloaded and peek_plate_index_in_3mf(retry_temp_path) == expected_plate:
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[CALLBACK] Re-download succeeded with corrected name %s "
|
|
|
|
|
+ "(plate %s) — replacing wrong file",
|
|
|
|
|
+ try_filename,
|
|
|
|
|
+ expected_plate,
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ temp_path.unlink(missing_ok=True)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ temp_path = retry_temp_path
|
|
|
|
|
+ downloaded_filename = try_filename
|
|
|
|
|
+ subtask_name = corrected_subtask
|
|
|
|
|
+ cache_3mf_download(printer_id, try_filename, temp_path)
|
|
|
|
|
+ retry_succeeded = True
|
|
|
|
|
+ break
|
|
|
|
|
+ elif downloaded:
|
|
|
|
|
+ # Wrong plate again — discard and keep trying
|
|
|
|
|
+ try:
|
|
|
|
|
+ retry_temp_path.unlink(missing_ok=True)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ except FileNotOnPrinterError:
|
|
|
|
|
+ continue
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.debug("Re-download failed for %s: %s", remote_path, e)
|
|
|
|
|
+ if retry_succeeded:
|
|
|
|
|
+ break
|
|
|
|
|
+ # If the retry didn't find a matching file, drop the wrong 3MF
|
|
|
|
|
+ # so the no-3MF fallback below creates an archive whose name
|
|
|
|
|
+ # at least reflects the right plate.
|
|
|
|
|
+ if not retry_succeeded:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "[CALLBACK] Could not re-download correct plate %s — falling back to no-3MF archive",
|
|
|
|
|
+ expected_plate,
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ temp_path.unlink(missing_ok=True)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ temp_path = None
|
|
|
|
|
+ downloaded_filename = None
|
|
|
|
|
+ # Override the stale subtask_name so the fallback archive's
|
|
|
|
|
+ # print_name reflects the correct plate. Prefer the swapped
|
|
|
|
|
+ # name when we have one; otherwise let filename win.
|
|
|
|
|
+ if corrected_subtask:
|
|
|
|
|
+ subtask_name = corrected_subtask
|
|
|
|
|
+ else:
|
|
|
|
|
+ subtask_name = ""
|
|
|
|
|
+
|
|
|
if not downloaded_filename or not temp_path:
|
|
if not downloaded_filename or not temp_path:
|
|
|
logger.warning("Could not find 3MF file for print: %s", filename or subtask_name)
|
|
logger.warning("Could not find 3MF file for print: %s", filename or subtask_name)
|
|
|
# Create a fallback archive without 3MF data so the print is still tracked
|
|
# Create a fallback archive without 3MF data so the print is still tracked
|