Skip to content

Commit 0c065da

Browse files
fix(devmgr): auto-reconnect stale LAN MQTT and recover startup race
LAN-mode-only printers (no Bambu cloud login) had two related failure modes that left the user manually re-selecting the printer to make Studio talk to it again. This commit fixes both -- they share the same root surface (DeviceManager + TryLoadLastMachine), affect the same users, and reproduce on the same workflow, so they ship together. == Stale MQTT socket recovery (DeviceManagerRefresher::on_timer) == After idling, macOS App Nap, the local network stack, or the printer can silently drop the MQTT-over-TLS TCP session. The next publish_gcode() returns BAMBU_NETWORK_ERR_SEND_MSG_FAILED (-4) and the user has to manually re-select the printer to trigger the disconnect+reconnect path in DeviceManager::set_selected_machine. on_timer's existing keep_alive() / refresh_connection() calls are gated on is_user_login() and never run for LAN-only users. Add a parallel branch that fires when all of these hold: - obj->is_lan_mode_printer() && obj->has_access_right() - obj->is_avaliable() (bind_state == "free") - !obj->is_in_printing() (don't clobber print UI mid-print) - !obj->is_connected() (last MQTT push older than 30s) When the gate passes, re-select the same machine id. That triggers the same-id-LAN branch in set_selected_machine which runs disconnect_printer -> reset -> connect -- the same path the manual workaround takes. Throttled to one attempt per 10s, bumped only on a successful set_selected_machine so a transient false return doesn't delay the next chance to recover. == Startup-race recovery (TryLoadLastMachine via SSDP) == TryLoadLastMachine::InnerLoad fires within milliseconds of app start, before SSDP has announced the printer's current IP. If the cached user_access_dev_ip is stale (slicer_uuid rotated since pairing, or DHCP gave the printer a new IP), bind_detect returns -2 immediately, erases user_access_dev_ip, and bails. The cloud fallback also fails because the LAN printer isn't in the list yet. By the time SSDP populates localMachineList ~1-3s later, no further InnerLoad retry runs and the printer is discovered-but-not-selected (see upstream issue #9445). Add GUI_App::try_load_last_machine_on_alive(dev_id) and call it from DeviceManager::on_machine_alive whenever an SSDP packet announces a previously-paired printer. The retry's InnerLoad finds user_access_dev_ip empty (the failed first attempt erased it) and falls through to the dev->get_my_machine non-null branch in GUI_App.cpp, which calls set_selected_machine directly. No second bind_detect is spawned. The method self-filters on dev_id == get_user_last_machine() and no-ops if a machine is already selected, so per-SSDP-packet invocation is cheap. == Test plan == Stale-MQTT: idle the app >30min on macOS with a LAN-only printer. Without patch, Send to Printer returns -4; with patch, the next 1Hz refresher tick auto-reconnects and the send succeeds. Verify the log line "LAN auto-reconnect: stale MQTT socket detected for dev_id=...". Startup race: with a stale user_access_dev_ip (rotate slicer_uuid in BambuStudio.conf, or power-cycle the router so SSDP is delayed), launch Studio. Without patch the printer is never selected; with patch the SSDP packet triggers a retry and selection succeeds. Verify "try_load_last_machine_on_alive: SSDP-triggered retry for ...". == Limitations == - After studio inactivity (>15min), the first user action after wake may still see one failed send before stale-MQTT auto- reconnect runs on the next refresher tick. A subsequent send within 1s succeeds. - The 10s throttle is a function-static, not per-dev-id, so in multi-printer households a recent reconnect attempt on printer A can delay the next attempt on printer B by up to 10s. - TryLoadLastMachine's destructor joins local_bind_thread; an SSDP-triggered InnerLoad firing during app shutdown can stall the join 1-3s waiting for the bind_detect timeout. Addresses upstream issue #9445.
1 parent e8c7dc1 commit 0c065da

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/slic3r/GUI/DeviceCore/DevManager.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,33 @@ namespace Slic3r
279279
obj->erase_user_access_code();
280280
obj->erase_user_access_dev_ip();
281281
}
282+
283+
// SSDP-driven retry of last-machine restore. The startup
284+
// TryLoadLastMachine::InnerLoad path is racy for LAN-only users:
285+
// if the cached user_access_dev_ip is stale (slicer_uuid rotated
286+
// since pairing, or the printer is at a new IP) the initial
287+
// bind_detect fails before SSDP can announce the printer's
288+
// current IP, and the printer ends up in localMachineList but
289+
// never selected.
290+
//
291+
// Recovery path: by the time SSDP populates localMachineList, the
292+
// failed initial bind_detect has erased user_access_dev_ip. The
293+
// retry's InnerLoad finds the encoded IP empty and falls through
294+
// to the dev->get_my_machine(...) non-null branch (GUI_App.cpp
295+
// around line 8429), which calls set_selected_machine directly.
296+
// No second bind_detect is spawned.
297+
//
298+
// try_load_last_machine_on_alive runs on the wx UI thread (this
299+
// function is dispatched via CallAfter from the SSDP listener),
300+
// same thread as DeviceManagerRefresher::on_timer, so there is
301+
// no concurrent invocation of set_selected_machine across the
302+
// two LAN-recovery code paths in this commit.
303+
//
304+
// Called for every SSDP announcement (~5s/printer), not just
305+
// first discovery. Cheap: try_load_last_machine_on_alive
306+
// self-filters on dev_id == get_user_last_machine() and no-ops
307+
// if a machine is already selected.
308+
Slic3r::GUI::wxGetApp().try_load_last_machine_on_alive(dev_id);
282309
}
283310
catch (...) {
284311
;
@@ -909,6 +936,69 @@ namespace Slic3r
909936
}
910937
}
911938

