mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Explain a print that never reached the printer's card, instead of sweeping for it (#2780)
Bambuddy reads a print's 3MF, cover and timelapse over FTPS on port 990, which on every Bambu model serves external storage only. Under some configurations H2-series and P2S firmware keeps the sliced file on internal storage, where Bambu Studio put it over the port-6000 service, and then no path on 990 can find it. The print command has always said which of the two it used -- `url` reads ftp://<name> or brtc://emmc/<name>. We discarded it and swept anyway: ~110 connections per print, all certain to fail, ending in an archive card with nothing on it and no stated reason. In the reporter's bundle all 35 dispatches to their H2C and P2S said internal storage, all 25 to their X1C said external, and all 44 empty cards belonged to the first two. Read the field, skip the sweep when it cannot succeed, and record which reason applied. A printer that uses the card is unaffected, and so is one we have no answer for -- silence is not evidence, and reading it as bad news would break archives that work today. The answer is held per print and dropped when that print ends, rather than kept as a standing fact about the printer. Plenty of prints never announce themselves: 14 of the 79 print starts in that bundle arrived with nothing on the request topic, started from the printer's own screen or picked up after a restart. Left standing, one slicer print to internal storage would suppress the lookup for every screen-started print after it, on a printer whose files really are on the card. The sticky reading is kept for the connection diagnostic alone, which is run after the print that prompted it and would otherwise have nothing to report. Two things that pointed the wrong way go with it. The archives banner told everyone to enable "Store sent files on external storage"; the reporter had it on for the whole three weeks and it would not have helped. The diagnostic passed a printer whose slot was empty, because it read only the toggle -- an empty slot is now a failure naming the slot, and a printer that has storage and still used its own is a warning. On P1-series that empty-slot failure yields to the existing unsupported-model skip: the toggle cannot be switched on there at all, so telling the operator to insert a card would promise a fix inserting a card does not deliver (#2524). Also close FTP sockets on the failure paths, which dropped them for the garbage collector -- 1813 in a day in that bundle -- and drop the advice to restart the printer, which the reporter tried twice while a single manual connection to the same printer handshook cleanly. This does not make the affected prints archive in full; that needs the port-6000 protocol tracked in #2762.
This commit is contained in:
@@ -31,6 +31,7 @@ from backend.app.schemas.slicer import SliceRequest
|
||||
from backend.app.services.archive import ArchiveService
|
||||
from backend.app.services.design_settings import overrides_from_config
|
||||
from backend.app.services.filament_requirements import annotate_rack_groups
|
||||
from backend.app.services.print_storage import REASON_INTERNAL_STORAGE, REASON_NO_EXTERNAL_STORAGE
|
||||
from backend.app.utils.http import build_content_disposition
|
||||
from backend.app.utils.safe_path import safe_join_under
|
||||
from backend.app.utils.threemf_tools import (
|
||||
@@ -512,10 +513,23 @@ async def no_3mf_warning(
|
||||
)
|
||||
),
|
||||
):
|
||||
"""Whether to nudge the user about install step 4 ("Store sent files on
|
||||
external storage"). True iff any archive in the last 30 days was created
|
||||
via the no-3MF fallback path — that's the deterministic symptom of the
|
||||
slicer-side variant of the setting being off.
|
||||
"""Whether to nudge the user about a print that archived without its 3MF,
|
||||
and why. True iff any archive in the last 30 days was created via the
|
||||
no-3MF fallback path.
|
||||
|
||||
Also returns ``reason``, because the advice differs and the original
|
||||
single-cause wording sent people the wrong way. Historically the only
|
||||
known cause was install step 4 ("Store sent files on external storage")
|
||||
being off in the slicer, so the banner said so unconditionally. On
|
||||
H2-series and P2S that advice is actively wrong: the setting is already on
|
||||
and turning it on again changes nothing, because the printer keeps the
|
||||
sliced file on internal storage that FTPS does not serve at all (#2780).
|
||||
|
||||
``reason`` is the slug from :mod:`print_storage` when we recorded one,
|
||||
else None for the original slicer-setting case. When archives disagree the
|
||||
most specific known reason wins — one printer storing internally is a real
|
||||
finding worth explaining, and it should not be masked by another printer's
|
||||
plain missing-file fallback.
|
||||
|
||||
Complements the connection-diagnostic ``external_storage`` check, which
|
||||
only catches the printer-side variant of the setting. On older slicers
|
||||
@@ -536,10 +550,24 @@ async def no_3mf_warning(
|
||||
if user is not None and not can_read_all:
|
||||
conditions.append(PrintArchive.created_by_id == user.id)
|
||||
result = await db.execute(select(PrintArchive.extra_data).where(*conditions))
|
||||
reasons: set[str] = set()
|
||||
has_fallback = False
|
||||
for (extra_data,) in result.all():
|
||||
if extra_data and extra_data.get("no_3mf_available"):
|
||||
return {"has_fallback": True}
|
||||
return {"has_fallback": False}
|
||||
if not extra_data or not extra_data.get("no_3mf_available"):
|
||||
continue
|
||||
has_fallback = True
|
||||
reason = extra_data.get("no_3mf_reason")
|
||||
if reason:
|
||||
reasons.add(reason)
|
||||
if not has_fallback:
|
||||
return {"has_fallback": False, "reason": None}
|
||||
# Most specific first. Archives predating this field carry no reason at
|
||||
# all, so an install with one H2C and three older printers still gets the
|
||||
# H2C explanation rather than the generic one.
|
||||
for candidate in (REASON_INTERNAL_STORAGE, REASON_NO_EXTERNAL_STORAGE):
|
||||
if candidate in reasons:
|
||||
return {"has_fallback": True, "reason": candidate}
|
||||
return {"has_fallback": True, "reason": None}
|
||||
|
||||
|
||||
@router.get("/slim", response_model=list[ArchiveSlim])
|
||||
@@ -2347,14 +2375,14 @@ async def scan_timelapse(
|
||||
if not files:
|
||||
# "Couldn't reach the printer" and "the printer has no timelapse
|
||||
# directory" are different problems with different fixes, and both used
|
||||
# to come back as one 500 (#2780). A printer whose file service stopped
|
||||
# answering over TLS needs a restart, and nothing here will work until
|
||||
# it gets one.
|
||||
# to come back as one 500 (#2780). Nothing here will work while the
|
||||
# printer's file service is not answering over TLS, so say that rather
|
||||
# than reporting an empty directory.
|
||||
if ftps_handshake_blocked(printer.ip_address):
|
||||
raise HTTPException(
|
||||
503,
|
||||
f"Printer {printer.ip_address} is not answering its file service over TLS. "
|
||||
"Restart the printer and try again.",
|
||||
"Bambuddy will try again shortly.",
|
||||
)
|
||||
raise HTTPException(404, "No timelapse directory found on the printer")
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ from backend.app.services.bambu_ftp import (
|
||||
get_storage_info_async,
|
||||
list_files_async,
|
||||
)
|
||||
from backend.app.services.print_storage import print_file_reachable_over_ftp
|
||||
from backend.app.services.printer_diagnostic import run_connection_diagnostic
|
||||
from backend.app.services.printer_manager import (
|
||||
display_temperatures,
|
||||
@@ -1217,6 +1218,19 @@ async def _produce_cover_image(
|
||||
break
|
||||
|
||||
if not downloaded:
|
||||
# The cover lives inside the 3MF, so it is only reachable if the 3MF is.
|
||||
# When the printer kept the print on internal storage there is nothing
|
||||
# at any of these paths, and walking all sixteen of them just to end on
|
||||
# a 404 that reads as "this print has no cover" helps nobody (#2780).
|
||||
storage = print_file_reachable_over_ftp(printer_manager.get_status(printer_id))
|
||||
if not storage.reachable:
|
||||
_cover_404_cache.setdefault(printer_id, set()).add(cache_key)
|
||||
raise HTTPException(
|
||||
404,
|
||||
f"The print file for '{subtask_name}' is not on storage Bambuddy can read over FTPS "
|
||||
f"({storage.reason}), so it has no cover to extract.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Trying to download cover for '{subtask_name}' from {printer.ip_address} (trying {len(remote_paths)} paths)"
|
||||
)
|
||||
@@ -1234,7 +1248,7 @@ async def _produce_cover_image(
|
||||
raise HTTPException(
|
||||
503,
|
||||
f"Printer {printer.ip_address} is not answering its file service over TLS. "
|
||||
"Restart the printer and try again.",
|
||||
"Bambuddy will try again shortly.",
|
||||
)
|
||||
try:
|
||||
downloaded = await download_file_try_paths_async(
|
||||
|
||||
Reference in New Issue
Block a user