| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 |
- """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
|