mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
fix(printers): recover MQTT sessions that stopped reconnecting (#2732)
The reporter's printer lost its session to a keep-alive timeout at 02:19
and did not come back until 11:24 — nine hours offline, with the web UI
open throughout.
check_staleness() was never going to catch it. Its first line is
`if self.state.connected and self.is_stale()`, so it only ever handles the
half-broken session that is still connected but has gone quiet. This
client had connected=False from 02:19:42 (the offline notification fired a
minute later), so every call returned immediately, and paho's own retry was
the only thing left watching. When that stopped making progress nothing
noticed.
Adds a sweep every 60s that rebuilds a client when all four hold: it is
disconnected, it had a working session before, it has been silent for five
minutes, and its MQTT port still answers. The port check is what keeps this
from becoming a nuisance — a switched-off printer is left to paho, so a
farm powering down overnight causes no client churn and no log spam. The
five-minute grace sits well past the 60s stale timeout and the 30s max
reconnect backoff, so a session recovering on its own is never interrupted.
The rebuild goes through force_reconnect_stale_session from async context,
which takes the hard-reset path: fresh client_id and paho's QoS 1 queue
dropped, so a project_file left unacked on the dead session cannot replay
into the new one and trip 0500_4003 (#1136). Rate-limited per printer,
cooldown cleared when the printer returns, and the sweep continues past a
client that throws rather than abandoning the rest of the farm. The log
line names how long the printer was gone and the last connect error, so a
session that dies repeatedly leaves a trail.
check_port gains a public alias in printer_diagnostic rather than having
the watchdog reach for the private name.
Also corrects the Developer Mode path added in the previous commit: the
wiki documents it under Settings > Network, not Settings > General. The
menu path is dropped from the translated string entirely, since it varies
by model and firmware and the wiki carries the detail.
This commit is contained in:
@@ -35,6 +35,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
|
||||
### Fixed
|
||||
- **A printer refusing Bambuddy's commands looked healthy, and its queue failed with the wrong advice (#2732, reporter @hennischd)** — Uploads succeeded, the printer echoed the job back, then sat idle for 270 seconds and the job was re-uploaded twice more before failing with a message about SD cards. Temperature changes returned success and did nothing. The connection diagnostic passed every check, and the support bundle said Developer Mode was on. **Root cause.** The printer was rejecting every control command and saying so: HMS `0500-0500-0001-0007`, "MQTT command verification failed" — the firmware's authorization check, which Bambu Lab documents Developer Mode as the way to disable. Nothing in Bambuddy connected that to anything. The error itself was received and then discarded by the frontend, because this code's meaning lives in bits that Bambuddy's short-code form throws away: it collapses to `0500_0007`, matches no catalog entry, and uncatalogued errors without firmware actions are filtered out of the badge count and the error list. Meanwhile the Developer Mode probe reads any response that isn't an explicit refusal as confirmation, and this firmware answers the probe with an empty result while refusing everything else — so Bambuddy inferred a healthy printer from a non-answer, and reported that inference as a passing diagnostic. **Fix.** HMS codes are now looked up by their full identifier before the short form, so this error survives to the screen, shows the four-group code the printer's own display shows, and carries the fix rather than Bambu's "update Studio or Handy" (which does not apply to a print sent from Bambuddy). The error is treated as authoritative about the printer's state: it sets Developer Mode to off regardless of what the probe concluded, which makes the diagnostic and the support bundle report the real situation, and it clears itself when the printer stops reporting it, so enabling Developer Mode and restarting is picked up without restarting Bambuddy. The probe no longer reads an inconclusive answer as confirmation — it reports what it knows, which for this firmware is nothing. A queue item whose command is rejected now fails on the first attempt naming the code and the fix, instead of spending three uploads and fifteen minutes of a farm's upload capacity to arrive at the wrong conclusion; a print that is visibly running is never touched, whatever HMS is lingering. Separately, the log hint suggesting a wrong or mis-cased serial number no longer fires in the moment after a reconnect, when the report counter it reads has just been reset and proves nothing — it cost this reporter a detour through their serial number on a printer whose serial was correct. Translated in all locales. Covered by tests for the code surviving the filter, the display form, the developer-mode override and its self-clearing, the inconclusive probe, first-attempt failure, the running-print guard and the suppressed hint.
|
||||
- **A printer that dropped off MQTT could stay offline indefinitely (#2732, reporter @hennischd)** — In the same bundle, the printer lost its MQTT session to a keep-alive timeout at 02:19 and did not come back until 11:24 — nine hours offline, with the web UI open the whole time. Bambuddy's stale-session detector only covers the other failure: a session that is still connected but has gone quiet. Once the connection is *down*, it returns immediately and the MQTT library's own retry is the only thing still watching; when that stops making progress, nothing notices. Bambuddy now runs a backstop sweep every minute. A printer that had a working session, has been silent for five minutes, and still answers on its MQTT port gets its client rebuilt from scratch with a fresh session — which also drops any command left unacknowledged on the dead one, so it cannot replay into the new session. Printers that are simply switched off are left alone: the port check tells the two apart, so there is no client churn and no nightly log spam for a farm that powers down. The rebuild is rate-limited per printer and never touches a connected one, and the log line names how long the printer was gone and what the last connection error was, so a session that dies repeatedly leaves a trail. Covered by tests for the recovery itself, the grace period, the switched-off case, the retry interval, and a farm sweep continuing past a printer that throws.
|
||||
- **Support bundles reported Obico as monitoring no printers when it was monitoring all of them (#2733)** — The bundle read the list of monitored printers with a parser that split on commas and treated an empty value as "none". The setting is a JSON array, and empty means *all printers* — which is the default. So a working default Obico setup showed `obico_enabled: false` against every printer in its own support bundle, sending anyone reading it down the wrong path. The bundle now parses the setting the way the detection service does, and takes the global on/off switch into account.
|
||||
- **A slot on Generic PLA offered only one K profile, however many the printer held (#2710, reporter @tommyboy180)** — The printer's Flow Dynamics table held nine calibrations, all of them under Generic PLA; Configure AMS Slot offered exactly one, the one already bound to the slot, and after a slot reset it offered none at all. The only way to assign the others was Bambu Studio. **Root cause.** Two independent faults, both triggered by picking a built-in generic preset. First, K profiles are matched to the selected preset by filament ID, but matches on Bambu's generic IDs (`GFL99` for Generic PLA, `GFG99` for Generic PETG, and so on) were deliberately thrown away as too broad — and since the comparison already requires both sides to carry the *same* ID, that exclusion could only ever fire when the chosen preset was itself the generic one, which is precisely the case where the match is right. Second, the matcher read the leading "Generic" in "Generic PLA" as a manufacturer, and so demanded the word "Generic" appear in the K profile's name; no real profile has it, which killed the name-based fallback as well. The one profile that did show up came from an unrelated safety net that always surfaces the slot's active profile, and a reset slot has none — hence the empty list. **Fix.** A K profile whose filament ID equals the selected preset's now matches, generic or not: the printer keeps one calibration table per filament ID, so a slot on Generic PLA offers everything calibrated under Generic PLA, whatever the user named those entries — "Dark Brown", "Marble", "Glow" and the rest now all appear. "Generic" is no longer treated as a brand, so profiles still match by material when a printer reports no filament ID at all. Since no name-and-ID matcher can be perfect against profile names the user invents, the picker now also lists every remaining profile on the printer under **Other K profiles on this printer**, so a profile that exists can always be selected; picking one works exactly as before, because Bambuddy already realigns the slot's filament context to whichever profile is chosen. The same generic-ID rule now applies to the spool form's K profile suggestions, guarded so it can never cross materials — a PETG spool is never offered PLA calibrations — and so that a spool naming its own brand keeps its brand-specific suggestions. Translated in all locales. Covered by tests for the reporter's nine-profile printer, the reset slot, printers that send no filament ID, the other-profiles group and applying a profile from it, and by guards that a brand preset still does not sweep in generic profiles.
|
||||
- **The print-complete photo showed the toolhead still printing, three minutes before the print ended (#2547, reporter @anthonyma94)** — The Discord photo caught the model mid-print with the head over it, instead of the finished print. **Root cause.** Bambuddy fired the photo the moment `layer_num` reached `total_layer_num`. That edge is the moment the printer *starts* its final layer, not the moment it finishes it: on the reporter's H2C it arrived at 92% with two minutes of print still to run, and the last layer took three minutes and seventeen seconds including a filament change. Worse, that trigger latched, which locked out both of the triggers that fire at a genuine end of print — so on printers that never report an end-of-print filament unload (H2C and A1 Mini confirmed) there was no way back to a correct photo. **Fix.** The last-layer trigger is gone. The photo is taken when the print reports itself finished, which is a signal every model sends and which lands after the toolhead has parked. Since the printer's own end G-code drops the plate about 100 mm just before that, Bambuddy now raises it back to just above the last printed layer, takes the photo, then lowers it again — restoring the framing asked for in #1145, #1397 and #1565. The plate move is an absolute Z to a height the toolhead occupied seconds earlier, so it stays inside the travel limits and keeps the nozzle above the part, and it is skipped outright unless Bambuddy can confirm the height belongs to this print: the sliced file is matched by print name, and its layer count is cross-checked against the layer count the printer reported. It is also skipped when another job is queued, when the printer has moved on, and when the new **Restore plate for finish photo** setting is off. Prints whose End G-code Bambuddy injected — SwapMod plate swaps and similar — are detected automatically and keep using a frame from during the print, since their model has left the bed by then (#1867); that frame now also refreshes through the final layer instead of freezing when it began. Prints that record a timelapse normally source the photo from the video's last frame and need no move at all, but when the video has not transferred in time — the usual outcome on P1-series — the live photo that ships in the notification now gets the same plate restore, so it is no longer a shot of an empty-looking lowered bed. The `stg_cur=22` trigger is left in place for any firmware that does emit it, but the bundle survey above still applies — in practice every model reaches the finish-state path. Translated in all locales; wiki updated. Covered by backend tests for the removed trigger, the two surviving ones, the plate move and every condition that suppresses it, and by frontend tests for the new setting.
|
||||
|
||||
@@ -6661,6 +6661,130 @@ def stop_spoolbuddy_watchdog():
|
||||
logging.getLogger(__name__).info("SpoolBuddy watchdog stopped")
|
||||
|
||||
|
||||
# Dead-MQTT-session recovery
|
||||
#
|
||||
# check_staleness() covers the "connected but silent" half-broken session. It
|
||||
# does nothing once ``state.connected`` is False, and paho's own auto-reconnect
|
||||
# is the only thing left watching at that point. When paho stops making
|
||||
# progress there is no backstop at all: the #2732 bundle has a P1S drop on a
|
||||
# keep-alive timeout at 02:19 and not reconnect until 11:24 — nine hours
|
||||
# offline with the UI open the whole time, recovered only when something
|
||||
# happened to nudge it.
|
||||
#
|
||||
# This loop is that backstop. It only touches printers that had a working
|
||||
# session and lost it, and only when the MQTT port still answers — a printer
|
||||
# that is simply switched off is left to paho, since rebuilding a client
|
||||
# against an unreachable host achieves nothing and would fill the log every
|
||||
# night.
|
||||
_connection_watchdog_task: asyncio.Task | None = None
|
||||
CONNECTION_WATCHDOG_INTERVAL = 60
|
||||
# How long a printer must have been silent before we stop trusting paho.
|
||||
# Comfortably above STALE_TIMEOUT (60 s) and the max reconnect backoff (30 s),
|
||||
# so a session that is recovering on its own is never interrupted.
|
||||
CONNECTION_WATCHDOG_OFFLINE_GRACE = 300
|
||||
# Per-printer floor between rebuild attempts.
|
||||
CONNECTION_WATCHDOG_RETRY_INTERVAL = 300
|
||||
_connection_watchdog_last_attempt: dict[int, float] = {}
|
||||
|
||||
|
||||
async def _recover_dead_printer_sessions() -> int:
|
||||
"""Rebuild MQTT clients that have been offline too long to still be trying.
|
||||
|
||||
Returns the number of printers a rebuild was attempted for (for tests and
|
||||
for the caller's logging). Never raises: one unreachable printer must not
|
||||
stop the sweep for the rest of the farm.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
from backend.app.services.printer_diagnostic import PORT_MQTT, check_port
|
||||
|
||||
now = time.monotonic()
|
||||
recovered = 0
|
||||
|
||||
for printer_id, client in list(printer_manager._clients.items()):
|
||||
try:
|
||||
if client.state.connected:
|
||||
_connection_watchdog_last_attempt.pop(printer_id, None)
|
||||
continue
|
||||
|
||||
# Time since the last inbound message is the age of the last known
|
||||
# good session — no extra bookkeeping needed, and it is the same
|
||||
# clock is_stale() reads. 0 means this client has never had one:
|
||||
# that is the initial-connect path, where paho retrying is the
|
||||
# correct and only behaviour, so leave it be.
|
||||
last_msg = client._last_message_time
|
||||
if not last_msg:
|
||||
continue
|
||||
offline_for = time.time() - last_msg
|
||||
if offline_for < CONNECTION_WATCHDOG_OFFLINE_GRACE:
|
||||
continue
|
||||
|
||||
last_attempt = _connection_watchdog_last_attempt.get(printer_id)
|
||||
if last_attempt is not None and now - last_attempt < CONNECTION_WATCHDOG_RETRY_INTERVAL:
|
||||
continue
|
||||
|
||||
if not await check_port(client.ip_address, PORT_MQTT):
|
||||
# Switched off, unplugged, or off the network. Paho's retry is
|
||||
# the right handler; say so at debug level and move on.
|
||||
logger.debug(
|
||||
"[#2732] Printer %s offline for %.0fs and its MQTT port is not answering "
|
||||
"— leaving the reconnect to paho",
|
||||
printer_id,
|
||||
offline_for,
|
||||
)
|
||||
_connection_watchdog_last_attempt[printer_id] = now
|
||||
continue
|
||||
|
||||
_connection_watchdog_last_attempt[printer_id] = now
|
||||
recovered += 1
|
||||
logger.warning(
|
||||
"[#2732] Printer %s has been offline for %.0fs but answers on MQTT port %d — "
|
||||
"rebuilding the client with a fresh session (last connect error: %s)",
|
||||
printer_id,
|
||||
offline_for,
|
||||
PORT_MQTT,
|
||||
client.last_connect_error or "none recorded",
|
||||
)
|
||||
# Async context, so this takes the hard-reset path: fresh client_id,
|
||||
# paho's QoS 1 queue dropped. That matters — a project_file left
|
||||
# unacked on the dead session would otherwise replay into the new
|
||||
# one and trip 0500_4003 on the printer (#1136).
|
||||
client.force_reconnect_stale_session(f"offline for {offline_for:.0f}s, port still answering")
|
||||
except Exception as e:
|
||||
logger.warning("[#2732] Connection watchdog failed for printer %s: %s", printer_id, e)
|
||||
|
||||
return recovered
|
||||
|
||||
|
||||
async def _connection_watchdog_loop():
|
||||
logger = logging.getLogger(__name__)
|
||||
# Let the initial connects settle before judging anyone offline.
|
||||
await asyncio.sleep(CONNECTION_WATCHDOG_OFFLINE_GRACE)
|
||||
while True:
|
||||
try:
|
||||
await _recover_dead_printer_sessions()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning("Connection watchdog sweep failed: %s", e)
|
||||
await asyncio.sleep(CONNECTION_WATCHDOG_INTERVAL)
|
||||
|
||||
|
||||
def start_connection_watchdog():
|
||||
global _connection_watchdog_task
|
||||
if _connection_watchdog_task is None:
|
||||
_connection_watchdog_task = asyncio.create_task(_connection_watchdog_loop())
|
||||
logging.getLogger(__name__).info("Printer connection watchdog started")
|
||||
|
||||
|
||||
def stop_connection_watchdog():
|
||||
global _connection_watchdog_task
|
||||
if _connection_watchdog_task:
|
||||
_connection_watchdog_task.cancel()
|
||||
_connection_watchdog_task = None
|
||||
_connection_watchdog_last_attempt.clear()
|
||||
logging.getLogger(__name__).info("Printer connection watchdog stopped")
|
||||
|
||||
|
||||
# Camera stream orphan cleanup
|
||||
_camera_cleanup_task: asyncio.Task | None = None
|
||||
CAMERA_CLEANUP_INTERVAL = 60
|
||||
@@ -7218,6 +7342,9 @@ async def lifespan(app: FastAPI):
|
||||
# Start camera stream orphan cleanup
|
||||
start_camera_cleanup()
|
||||
|
||||
# Start the backstop for MQTT sessions paho has stopped recovering (#2732)
|
||||
start_connection_watchdog()
|
||||
|
||||
# One-shot sweep for timelapse session directories orphaned by a crash
|
||||
# or restart that happened mid-print (in-memory session tracking can't
|
||||
# survive that, and nothing else reaps the leftover frames/output file)
|
||||
@@ -7270,6 +7397,7 @@ async def lifespan(app: FastAPI):
|
||||
stop_runtime_tracking()
|
||||
stop_spoolbuddy_watchdog()
|
||||
stop_camera_cleanup()
|
||||
stop_connection_watchdog()
|
||||
from backend.app.services.loop_watchdog import stop_loop_watchdog
|
||||
|
||||
stop_loop_watchdog()
|
||||
|
||||
@@ -57,6 +57,12 @@ async def _check_port(ip: str, port: int, timeout: float = _PORT_PROBE_TIMEOUT)
|
||||
return False
|
||||
|
||||
|
||||
# Public alias. The connection watchdog probes the MQTT port before rebuilding a
|
||||
# client, so it can tell "the printer is switched off" (leave it alone, paho will
|
||||
# keep retrying) from "the printer is answering but our session is dead" (#2732).
|
||||
check_port = _check_port
|
||||
|
||||
|
||||
def _auth_reason_params(reason: str | None) -> dict:
|
||||
"""Map a client's CONNACK-refusal slug onto the check's `params.reason`.
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Tests for the dead-MQTT-session watchdog (#2732).
|
||||
|
||||
``check_staleness()`` guards the "connected but silent" session and returns
|
||||
immediately once ``state.connected`` is False — from there, paho's own
|
||||
auto-reconnect is the only thing still watching. The #2732 bundle shows what
|
||||
happens when that stops making progress: a P1S dropped on a keep-alive timeout
|
||||
at 02:19 and did not reconnect until 11:24, nine hours offline with the UI open
|
||||
throughout.
|
||||
|
||||
This watchdog is the backstop. The rules it has to keep are narrow on purpose —
|
||||
it must not interfere with a session that is recovering on its own, and it must
|
||||
not churn clients for printers that are simply switched off.
|
||||
"""
|
||||
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.app.main import (
|
||||
CONNECTION_WATCHDOG_OFFLINE_GRACE,
|
||||
CONNECTION_WATCHDOG_RETRY_INTERVAL,
|
||||
_connection_watchdog_last_attempt,
|
||||
_recover_dead_printer_sessions,
|
||||
)
|
||||
|
||||
|
||||
def _client(*, connected: bool, last_message_age: float | None, ip: str = "192.168.1.100"):
|
||||
"""Stand-in for BambuMQTTClient with only the fields the watchdog reads."""
|
||||
return SimpleNamespace(
|
||||
state=SimpleNamespace(connected=connected),
|
||||
_last_message_time=0.0 if last_message_age is None else time.time() - last_message_age,
|
||||
ip_address=ip,
|
||||
last_connect_error=None,
|
||||
force_reconnect_stale_session=MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
async def _sweep(clients: dict, *, port_open: bool = True):
|
||||
with (
|
||||
patch("backend.app.main.printer_manager._clients", clients),
|
||||
patch("backend.app.services.printer_diagnostic.check_port", AsyncMock(return_value=port_open)),
|
||||
):
|
||||
return await _recover_dead_printer_sessions()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cooldowns():
|
||||
_connection_watchdog_last_attempt.clear()
|
||||
yield
|
||||
_connection_watchdog_last_attempt.clear()
|
||||
|
||||
|
||||
class TestRebuildsDeadSessions:
|
||||
@pytest.mark.asyncio
|
||||
async def test_rebuilds_a_long_dead_session(self):
|
||||
"""The #2732 case: offline for hours, printer answering the whole time."""
|
||||
client = _client(connected=False, last_message_age=32718.0)
|
||||
|
||||
assert await _sweep({1: client}) == 1
|
||||
client.force_reconnect_stale_session.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnect_reason_names_the_duration(self):
|
||||
client = _client(connected=False, last_message_age=32718.0)
|
||||
await _sweep({1: client})
|
||||
assert "32718" in client.force_reconnect_stale_session.call_args.args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sweeps_every_printer_in_the_farm(self):
|
||||
clients = {i: _client(connected=False, last_message_age=9999.0) for i in range(1, 4)}
|
||||
assert await _sweep(clients) == 3
|
||||
|
||||
|
||||
class TestLeavesHealthyAndRecoveringSessionsAlone:
|
||||
@pytest.mark.asyncio
|
||||
async def test_connected_printer_is_untouched(self):
|
||||
client = _client(connected=True, last_message_age=99999.0)
|
||||
assert await _sweep({1: client}) == 0
|
||||
client.force_reconnect_stale_session.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inside_the_grace_period_paho_keeps_the_job(self):
|
||||
"""Below the grace window a reconnect may well be in flight; interrupting
|
||||
it would turn a self-healing blip into a forced session rebuild."""
|
||||
client = _client(connected=False, last_message_age=CONNECTION_WATCHDOG_OFFLINE_GRACE - 30)
|
||||
assert await _sweep({1: client}) == 0
|
||||
client.force_reconnect_stale_session.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_client_that_never_had_a_session_is_left_to_paho(self):
|
||||
"""No inbound message ever means this is the initial connect, where
|
||||
retrying is both correct and the only thing to do."""
|
||||
client = _client(connected=False, last_message_age=None)
|
||||
assert await _sweep({1: client}) == 0
|
||||
client.force_reconnect_stale_session.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnecting_clears_the_cooldown(self):
|
||||
"""A printer that comes back must not carry a stale cooldown into its
|
||||
next outage."""
|
||||
client = _client(connected=False, last_message_age=9999.0)
|
||||
await _sweep({1: client})
|
||||
assert 1 in _connection_watchdog_last_attempt
|
||||
|
||||
client.state.connected = True
|
||||
await _sweep({1: client})
|
||||
assert 1 not in _connection_watchdog_last_attempt
|
||||
|
||||
|
||||
class TestUnreachablePrinters:
|
||||
@pytest.mark.asyncio
|
||||
async def test_switched_off_printer_is_not_rebuilt(self):
|
||||
"""Rebuilding a client against a host that isn't answering achieves
|
||||
nothing and would log a warning per printer all night."""
|
||||
client = _client(connected=False, last_message_age=9999.0)
|
||||
assert await _sweep({1: client}, port_open=False) == 0
|
||||
client.force_reconnect_stale_session.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unreachable_printer_still_takes_the_cooldown(self):
|
||||
"""Otherwise every sweep re-probes the port of every dead printer."""
|
||||
client = _client(connected=False, last_message_age=9999.0)
|
||||
await _sweep({1: client}, port_open=False)
|
||||
assert 1 in _connection_watchdog_last_attempt
|
||||
|
||||
|
||||
class TestRetryInterval:
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_rebuild_again_within_the_interval(self):
|
||||
client = _client(connected=False, last_message_age=9999.0)
|
||||
assert await _sweep({1: client}) == 1
|
||||
assert await _sweep({1: client}) == 0
|
||||
client.force_reconnect_stale_session.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_once_the_interval_has_passed(self):
|
||||
client = _client(connected=False, last_message_age=9999.0)
|
||||
await _sweep({1: client})
|
||||
_connection_watchdog_last_attempt[1] -= CONNECTION_WATCHDOG_RETRY_INTERVAL + 1
|
||||
|
||||
assert await _sweep({1: client}) == 1
|
||||
assert client.force_reconnect_stale_session.call_count == 2
|
||||
|
||||
|
||||
class TestSweepIsFaultTolerant:
|
||||
@pytest.mark.asyncio
|
||||
async def test_one_broken_client_does_not_stop_the_others(self):
|
||||
"""A farm sweep that aborts on the first bad client would leave every
|
||||
printer after it unrecovered."""
|
||||
bad = _client(connected=False, last_message_age=9999.0)
|
||||
bad.force_reconnect_stale_session.side_effect = RuntimeError("boom")
|
||||
good = _client(connected=False, last_message_age=9999.0)
|
||||
|
||||
assert await _sweep({1: bad, 2: good}) == 2
|
||||
good.force_reconnect_stale_session.assert_called_once()
|
||||
@@ -2843,7 +2843,7 @@ export default {
|
||||
title: 'Fehler - {{name}}',
|
||||
noErrors: 'Keine Fehler',
|
||||
viewOnWiki: 'Im Bambu Lab Wiki ansehen',
|
||||
mqttVerifyFailedRemedy: 'Aktiviere den Entwicklermodus am Drucker (Einstellungen > Allgemein), starte den Drucker neu und starte den Auftrag dann erneut.',
|
||||
mqttVerifyFailedRemedy: 'Aktiviere den Entwicklermodus am Drucker, starte den Drucker neu und starte den Auftrag dann erneut.',
|
||||
unknownCode: 'Unbekannter HMS-Code — Details siehe Bambu Lab Wiki.',
|
||||
clearInstructions: 'Löschen Sie die Fehler am Drucker, um sie hier zu entfernen.',
|
||||
clearErrors: 'Fehler löschen',
|
||||
|
||||
@@ -2872,7 +2872,7 @@ export default {
|
||||
title: 'Errors - {{name}}',
|
||||
noErrors: 'No errors',
|
||||
viewOnWiki: 'View on Bambu Lab Wiki',
|
||||
mqttVerifyFailedRemedy: 'Enable Developer Mode on the printer (Settings > General), restart the printer, then start the job again.',
|
||||
mqttVerifyFailedRemedy: 'Enable Developer Mode on the printer, restart the printer, then start the job again.',
|
||||
unknownCode: 'Unknown HMS code — see the Bambu Lab wiki for details.',
|
||||
clearInstructions: 'Clear errors on the printer to dismiss them here.',
|
||||
clearErrors: 'Clear Errors',
|
||||
|
||||
@@ -2846,7 +2846,7 @@ export default {
|
||||
title: 'Errores - {{name}}',
|
||||
noErrors: 'No hay errores',
|
||||
viewOnWiki: 'Ver en la wiki de Bambu Lab',
|
||||
mqttVerifyFailedRemedy: 'Activa el modo desarrollador en la impresora (Ajustes > General), reinicia la impresora y vuelve a iniciar el trabajo.',
|
||||
mqttVerifyFailedRemedy: 'Activa el modo desarrollador en la impresora, reinicia la impresora y vuelve a iniciar el trabajo.',
|
||||
unknownCode: 'Código HMS desconocido — consulta la wiki de Bambu Lab para más detalles.',
|
||||
clearInstructions: 'Borre los errores en la impresora para descartarlos aquí.',
|
||||
clearErrors: 'Borrar errores',
|
||||
|
||||
@@ -2832,7 +2832,7 @@ export default {
|
||||
title: 'Erreurs - {{name}}',
|
||||
noErrors: 'Aucune erreur',
|
||||
viewOnWiki: 'Voir sur le Wiki Bambu Lab',
|
||||
mqttVerifyFailedRemedy: "Activez le mode developpeur sur l'imprimante (Parametres > General), redemarrez l'imprimante, puis relancez la tache.",
|
||||
mqttVerifyFailedRemedy: "Activez le mode developpeur sur l'imprimante, redemarrez l'imprimante, puis relancez la tache.",
|
||||
unknownCode: 'Code HMS inconnu — consultez le wiki Bambu Lab pour plus de détails.',
|
||||
clearInstructions: 'Effacez les erreurs sur l\'imprimante pour les retirer ici.',
|
||||
clearErrors: 'Effacer les erreurs',
|
||||
|
||||
@@ -2831,7 +2831,7 @@ export default {
|
||||
title: 'Errori - {{name}}',
|
||||
noErrors: 'Nessun errore',
|
||||
viewOnWiki: 'Vedi su Bambu Lab Wiki',
|
||||
mqttVerifyFailedRemedy: 'Attiva la modalita sviluppatore sulla stampante (Impostazioni > Generale), riavvia la stampante e avvia di nuovo il lavoro.',
|
||||
mqttVerifyFailedRemedy: 'Attiva la modalita sviluppatore sulla stampante, riavvia la stampante e avvia di nuovo il lavoro.',
|
||||
unknownCode: 'Codice HMS sconosciuto — consulta la wiki di Bambu Lab per i dettagli.',
|
||||
clearInstructions: 'Cancella gli errori sulla stampante per rimuoverli qui.',
|
||||
clearErrors: 'Cancella errori',
|
||||
|
||||
@@ -2843,7 +2843,7 @@ export default {
|
||||
title: 'エラー - {{name}}',
|
||||
noErrors: 'エラーなし',
|
||||
viewOnWiki: 'Bambu Lab Wikiで表示',
|
||||
mqttVerifyFailedRemedy: 'プリンターで開発者モードを有効にし(設定 > 一般)、プリンターを再起動してから、ジョブをもう一度開始してください。',
|
||||
mqttVerifyFailedRemedy: 'プリンターで開発者モードを有効にし、プリンターを再起動してから、ジョブをもう一度開始してください。',
|
||||
unknownCode: '不明なHMSコード — 詳細はBambu Lab Wikiを参照してください。',
|
||||
clearInstructions: 'プリンターでエラーをクリアするとここからも消えます。',
|
||||
clearErrors: 'エラーをクリア',
|
||||
|
||||
@@ -2693,7 +2693,7 @@ export default {
|
||||
title: '오류 - {{name}}',
|
||||
noErrors: '오류 없음',
|
||||
viewOnWiki: 'Bambu Lab 위키에서 보기',
|
||||
mqttVerifyFailedRemedy: '프린터에서 개발자 모드를 활성화하고(설정 > 일반) 프린터를 재시작한 다음 작업을 다시 시작하세요.',
|
||||
mqttVerifyFailedRemedy: '프린터에서 개발자 모드를 활성화하고 프린터를 재시작한 다음 작업을 다시 시작하세요.',
|
||||
unknownCode: '알 수 없는 HMS 코드 — 자세한 내용은 Bambu Lab 위키를 참조하세요.',
|
||||
clearInstructions: '오류를 해제하려면 프린터에서 오류를 지우세요.',
|
||||
clearErrors: '오류 지우기',
|
||||
|
||||
@@ -2831,7 +2831,7 @@ export default {
|
||||
title: 'Erros - {{name}}',
|
||||
noErrors: 'Nenhum erro',
|
||||
viewOnWiki: 'Ver no Bambu Lab Wiki',
|
||||
mqttVerifyFailedRemedy: 'Ative o Modo Desenvolvedor na impressora (Configuracoes > Geral), reinicie a impressora e inicie o trabalho novamente.',
|
||||
mqttVerifyFailedRemedy: 'Ative o Modo Desenvolvedor na impressora, reinicie a impressora e inicie o trabalho novamente.',
|
||||
unknownCode: 'Código HMS desconhecido — consulte o wiki da Bambu Lab para mais detalhes.',
|
||||
clearInstructions: 'Limpe os erros na impressora para descartá-los aqui.',
|
||||
clearErrors: 'Limpar Erros',
|
||||
|
||||
@@ -2685,7 +2685,7 @@ export default {
|
||||
title: "Ошибки — {{name}}",
|
||||
noErrors: "Ошибок нет",
|
||||
viewOnWiki: "Открыть в Bambu Lab Wiki",
|
||||
mqttVerifyFailedRemedy: "Включите режим разработчика на принтере (Настройки > Общие), перезагрузите принтер и запустите задание снова.",
|
||||
mqttVerifyFailedRemedy: "Включите режим разработчика на принтере, перезагрузите принтер и запустите задание снова.",
|
||||
unknownCode: "Неизвестный код HMS — подробности см. в Bambu Lab Wiki.",
|
||||
clearInstructions: "Устраните ошибки на принтере, чтобы они исчезли из этого списка.",
|
||||
clearErrors: "Очистить ошибки",
|
||||
|
||||
@@ -2847,7 +2847,7 @@ export default {
|
||||
title: 'Hatalar - {{name}}',
|
||||
noErrors: 'Hata yok',
|
||||
viewOnWiki: 'Bambu Lab Wiki\'de görüntüle',
|
||||
mqttVerifyFailedRemedy: 'Yazicida Gelistirici Modunu etkinlestirin (Ayarlar > Genel), yaziciyi yeniden baslatin ve isi tekrar baslatin.',
|
||||
mqttVerifyFailedRemedy: 'Yazicida Gelistirici Modunu etkinlestirin, yaziciyi yeniden baslatin ve isi tekrar baslatin.',
|
||||
unknownCode: 'Bilinmeyen HMS kodu — ayrıntılar için Bambu Lab wiki sayfasına bakın.',
|
||||
clearInstructions: 'Buradan kapatmak için yazıcıdaki hataları temizleyin.',
|
||||
clearErrors: 'Hataları Temizle',
|
||||
|
||||
@@ -2872,7 +2872,7 @@ export default {
|
||||
title: "Помилки - {{name}}",
|
||||
noErrors: "Помилок немає",
|
||||
viewOnWiki: "Переглянути на Bambu Lab Wiki",
|
||||
mqttVerifyFailedRemedy: "Увімкніть режим розробника на принтері (Налаштування > Загальні), перезавантажте принтер і запустіть завдання знову.",
|
||||
mqttVerifyFailedRemedy: "Увімкніть режим розробника на принтері, перезавантажте принтер і запустіть завдання знову.",
|
||||
unknownCode: "Невідомий код HMS — подробиці дивіться у вікі Bambu Lab.",
|
||||
clearInstructions: "Усуньте помилки на принтері, щоб вони зникли тут.",
|
||||
clearErrors: "Очистити помилки",
|
||||
|
||||
@@ -2831,7 +2831,7 @@ export default {
|
||||
title: '错误 - {{name}}',
|
||||
noErrors: '无错误',
|
||||
viewOnWiki: '在拓竹 Wiki 上查看',
|
||||
mqttVerifyFailedRemedy: '在打印机上启用开发者模式(设置 > 通用),重启打印机,然后重新开始该任务。',
|
||||
mqttVerifyFailedRemedy: '在打印机上启用开发者模式,重启打印机,然后重新开始该任务。',
|
||||
unknownCode: '未知 HMS 代码 — 详情请参阅拓竹 Wiki。',
|
||||
clearInstructions: '在打印机上清除错误以在此处消除它们。',
|
||||
clearErrors: '清除错误',
|
||||
|
||||
@@ -2831,7 +2831,7 @@ export default {
|
||||
title: '錯誤 - {{name}}',
|
||||
noErrors: '無錯誤',
|
||||
viewOnWiki: '在拓竹 Wiki 上檢視',
|
||||
mqttVerifyFailedRemedy: '在印表機上啟用開發者模式(設定 > 一般),重新啟動印表機,然後重新開始該工作。',
|
||||
mqttVerifyFailedRemedy: '在印表機上啟用開發者模式,重新啟動印表機,然後重新開始該工作。',
|
||||
unknownCode: '未知 HMS 代碼 — 詳情請參閱拓竹 Wiki。',
|
||||
clearInstructions: '在印表機上清除錯誤以在此處消除它們。',
|
||||
clearErrors: '清除錯誤',
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-I83QBJfM.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CrcwM7vK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-oReXTzKG.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user