Look for a print's file when the printer says it is on the card

(#2780 regression)

A print of a file already on the printer -- a reprint from the
touchscreen, from Handy, or a slicer send-to-storage followed by a print
-- reports its location as a path rather than as a fresh upload:
file:///media/usb0/<name>. Since #2780 landed on 2026-08-14 Bambuddy read
anything that was not ftp:// as "the printer kept this internally",
skipped the FTPS sweep, and archived the print with a name and timing
only. Measured on an H2D: the file was listable and downloadable over
FTPS at the moment Bambuddy declared it unreachable. Before that change
those prints archived normally, so this is a regression, and it is not
confined to the H2 series the change was about -- an X1C reprint from its
own screen loses its thumbnail exactly the same way.

That module's own rule is to skip only on positive evidence, and a
file:// path is not evidence of internal storage. It now reads the path:
the printer's model cache under /userdata is a genuine skip, anything
else is unknown and sweeps, which is what it did before. Unknown rather
than external on purpose -- the empty-slot check still runs ahead of it,
so a file:// print on a printer with nothing in the slot reports the
missing card instead of sweeping for something that cannot be there.

The user-facing copy shipped this morning is corrected in the same
change, because it was written before we understood how Bambu Studio
actually chooses. Its Print button always uses internal memory; only Send
offers Cache or External, and that defaults to Cache too. So the advice
now leads with the two routes that take one step -- start the print from
Bambuddy, or slice in OrcaSlicer -- and offers Send-with-External and a
separate print start as the way to stay in Bambu Studio. The earlier
wording named no remedy at all and blamed the printer's firmware for a
choice the slicer makes. Banner and diagnostic, thirteen locales, README,
bundle rebuilt.
This commit is contained in:
maziggy
2026-08-17 11:24:53 +02:00
parent 034f8ac2c3
commit 2c7df27d1a
19 changed files with 146 additions and 47 deletions
@@ -77,6 +77,71 @@ class TestUrlScheme:
"""None is a third answer and must not collapse into False."""
assert url_is_external_storage(url) is None
class TestFileScheme:
"""A print of a file that was already on the printer.
``file://`` is what the printer reports for a reprint from its own screen,
from Handy, or after a slicer sends to storage and then prints. Measured on
an H2D 2026-08-17: ``file:///media/usb0/foobar.gcode.3mf`` while that exact
file was listable and downloadable over FTPS. Reading it as internal storage
skipped the sweep and produced an archive with no 3MF, for a file sitting
right there -- and it did so on every model, not just the H2 series.
"""
@pytest.mark.parametrize(
"url",
[
"file:///media/usb0/foobar.gcode.3mf",
"file:///media/sdcard/Benchy.gcode.3mf",
"file:///media/usb0/timelapse/video.mp4",
],
)
def test_an_external_mount_is_not_evidence_of_internal_storage(self, url):
"""None, not True: the path is good reason to look, and looking is what
the caller's default already does."""
assert url_is_external_storage(url) is None
def test_the_model_cache_is_internal(self):
"""``/userdata/model/history/<name>`` is where the printer's own file
listing puts cached models, and port 990 does not serve it."""
assert url_is_external_storage("file:///userdata/model/history/Cube.gcode.3mf") is False
def test_an_unrecognised_path_sweeps_rather_than_skips(self):
"""Skip only on positive evidence -- a path we do not know is not that."""
assert url_is_external_storage("file:///somewhere/new/Cube.3mf") is None
def test_the_sweep_runs_for_a_file_on_the_card(self):
"""The regression in one assertion."""
state = FakeState(
current_project_url="file:///media/usb0/foobar.gcode.3mf",
sdcard=True,
sdcard_reported=True,
)
assert print_file_reachable_over_ftp(state).reachable is True
def test_an_empty_slot_still_wins(self):
"""With nothing in the slot the file cannot be on it, whatever the path
says -- and the operator gets the reason they can act on."""
state = FakeState(
current_project_url="file:///media/usb0/foobar.gcode.3mf",
sdcard=False,
sdcard_reported=True,
)
verdict = print_file_reachable_over_ftp(state)
assert verdict.reachable is False
assert verdict.reason == REASON_NO_EXTERNAL_STORAGE
def test_the_model_cache_still_skips(self):
state = FakeState(
current_project_url="file:///userdata/model/history/Cube.gcode.3mf",
sdcard=True,
sdcard_reported=True,
)
verdict = print_file_reachable_over_ftp(state)
assert verdict.reachable is False
assert verdict.reason == REASON_INTERNAL_STORAGE
@pytest.mark.parametrize("url", [12345, [], {}, object()])
def test_a_non_string_url_declines_too(self, url):
"""The value arrives straight off the wire, so it is whatever the