939+
// LAN-only stale-MQTT auto-reconnect.
940+
//
941+
// For LAN-mode-only printers (no Bambu cloud login), nothing else in this
942+
// refresher runs: check_pushing() / refresh_connection() are gated on
943+
// is_user_login() above, so the keep_alive() that would otherwise probe
944+
// the MQTT session never fires. After the app sits idle long enough that
945+
// macOS App Nap, the network stack, or the printer side closes the
946+
// underlying TCP socket, the next publish_gcode() returns
947+
// BAMBU_NETWORK_ERR_SEND_MSG_FAILED (-4) and the user has to manually
948+
// re-select the printer to re-trigger the disconnect+reconnect path in
949+
// DeviceManager::set_selected_machine.
950+
//
951+
// Detect the stale-socket condition for LAN printers and run the same
952+
// reconnect path automatically. is_connected() returns false for LAN
953+
// printers when last_update_time is older than DISCONNECT_TIMEOUT (30s),
954+
// see MachineObject::is_connected in DeviceManager.cpp. Throttled to one
955+
// attempt per LAN_RECONNECT_INTERVAL_MS so a powered-off printer doesn't
956+
// get hammered.
957+
//
958+
// Skipped while a print is in progress (!is_in_printing()): MachineObject::reset()
959+
// -- which set_selected_machine's same-id-LAN branch calls -- clobbers
960+
// print_status / iot_print_status / subtask_ / print_json. Recovering MQTT
961+
// mid-print would briefly blank the user's progress UI. The print itself
962+
// continues on the printer regardless; we'll reconnect on the next tick
963+
// after the print finishes.
964+
//
965+
// Skipped when bind_state != "free" (is_avaliable()): set_selected_machine
966+
// would just return false because get_my_machine_list() filters out
967+
// occupied printers, but the throttle would still bump and we'd log
968+
// every 10s with no recovery possible.
969+
if (obj->is_lan_mode_printer() && obj->has_access_right() &&
970+
obj->is_avaliable() &&
971+
!obj->is_in_printing() &&
972+
!obj->is_connected())
973+
{
974+
constexpr int LAN_RECONNECT_INTERVAL_MS = 10 * 1000;
975+
static std::chrono::system_clock::time_point last_lan_reconnect_attempt{};
976+
const auto now = std::chrono::system_clock::now();
977+
const auto since_last_attempt =
978+
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_lan_reconnect_attempt).count();
979+
980+
if (since_last_attempt > LAN_RECONNECT_INTERVAL_MS)
981+
{
982+
BOOST_LOG_TRIVIAL(info)
983+
<< "LAN auto-reconnect: stale MQTT socket detected for dev_id="
984+
<< BBLCrossTalk::Crosstalk_DevId(obj->get_dev_id())
985+
<< ", re-selecting machine to trigger disconnect+reconnect";
986+
// Re-selecting the same LAN id hits the same-id-LAN branch in
987+
// set_selected_machine (DevManager.cpp), which calls
988+
// m_agent->disconnect_printer(), obj->reset(), then
989+
// obj->connect(...). This is the exact path the user takes
990+
// manually via the Devices tab.
991+
//
992+
// Bump the throttle only on successful set_selected_machine.
993+
// If it returns false (e.g. printer dropped from
994+
// get_my_machine_list because bind_state flipped to
995+
// "occupied" between the gate and this call), we don't want
996+
// to wait 10s before the next chance to recover.
997+
if (m_manager->set_selected_machine(obj->get_dev_id()))
998+
last_lan_reconnect_attempt = now;
999+
}
1000+
}
1001+
9121002
// certificate
9131003
agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
9141004
}

