mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
The alert compares against ams_temp_fair, the same threshold that colours the printer card, which defaults to 35C. Drying deliberately runs at 45C for PLA, 65C for PETG and up to 85C on an AMS-HT, and the alert repeats once an hour for as long as the condition holds, so a twelve-hour dry sent twelve notifications about a temperature the user chose. It then kept sending them while the unit cooled back down, which is the half the reporter confirmed on an AMS 2 Pro and an H2C. Dispatch now consults the drying state the firmware already reports. dry_time alone is not enough: it reads 0 through the cooling phase that closes a cycle, so dry_status -- info bits 4-7, already parsed for the drying-complete edge -- carries the rest. That constant moves out of bambu_mqtt into a leaf util rather than being duplicated; drying_preflight would have been the natural home, but it imports printer_manager, which imports bambu_mqtt, and bambu_mqtt is one of the callers. The cool-down afterwards is held by a latch released as soon as the unit reads back at or below the threshold, rather than after a fixed delay, so a 65C cycle in a cold basement and a 45C one in a warm room each get the time they actually need. A two-hour cap bounds the one case the latch cannot resolve on its own -- a unit that never returns below the threshold -- and since such a unit would have been alarming with no drying involved, releasing there restores the ordinary behaviour instead of inventing a new alert. Two exclusions are deliberate. Humidity is untouched, because during drying that reading falling is the whole point. And dry_status 6, HeatOutOfControl, is kept out of the active set: an AMS that has lost thermal control is exactly when the alert should still arrive, so it must never read as expected heat. A cycle plus its cool-down outlasts a restart, so the latch is a settings row rather than a dict beside _ams_alarm_cooldown -- the internal timestamp-row pattern support.py already uses. It is read once per pass and written back only when a unit changed it. Stamps ahead of now are clamped on read, since a box whose clock jumps backwards writes them and suppression is measured as now minus the stamp; without the clamp the cap would measure from a moment that has not happened yet and hold the alert quiet for the skew on top of it. No new setting. The reporter was offered the opt-out checkbox they asked for and said they would not want it if the alert simply never fired during drying.
89 lines
3.9 KiB
Python
89 lines
3.9 KiB
Python
"""Shared reading of the firmware's own AMS drying state.
|
|
|
|
Kept as a leaf module on purpose. ``drying_preflight`` would be the natural
|
|
home, but it imports ``printer_manager``, which imports ``bambu_mqtt`` — and
|
|
``bambu_mqtt`` is one of the callers here, so putting these there would close an
|
|
import cycle. Nothing in this module imports from the app.
|
|
"""
|
|
|
|
from collections.abc import Mapping
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
# ``dry_status`` is bits 4-7 of the per-AMS ``info`` hex string (BambuStudio
|
|
# DevFilaSystem.cpp): 0=Off, 1=Checking, 2=Drying, 3=Cooling, 4=Stopping,
|
|
# 5=Error, 6=HeatOutOfControl, 7=PrdTesting. Only the first three mean a cycle
|
|
# is still live.
|
|
#
|
|
# 4 (Stopping) and 5 (Error) are excluded because the cycle is over or ending.
|
|
# 6 (HeatOutOfControl) is excluded deliberately and for a different reason: an
|
|
# AMS that has lost thermal control is exactly when a high-temperature alarm
|
|
# should still reach the user, so it must never read as "expected heat".
|
|
ACTIVE_DRY_STATUSES = frozenset({1, 2, 3}) # Checking, Drying, Cooling
|
|
|
|
|
|
def is_drying_active(ams_data: Any) -> bool:
|
|
"""True when this AMS unit reports a drying cycle in progress.
|
|
|
|
Two independent signals, because neither alone is sufficient. ``dry_time``
|
|
is minutes remaining and reads 0 through the cooling phase that closes a
|
|
cycle; ``dry_status`` covers that phase but is only present when the
|
|
firmware sent a parseable ``info`` field.
|
|
"""
|
|
if not isinstance(ams_data, Mapping):
|
|
return False
|
|
try:
|
|
if int(ams_data.get("dry_time") or 0) > 0:
|
|
return True
|
|
except (TypeError, ValueError):
|
|
pass # Unparseable countdown — fall through to the phase field
|
|
try:
|
|
return int(ams_data["dry_status"]) in ACTIVE_DRY_STATUSES
|
|
except (KeyError, TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
def temperature_alarm_suppressed(
|
|
*,
|
|
drying_active: bool,
|
|
temperature: float | None,
|
|
threshold: float,
|
|
latched_at: datetime | None,
|
|
now: datetime,
|
|
grace_minutes: int,
|
|
) -> tuple[bool, datetime | None]:
|
|
"""Decide whether to hold back the AMS high-temperature alarm (#1802).
|
|
|
|
Drying heats an AMS far past the alarm threshold by design — 45 C for PLA,
|
|
65 C for PETG, up to 85 C on an AMS-HT, against a default threshold of
|
|
35 C — so without this the alarm fires once an hour for the length of the
|
|
cycle and keeps going while the unit cools back down.
|
|
|
|
Returns ``(suppress, latched_at)``. The second element is the latch to
|
|
persist: a timestamp while suppression is in force, ``None`` to clear it.
|
|
|
|
Suppression is released as soon as the unit reads back at or below the
|
|
threshold rather than after a fixed delay, so a 65 C cycle in a cold
|
|
basement and a 45 C one in a warm room each get exactly the cool-down they
|
|
need. ``grace_minutes`` only bounds the case where the unit never returns
|
|
below the threshold at all — and a unit that stays that hot would have been
|
|
alarming with no drying involved, so releasing there restores the ordinary
|
|
behaviour instead of inventing a new alert.
|
|
"""
|
|
if drying_active:
|
|
return True, now
|
|
if latched_at is None:
|
|
return False, None
|
|
# Back at a normal storage temperature: the cool-down is over. Note this is
|
|
# also the only path that can clear the latch promptly, so it is checked
|
|
# before the cap.
|
|
if temperature is not None and temperature <= threshold:
|
|
return False, None
|
|
# ``latched_at`` is never in the future: the caller either just stamped it
|
|
# with this ``now`` or read it back through a loader that clamps. A future
|
|
# stamp would make this difference negative and hold suppression for the
|
|
# skew on top of the cap, which is why the clamp lives at the read.
|
|
if now - latched_at >= timedelta(minutes=grace_minutes):
|
|
return False, None
|
|
return True, latched_at
|