mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Say why a drying cycle ended when the firmware cuts it short (#2770)
An H2D started a twelve-hour PETG dry at 65 degC and the AMS gave up on it
twenty minutes in, with 700 of the 720 minutes still on the clock. It cooled,
humidity climbed back over the threshold, auto-drying started another
twelve-hour cycle, and that one went the same way; the reporter's AMS
temperature history shows the loop running all morning.
The log had one line for it: "AMS 0 drying complete", which is exactly what it
says for a dry that ran its full twelve hours. Nothing in a support bundle told
the two apart, and the one number that does -- the time still remaining -- was
written into that line as the previous value, where it reads like a duration
rather than a shortfall. The reporter took 700 for seconds and concluded the
cycle had lasted twelve minutes.
Bambuddy did not stop that cycle; every stop it sends is logged with the full
outgoing command and there was none. So ending it was the printer's decision,
and the account of why lives in three things already received and parsed and
never written down: the drying phase and sub-phase from the AMS info hex, the
per-unit dry_sf_reason constraint codes, and the live HMS errors.
A cycle that ends with most of its countdown left now logs all three alongside
how much of the requested duration ran. One that reaches its duration keeps the
single line it has always had. A stop Bambuddy sent is named as ours -- it is
short of its duration too, and on the telemetry alone is indistinguishable from
the firmware abandoning the cycle, so without tracking it the print-takes-priority
stop and the Stop button would both have been blamed on the printer.
Diagnostics only. Nothing about when drying starts or stops has changed, and the
restart loop is not addressed: what the firmware objects to has to be established
before Bambuddy can sensibly decide how long to wait before trying again.
This commit is contained in:
@@ -4044,13 +4044,13 @@ class TestSendDryingCommand:
|
||||
def test_start_caches_target_for_badge(self, mqtt_client):
|
||||
"""mode=1 send populates _drying_targets so the badge can render it."""
|
||||
mqtt_client.send_drying_command(ams_id=2, temp=65, duration=12, mode=1, filament="PETG")
|
||||
assert mqtt_client._drying_targets[2] == {"filament": "PETG", "temp": 65}
|
||||
assert mqtt_client._drying_targets[2] == {"filament": "PETG", "temp": 65, "duration_hours": 12}
|
||||
|
||||
def test_start_overwrites_prior_target_for_same_ams(self, mqtt_client):
|
||||
"""A second start on the same AMS replaces the cached target."""
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=55, duration=4, mode=1, filament="PLA")
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=70, duration=6, mode=1, filament="ABS")
|
||||
assert mqtt_client._drying_targets[0] == {"filament": "ABS", "temp": 70}
|
||||
assert mqtt_client._drying_targets[0] == {"filament": "ABS", "temp": 70, "duration_hours": 6}
|
||||
|
||||
def test_stop_clears_target(self, mqtt_client):
|
||||
"""mode=0 send drops the cache so the badge stops showing the target."""
|
||||
@@ -4065,7 +4065,7 @@ class TestSendDryingCommand:
|
||||
mqtt_client.send_drying_command(ams_id=128, temp=80, duration=6, mode=1, filament="PA-CF")
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=0, duration=0, mode=0)
|
||||
assert 0 not in mqtt_client._drying_targets
|
||||
assert mqtt_client._drying_targets[128] == {"filament": "PA-CF", "temp": 80}
|
||||
assert mqtt_client._drying_targets[128] == {"filament": "PA-CF", "temp": 80, "duration_hours": 6}
|
||||
|
||||
|
||||
class TestStartPrintAmsMapping:
|
||||
@@ -6120,6 +6120,89 @@ class TestDryingCompleteCallback:
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
|
||||
assert mqtt_client._drying_events == [0]
|
||||
|
||||
def test_early_end_logs_firmware_reason_codes(self, mqtt_client, caplog):
|
||||
"""#2770 — a 12-hour cycle the firmware abandoned 20 minutes in logged
|
||||
only 'drying complete', so the report carried no evidence of why. An
|
||||
early end now names the shortfall and the reason fields we already
|
||||
parse: phase, sub-phase, cannot-dry codes and live HMS."""
|
||||
from backend.app.services.bambu_mqtt import HMSError
|
||||
|
||||
mqtt_client.state.hms_errors = [
|
||||
HMSError(code="0x2000003", attr=0x07008000, module=7, severity=2, full_code="0700800002000003")
|
||||
]
|
||||
mqtt_client._client = MagicMock()
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=65, duration=12, mode=1, filament="PETG")
|
||||
mqtt_client._handle_ams_data(
|
||||
{"ams": [{"id": "0", "dry_time": 700, "info": "10002123", "dry_sf_reason": [1], "tray": []}]}
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger="backend.app.services.bambu_mqtt"):
|
||||
mqtt_client._handle_ams_data(
|
||||
{"ams": [{"id": "0", "dry_time": 0, "info": "10002103", "dry_sf_reason": [1], "tray": []}]}
|
||||
)
|
||||
|
||||
assert mqtt_client._drying_events == [0]
|
||||
message = "\n".join(r.getMessage() for r in caplog.records)
|
||||
assert "drying ended early" in message
|
||||
# The shortfall, against the duration we asked the firmware for.
|
||||
assert "700 of 720 minutes" in message
|
||||
# dry_status 0 (Off) and dry_sub_status 0 from info hex 10002103.
|
||||
assert "dry_status=0" in message
|
||||
assert "dry_sub_status=0" in message
|
||||
# InsufficientPower, and the AMS heater-fan HMS that goes with it.
|
||||
assert "dry_sf_reason=[1]" in message
|
||||
assert "0700800002000003" in message
|
||||
|
||||
def test_early_end_without_a_cached_target_still_logs(self, mqtt_client, caplog):
|
||||
"""A cycle Bambuddy did not start — from the printer's screen, from
|
||||
Studio, or from before a restart — has no cached duration to compare
|
||||
against. The remaining time alone still proves it was cut short, so the
|
||||
reason codes must be logged rather than withheld for lack of a target."""
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 480, "tray": []}]})
|
||||
with caplog.at_level(logging.INFO, logger="backend.app.services.bambu_mqtt"):
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
|
||||
|
||||
message = "\n".join(r.getMessage() for r in caplog.records)
|
||||
assert "drying ended early" in message
|
||||
assert "480 of ? minutes" in message
|
||||
assert "hms=none" in message
|
||||
|
||||
def test_stop_we_sent_is_not_blamed_on_the_firmware(self, mqtt_client, caplog):
|
||||
"""A stop Bambuddy sends — print takes priority, or the user's Stop
|
||||
button — also ends the cycle far short of its duration, which on the
|
||||
telemetry alone looks exactly like the firmware abandoning it. It must
|
||||
be named as ours rather than reported as an unexplained early end."""
|
||||
mqtt_client._client = MagicMock()
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=65, duration=12, mode=1, filament="PETG")
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 700, "tray": []}]})
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=0, duration=0, mode=0)
|
||||
with caplog.at_level(logging.INFO, logger="backend.app.services.bambu_mqtt"):
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
|
||||
|
||||
message = "\n".join(r.getMessage() for r in caplog.records)
|
||||
assert "drying stopped by Bambuddy" in message
|
||||
assert "ended early" not in message
|
||||
# And the attribution is consumed, so a later firmware-ended cycle on
|
||||
# the same unit is not credited to a stop we sent hours earlier.
|
||||
mqtt_client.send_drying_command(ams_id=0, temp=65, duration=12, mode=1, filament="PETG")
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 700, "tray": []}]})
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.INFO, logger="backend.app.services.bambu_mqtt"):
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
|
||||
assert "drying ended early" in "\n".join(r.getMessage() for r in caplog.records)
|
||||
|
||||
def test_cycle_that_runs_to_term_keeps_the_plain_completion_log(self, mqtt_client, caplog):
|
||||
"""The countdown of a cycle that finishes normally is all but exhausted
|
||||
when it drops to 0. Nothing needs explaining, so it keeps the one-line
|
||||
message it has always had — the early-end diagnostics must not become
|
||||
noise on every completed dry."""
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 1, "tray": []}]})
|
||||
with caplog.at_level(logging.INFO, logger="backend.app.services.bambu_mqtt"):
|
||||
mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
|
||||
|
||||
message = "\n".join(r.getMessage() for r in caplog.records)
|
||||
assert "drying complete (dry_time 1 → 0)" in message
|
||||
assert "ended early" not in message
|
||||
|
||||
|
||||
class TestPrintRunningObservedCallback:
|
||||
"""#1485 follow-up: on_print_running_observed fires the FIRST time we
|
||||
|
||||
Reference in New Issue
Block a user