src/slic3r/GUI/GUI_App.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,6 +2650,32 @@ void GUI_App::on_start_subscribe_again(std::string dev_id)
26502650
start_subscribe_timer->Start(5000, wxTIMER_ONE_SHOT);
26512651
}
26522652

2653+
void GUI_App::try_load_last_machine_on_alive(const std::string &dev_id)
2654+
{
2655+
// Called from DeviceManager::on_machine_alive for every SSDP announcement
2656+
// (~5s/printer). Runs on the wx UI thread (the SSDP listener dispatches
2657+
// via CallAfter), same thread as DeviceManagerRefresher::on_timer, so
2658+
// there is no concurrent invocation of set_selected_machine between the
2659+
// SSDP-retry path here and the stale-MQTT-recovery path in on_timer.
2660+
if (dev_id.empty()) return;
2661+
if (!m_agent || !m_device_manager) return;
2662+
2663+
// Only retry for the machine the user had previously selected. Filter
2664+
// aggressively before doing anything observable so SSDP packets for
2665+
// other printers on the network are nearly free.
2666+
const auto &last = m_device_manager->get_user_last_machine();
2667+
if (last.empty() || last != dev_id) return;
2668+
2669+
// If a machine is already selected we have nothing to fix. InnerLoad
2670+
// would early-return anyway; we skip the call entirely to keep the
2671+
// log clean.
2672+
if (m_device_manager->get_selected_machine() != nullptr) return;
2673+
2674+
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": SSDP-triggered retry for "
2675+
<< BBLCrossTalk::Crosstalk_DevId(dev_id);
2676+
m_load_last_machine.InnerLoad(m_agent, m_device_manager);
2677+
}
2678+
26532679
std::string GUI_App::get_local_models_path()
26542680
{
26552681
std::string local_path = "";

src/slic3r/GUI/GUI_App.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ class GUI_App : public wxApp
365365
public:
366366
//try again when subscription fails
367367
void on_start_subscribe_again(std::string dev_id);
368+
369+
// SSDP-driven retry of TryLoadLastMachine::InnerLoad. Called from
370+
// DeviceManager::on_machine_alive when an SSDP packet announces a
371+
// previously-paired printer. Closes the startup race where the cached
372+
// user_access_dev_ip is stale (e.g. slicer_uuid rotated since pairing
373+
// so the encoded IP can't be decoded, or the printer is at a new IP)
374+
// and the initial InnerLoad's bind_detect fails -- by retrying once
375+
// the printer's actual current IP is known via SSDP, we let the LAN
376+
// path complete instead of leaving the printer discovered-but-not-
377+
// selected. Idempotent and cheap to call repeatedly: InnerLoad
378+
// returns early if a machine is already selected.
379+
void try_load_last_machine_on_alive(const std::string &dev_id);
380+
368381
std::string get_local_models_path();
369382
bool OnInit() override;
370383
int OnExit() override;

0 commit comments

Comments
 (0)