From 3a0fda7d18b4299f4ef1746c17c74cf33d74b138 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 7 Sep 2026 17:28:40 +0800 Subject: [PATCH 1/3] fix: make model_id/dev_type optional instead of blocking --- .../GUI/CalibrationWizardPresetPage.cpp | 6 +++ src/slic3r/GUI/DeviceCore/DevConfigUtil.h | 17 +++++++- src/slic3r/GUI/DeviceManager.cpp | 2 + src/slic3r/GUI/GUI_App.cpp | 6 +++ src/slic3r/GUI/MultiMachine.cpp | 6 +++ src/slic3r/GUI/Plater.cpp | 22 +++++++--- src/slic3r/GUI/PrePrintChecker.cpp | 3 +- src/slic3r/GUI/PrePrintChecker.hpp | 1 + src/slic3r/GUI/SelectMachine.cpp | 40 +++++++++++++++++-- src/slic3r/GUI/SendToPrinter.cpp | 6 +++ src/slic3r/GUI/SyncAmsInfoDialog.cpp | 13 +++++- 11 files changed, 110 insertions(+), 12 deletions(-) diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 5267715439..6d28189c54 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -6,6 +6,7 @@ #include "libslic3r/Print.hpp" #include "DeviceCore/DevConfig.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaSystem.h" @@ -1648,6 +1649,11 @@ bool CalibrationPresetPage::is_blocking_printing() auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); auto target_model = obj_->printer_type; + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return false; + } + if (source_model != target_model) { std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); diff --git a/src/slic3r/GUI/DeviceCore/DevConfigUtil.h b/src/slic3r/GUI/DeviceCore/DevConfigUtil.h index 2b4af44061..60808a0170 100644 --- a/src/slic3r/GUI/DeviceCore/DevConfigUtil.h +++ b/src/slic3r/GUI/DeviceCore/DevConfigUtil.h @@ -59,6 +59,21 @@ public: /*printer*/ // info static std::map get_all_model_id_with_name(); + // A printer agent may not know the physical model. Keep that case optional so + // model compatibility checks do not turn missing identity into a hard error. + static bool is_optional_printer_model_id(const std::string& model_id) + { + if (model_id.empty()) + return true; + if (model_id.size() != 9) + return false; + + static constexpr char generic_model_id[] = "orcasonar"; + return std::equal(model_id.begin(), model_id.end(), generic_model_id, + [](char lhs, char rhs) { + return static_cast(std::tolower(static_cast(lhs))) == rhs; + }); + } static std::string get_printer_type(const std::string& type_str) { return get_value_from_config(type_str, "printer_type"); } static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config(type_str, "display_name"); } static std::string get_printer_series_str(std::string type_str) { return get_value_from_config(type_str, "printer_series"); } @@ -227,4 +242,4 @@ static std::string _parse_printer_type(const std::string &type_str) return type_str; } -};// namespace Slic3r \ No newline at end of file +};// namespace Slic3r diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 6c155fe158..fc66604f7c 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -372,6 +372,8 @@ wxString MachineObject::get_printer_type_display_str() const std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); if (!display_name.empty()) return display_name; + else if (printer_type == "orcasonar") + return "OrcaSonar Printer"; else return _L("Unknown"); } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1e9cfab5da..94b36b18f8 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Technologies.hpp" #include "libslic3r/Platform.hpp" #include "GUI_App.hpp" +#include "DeviceCore/DevConfigUtil.h" #include "GUI_Init.hpp" #include "GUI_ObjectList.hpp" #include "slic3r/GUI/UserManager.hpp" @@ -2393,6 +2394,11 @@ bool GUI_App::is_blocking_printing(MachineObject *obj_) PresetBundle *preset_bundle = wxGetApp().preset_bundle; std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return false; + } + if (source_model != target_model) { std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); diff --git a/src/slic3r/GUI/MultiMachine.cpp b/src/slic3r/GUI/MultiMachine.cpp index c0e84f40e9..19328c501f 100644 --- a/src/slic3r/GUI/MultiMachine.cpp +++ b/src/slic3r/GUI/MultiMachine.cpp @@ -3,6 +3,7 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" +#include "DeviceCore/DevConfigUtil.h" namespace Slic3r { namespace GUI { @@ -114,6 +115,11 @@ bool DeviceItem::is_blocking_printing(MachineObject* obj_) PresetBundle* preset_bundle = wxGetApp().preset_bundle; source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return false; + } + if (source_model != target_model) { std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6d75be8a3e..7b318c4ffe 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2010,12 +2010,14 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man std::string machine_print_name = obj->get_show_printer_type(); PresetBundle *preset_bundle = wxGetApp().preset_bundle; std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); - Preset* machine_preset = get_printer_preset(obj); - if (!machine_preset) { + const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type); + const bool optional_target_model = DevPrinterConfigUtil::is_optional_printer_model_id(target_model_id); + Preset* machine_preset = optional_printer_model ? nullptr : get_printer_preset(obj); + if (!optional_printer_model && !optional_target_model && !machine_preset) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; return false; } - if (machine_print_name != target_model_id) { + if (!optional_printer_model && !optional_target_model && machine_print_name != target_model_id) { MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); if (dlg.ShowModal() == wxID_NO) { @@ -2207,6 +2209,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) return; } + if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) { + clear_all_sync_status(); + return; + } + bool printer_synced = false; // 1. update printer status const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); @@ -20490,9 +20497,14 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) } if (!check_printer_initialized(obj, true, popup_warning)) return false; - Preset * machine_preset = get_printer_preset(obj); - if (!machine_preset) + const std::string machine_model = obj->printer_type; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + const std::string selected_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string(); + if (!DevPrinterConfigUtil::is_optional_printer_model_id(machine_model) && + !DevPrinterConfigUtil::is_optional_printer_model_id(selected_model) && + !get_printer_preset(obj)) { return false; + } if (wxGetApp().is_blocking_printing()) { if (popup_warning) { diff --git a/src/slic3r/GUI/PrePrintChecker.cpp b/src/slic3r/GUI/PrePrintChecker.cpp index 0575af23ed..7d0ae63cf1 100644 --- a/src/slic3r/GUI/PrePrintChecker.cpp +++ b/src/slic3r/GUI/PrePrintChecker.cpp @@ -63,6 +63,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusRackReading: return "PrintStatusRackReading"; case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning"; case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning"; + case PrintStatusOptionalPrinterModel: return "PrintStatusOptionalPrinterModel"; case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch"; case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC"; case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn"; @@ -104,6 +105,7 @@ wxString PrePrintChecker::get_pre_state_msg(PrintDialogStatus status) case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated."); case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate."); case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse."); + case PrintStatusOptionalPrinterModel: return _L("The selected printer model could not be identified, so compatibility with the print file configuration cannot be verified. Please verify the printer preset before sending."); case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing."); case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration."); case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value."); @@ -379,4 +381,3 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector& infos) } }; - diff --git a/src/slic3r/GUI/PrePrintChecker.hpp b/src/slic3r/GUI/PrePrintChecker.hpp index f629b70a73..bab608348d 100644 --- a/src/slic3r/GUI/PrePrintChecker.hpp +++ b/src/slic3r/GUI/PrePrintChecker.hpp @@ -112,6 +112,7 @@ enum PrintDialogStatus : unsigned int { // Orca: a nozzle diameter that differs from the one the printer remembers is a warning, // not an error, so non-standard nozzles can still be printed with. PrintStatusNozzleDiameterMismatch, + PrintStatusOptionalPrinterModel, PrintStatusPrinterWarningEnd, // Warnings for filament diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index fa4482fcef..546649736d 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -21,6 +21,7 @@ #include "Jobs/PlaterWorker.hpp" #include "DeviceCore/DevConfig.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevNozzleSystem.h" #include "DeviceCore/DevNozzleRack.h" #include "DeviceCore/DevExtensionTool.h" @@ -2315,8 +2316,10 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); @@ -2625,6 +2637,10 @@ bool SelectMachineDialog::is_same_printer_model() if(preset_bundle == nullptr) return result; const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); const auto target_model = obj_->printer_type; + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return true; + } // Orca: ignore P1P -> P1S if (source_model != target_model) { if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") || @@ -4786,6 +4802,22 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) return; } + bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type); + if (m_print_type == PrintFromType::FROM_NORMAL) { + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + has_optional_printer_model = has_optional_printer_model || + (preset_bundle && DevPrinterConfigUtil::is_optional_printer_model_id( + preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle))); + } else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) { + has_optional_printer_model = has_optional_printer_model || + DevPrinterConfigUtil::is_optional_printer_model_id( + m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id); + } + + if (has_optional_printer_model) { + show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel); + } + if (is_blocking_printing(obj_)) { show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); return; diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index a350bd19ea..1fac389862 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -24,6 +24,7 @@ #include "BitmapCache.hpp" #include "DeviceCore/DevManager.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevStorage.h" #include "slic3r/Utils/FileTransferUtils.hpp" @@ -1350,6 +1351,11 @@ bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_) auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); auto target_model = obj_->printer_type; + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return false; + } + if (source_model != target_model) { std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 47931e2312..aa203f3577 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1875,6 +1875,11 @@ bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_) if (m_required_data_plate_data_list.size() > 0) { source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; } } + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return false; + } + if (source_model != target_model) { std::vector compatible_machine = obj_->get_compatible_machine(); vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); @@ -1931,7 +1936,13 @@ bool SyncAmsInfoDialog::is_same_printer_model() if (obj_ == nullptr) { return result; } PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) != obj_->printer_type) { + const std::string source_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string(); + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type)) { + return true; + } + + if (preset_bundle && source_model != obj_->printer_type) { if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) { return true; } From c4599942908907763292ab73c3f1d84bbf5ce8cc Mon Sep 17 00:00:00 2001 From: peachismomo Date: Tue, 8 Sep 2026 04:40:12 +0800 Subject: [PATCH 2/3] fix: connect via ip dialog --- src/slic3r/Utils/OrcaPrinterAgent.cpp | 329 +++++++++++++++++++++++++- 1 file changed, 323 insertions(+), 6 deletions(-) diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 6c7fed7eab..2017a41642 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,13 +1,19 @@ #include "OrcaPrinterAgent.hpp" +#include "Http.hpp" #include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" #include "OrcaCloudServiceAgent.hpp" +#include "bambu_networking.hpp" #include +#include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -24,6 +30,147 @@ namespace Slic3r { const std::string OrcaPrinterAgent_VERSION = "0.0.1"; +namespace { + +namespace fs = boost::filesystem; + +// params.filename is normally the exported .3mf archive; the sliced G-code sits +// beside it with the same stem (".12345.0.3mf" -> ".12345.0.gcode"). params.dst_file, +// when set, already points straight at a file (the "print a file already on the +// card" flow), so it wins. +std::string resolve_local_gcode_path(const PrintParams& params) +{ + if (!params.dst_file.empty()) + return params.dst_file; + + std::string path = params.filename; + if (boost::iends_with(path, ".3mf")) + path.replace(path.size() - 4, 4, ".gcode"); + return path; +} + +// The name the file is stored as under the printer's `gcodes` root, and the value +// passed to OrcaSonar's print.gcode_file `param`. Must be a pure function of params +// so start_local_print's upload and start_sdcard_print's start agree on it. +// OrcaSonar rejects newlines, ';', '#', '*' and NUL in the path, and Klipper's +// SDCARD_PRINT_FILE splits its argument on whitespace, so collapse anything unsafe. +std::string remote_gcode_name(const PrintParams& params) +{ + std::string name = params.project_name.empty() + ? fs::path(resolve_local_gcode_path(params)).filename().string() + : fs::path(params.project_name).filename().string(); + + if (boost::iends_with(name, ".3mf")) // "model.gcode.3mf" -> "model.gcode" + name.erase(name.size() - 4); + + std::replace_if( + name.begin(), name.end(), + [](unsigned char c) { + return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\'; + }, + '_'); + + if (name.empty()) + name = "orca_print"; + if (!boost::iends_with(name, ".gcode")) + name += ".gcode"; + return name; +} + +// http(s) origin of the Moonraker-compatible upload facade, derived from the live +// LAN MQTT session URL ("ws://host:port/mqtt" -> "http://host:port"). Used only as +// a fallback when the print job carries no dev_ip of its own. +std::string http_origin_from_lan_ws(const std::string& ws_url) +{ + if (ws_url.empty()) + return {}; + std::string s = ws_url; + if (boost::istarts_with(s, "wss://")) + s = "https://" + s.substr(6); + else if (boost::istarts_with(s, "ws://")) + s = "http://" + s.substr(5); + const auto scheme = s.find("://"); + if (scheme != std::string::npos) { + if (const auto slash = s.find('/', scheme + 3); slash != std::string::npos) + s.erase(slash); + } + return s; +} + +// print.gcode_file is non-idempotent and OrcaSonar replays a cached response for a +// reused (namespace, command, sequence_id). Seed from the wall clock so ids do not +// collide across slicer restarts, then bump once per call within a run. +std::string next_gcode_file_sequence_id() +{ + static std::atomic counter{[] { + const auto now = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(now).count()); + }()}; + return std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +// Ask an OrcaSonar instance at host:port for its MQTT device id. Every command +// topic is keyed on it (device//request), so a manual "connect by IP" has to +// learn it from the printer instead of inventing one from the address. OrcaSonar's +// landing page (GET /) returns "OrcaSonar running\ndevice_id=\nmqtt=\n"; +// /upnp/device.xml carries the same id as uuid: and is the fallback. +bool probe_orcasonar_device_id(const std::string& host, const std::string& port, std::string& device_id) +{ + const std::string origin = "http://" + host + ":" + port; + + auto fetch = [](const std::string& url, std::string& body) { + bool ok = false; + Http::get(url) + .timeout_connect(4) + .timeout_max(6) + .on_complete([&](std::string b, unsigned status) { + if (status == 200) { + body = std::move(b); + ok = true; + } + }) + .on_error([&](std::string, std::string err, unsigned status) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url + << " failed status=" << status << " err=" << err; + }) + .perform_sync(); + return ok; + }; + + auto extract = [](const std::string& body, const std::string& start_token, char end_char) -> std::string { + const auto pos = body.find(start_token); + if (pos == std::string::npos) + return {}; + const auto value_start = pos + start_token.size(); + const auto value_end = body.find(end_char, value_start); + std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start); + boost::trim(value); + return value; + }; + + std::string body; + if (fetch(origin + "/", body)) { + std::string id = extract(body, "device_id=", '\n'); + if (!id.empty()) { + device_id = std::move(id); + return true; + } + } + + body.clear(); + if (fetch(origin + "/upnp/device.xml", body)) { + std::string id = extract(body, "uuid:", '<'); + if (!id.empty()) { + device_id = std::move(id); + return true; + } + } + + return false; +} + +} // namespace + class OrcaPrinterAgent::OrcaSonarDiscovery { public: @@ -921,12 +1068,41 @@ bool OrcaPrinterAgent::start_discovery(bool start, bool /*sending*/) } // ============================================================================ -// Binding - All Stubs +// Binding // ============================================================================ int OrcaPrinterAgent::ping_bind(std::string ping_code) { return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { return BAMBU_NETWORK_SUCCESS; } +// Runs on the "Input IP address" dialog worker thread, before any MachineObject +// exists. Probe the address for a live OrcaSonar and hand its real device id back +// so DeviceManager::insert_local_device keys the machine correctly; connect_type +// and bind_state must be set for is_lan_mode_printer()/is_avaliable() to hold, or +// set_selected_machine never routes to the LAN connect path. +int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string /*sec_link*/, detectResult& detect) +{ + std::string host, port; + if (!parse_lan_endpoint(dev_ip, host, port)) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: unparsable dev_ip=" << dev_ip; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; // -1: dialog shows "Failed to connect to printer." + } + + std::string device_id; + if (!probe_orcasonar_device_id(host, port, device_id) || device_id.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: no OrcaSonar reachable at " << host << ":" << port; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + detect.dev_id = device_id; + detect.dev_name = device_id; + detect.model_id = ""; // unknown; DeviceManager::insert_local_device defaults it + detect.version = ""; + detect.connect_type = "lan"; // required by MachineObject::is_lan_mode_printer() + detect.bind_state = "free"; // required by MachineObject::is_avaliable() + detect.result_msg = ""; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id + << " at " << host << ":" << port; + return BAMBU_NETWORK_SUCCESS; +} int OrcaPrinterAgent::bind(std::string dev_ip, std::string dev_id, @@ -1061,14 +1237,155 @@ int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, OnWaitFn wait_fn) { return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) -{ return BAMBU_NETWORK_SUCCESS; } +// Upload one G-code file to the printer's `gcodes` root over OrcaSonar's +// Moonraker-compatible HTTP facade. No print is started here (print=false); the +// caller issues print.gcode_file over MQTT separately (start_sdcard_print). +int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn /*wait_fn*/) +{ + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); + const std::string local_path = resolve_local_gcode_path(params); + const fs::path source(local_path); + boost::system::error_code ec; + if (!fs::exists(source, ec) || !fs::is_regular_file(source, ec)) { + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; + return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; + } + + const std::uintmax_t file_size = fs::file_size(source, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: cannot stat G-code file " << local_path << ": " << ec.message(); + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + if (file_size > 1024ull * 1024 * 1024) { // OrcaSonar caps a single upload at 1 GiB + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file too large: " << file_size << " bytes"; + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + + std::string host, port, origin; + if (parse_lan_endpoint(params.dev_ip, host, port)) + origin = "http://" + host + ":" + port; + else + origin = http_origin_from_lan_ws(lan_connection_target()); + if (origin.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: no LAN HTTP endpoint for G-code upload (dev_ip=" << params.dev_ip << ")"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + const std::string upload_name = remote_gcode_name(params); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin + << "/server/files/upload as " << upload_name << " (" << file_size << " bytes)"; + + if (update_fn) + update_fn(PrintingStageUpload, 0, "Uploading..."); + + bool canceled = false; + long http_status = 0; + std::string http_error; + std::string response_body; + + auto http = Http::post(origin + "/server/files/upload"); + if (!params.password.empty()) + http.header("X-Api-Key", params.password); // trusted LAN facades may not require it; harmless when they do not + http.form_add("root", "gcodes") + .form_add("print", "false") + .form_add_file("file", source, upload_name) + .timeout_connect(5) + .timeout_max(300) // large G-code over a slow link + .on_complete([&](std::string body, unsigned status) { + http_status = status; + response_body = std::move(body); + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_status = status; + http_error = std::move(err); + response_body = std::move(body); + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + if (cancel_fn && cancel_fn()) { + cancel = true; + canceled = true; + return; + } + if (update_fn && progress.ultotal > 0) { + const int percent = static_cast((progress.ulnow * 100) / progress.ultotal); + update_fn(PrintingStageUpload, percent, "Uploading..."); + } + }) + .perform_sync(); + + if (canceled) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: G-code upload canceled by user"; + return BAMBU_NETWORK_ERR_CANCELED; + } + + // OrcaSonar's Moonraker facade returns 201 Created on a successful save. + if (http_status != 200 && http_status != 201) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status + << " error=" << http_error << " body=" << response_body; + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + + if (update_fn) + update_fn(PrintingStageUpload, 100, "File uploaded"); + return BAMBU_NETWORK_SUCCESS; +} + +// Upload the sliced G-code, then start it: the LAN "print now" path. int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) -{ return BAMBU_NETWORK_SUCCESS; } +{ + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + const int upload_rc = start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); + if (upload_rc != BAMBU_NETWORK_SUCCESS) + return upload_rc; + + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + return start_sdcard_print(params, update_fn, cancel_fn); +} + +// Start a file that already lives on the printer by publishing the canonical +// OPCP print.gcode_file command to device//request. The acknowledgement +// and lifecycle progress arrive asynchronously as print.push_status on the +// report topic, which the GUI already consumes. int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) -{ return BAMBU_NETWORK_SUCCESS; } +{ + if (params.dev_id.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print rejected missing dev_id"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + // dst_file, when set, names a file already on the printer (print-from-SD flow); + // otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`. + const std::string target = params.dst_file.empty() ? remote_gcode_name(params) + : fs::path(params.dst_file).filename().string(); + + nlohmann::json j; + j["print"]["command"] = "gcode_file"; + j["print"]["sequence_id"] = next_gcode_file_sequence_id(); + j["print"]["param"] = target; + + if (update_fn) + update_fn(PrintingStageSending, 0, "Starting print..."); + + const bool is_lan = params.connection_type == "lan"; + const int rc = route_send(is_lan, params.dev_id, j.dump()); + if (rc != BAMBU_NETWORK_SUCCESS) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc + << " dev_id=" << params.dev_id << " param=" << target; + return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; + } + + if (update_fn) + update_fn(PrintingStageFinished, 100, "Print started"); + return BAMBU_NETWORK_SUCCESS; +} // ============================================================================ // Callback Registration From 01c553bad9315d4f7df95e1ea7bb3a11b2cc0314 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 8 Sep 2026 19:13:48 +0800 Subject: [PATCH 3/3] feat: LAN impl for Orca Printer Agent --- src/slic3r/GUI/DeviceCore/DevStorage.cpp | 4 +- src/slic3r/Utils/OrcaPrinterAgent.cpp | 389 ++++++++++++++--------- 2 files changed, 244 insertions(+), 149 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevStorage.cpp b/src/slic3r/GUI/DeviceCore/DevStorage.cpp index 0c60b096bd..cb17ba9430 100644 --- a/src/slic3r/GUI/DeviceCore/DevStorage.cpp +++ b/src/slic3r/GUI/DeviceCore/DevStorage.cpp @@ -20,8 +20,8 @@ DevStorage::SdcardState Slic3r::DevStorage::set_sdcard_state(int state) if (system) { try { - if (print_json.contains("sdcard")) { - if (print_json["sdcard"].get()) + if (print_json.contains("sdcard") || print_json.contains("support_send_to_sd")) { + if (print_json["sdcard"].get() || print_json["support_send_to_sd"].get()) system->m_sdcard_state = DevStorage::SdcardState::HAS_SDCARD_NORMAL; else system->m_sdcard_state = DevStorage::SdcardState::NO_SDCARD; diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 2017a41642..83d2924d68 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -22,10 +22,14 @@ #include #include #include +#include #include #include #include +#include +#include + namespace Slic3r { const std::string OrcaPrinterAgent_VERSION = "0.0.1"; @@ -56,19 +60,15 @@ std::string resolve_local_gcode_path(const PrintParams& params) // SDCARD_PRINT_FILE splits its argument on whitespace, so collapse anything unsafe. std::string remote_gcode_name(const PrintParams& params) { - std::string name = params.project_name.empty() - ? fs::path(resolve_local_gcode_path(params)).filename().string() - : fs::path(params.project_name).filename().string(); + std::string name = params.project_name.empty() ? fs::path(resolve_local_gcode_path(params)).filename().string() : + fs::path(params.project_name).filename().string(); if (boost::iends_with(name, ".3mf")) // "model.gcode.3mf" -> "model.gcode" name.erase(name.size() - 4); std::replace_if( name.begin(), name.end(), - [](unsigned char c) { - return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\'; - }, - '_'); + [](unsigned char c) { return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\'; }, '_'); if (name.empty()) name = "orca_print"; @@ -109,66 +109,154 @@ std::string next_gcode_file_sequence_id() return std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); } -// Ask an OrcaSonar instance at host:port for its MQTT device id. Every command -// topic is keyed on it (device//request), so a manual "connect by IP" has to -// learn it from the printer instead of inventing one from the address. OrcaSonar's -// landing page (GET /) returns "OrcaSonar running\ndevice_id=\nmqtt=\n"; -// /upnp/device.xml carries the same id as uuid: and is the fallback. -bool probe_orcasonar_device_id(const std::string& host, const std::string& port, std::string& device_id) +static constexpr const char* ORCASONAR_FALLBACK = "orcasonar"; + +bool fetch_orcasonar_body(const std::string& url, std::string& body) { - const std::string origin = "http://" + host + ":" + port; + bool ok = false; + Http::get(url) + .timeout_connect(4) + .timeout_max(6) + .on_complete([&](std::string b, unsigned status) { + if (status == 200) { + body = std::move(b); + ok = true; + } + }) + .on_error([&](std::string, std::string err, unsigned status) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url << " failed status=" << status << " err=" << err; + }) + .perform_sync(); + return ok; +} - auto fetch = [](const std::string& url, std::string& body) { - bool ok = false; - Http::get(url) - .timeout_connect(4) - .timeout_max(6) - .on_complete([&](std::string b, unsigned status) { - if (status == 200) { - body = std::move(b); - ok = true; - } - }) - .on_error([&](std::string, std::string err, unsigned status) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url - << " failed status=" << status << " err=" << err; - }) - .perform_sync(); - return ok; - }; +std::string extract_line_value(const std::string& body, const std::string& key) +{ + const auto pos = body.find(key); + if (pos == std::string::npos) + return {}; + const auto value_start = pos + key.size(); + const auto value_end = body.find('\n', value_start); + std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start); + boost::trim(value); + return value; +} - auto extract = [](const std::string& body, const std::string& start_token, char end_char) -> std::string { - const auto pos = body.find(start_token); - if (pos == std::string::npos) - return {}; - const auto value_start = pos + start_token.size(); - const auto value_end = body.find(end_char, value_start); - std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start); - boost::trim(value); - return value; - }; +// Manual binding uses the OrcaSonar landing page as its sole identity source. +// The page returns device_id and may return device_name/model_id. +bool probe_orcasonar_landing_page(const std::string& host, + const std::string& port, + std::string& device_id, + std::string& device_name, + std::string& model_id) +{ + device_name = ORCASONAR_FALLBACK; + model_id = ORCASONAR_FALLBACK; std::string body; - if (fetch(origin + "/", body)) { - std::string id = extract(body, "device_id=", '\n'); - if (!id.empty()) { - device_id = std::move(id); + if (fetch_orcasonar_body("http://" + host + ":" + port + "/", body)) { + device_id = extract_line_value(body, "device_id="); + const std::string name = extract_line_value(body, "device_name="); + const std::string model = extract_line_value(body, "model_id="); + if (!name.empty()) + device_name = name; + if (!model.empty()) + model_id = model; + if (!device_id.empty()) return true; - } - } - - body.clear(); - if (fetch(origin + "/upnp/device.xml", body)) { - std::string id = extract(body, "uuid:", '<'); - if (!id.empty()) { - device_id = std::move(id); - return true; - } } return false; } +// SSDP discovery uses the LOCATION URL's UPnP device description as its sole +// identity source. OrcaSonar maps device_id/device_name/model_id to UDN, +// friendlyName, and modelNumber respectively. +bool parse_orcasonar_device_xml(const std::string& body, + std::string& device_id, + std::string& device_name, + std::string& model_id) +{ + device_name = ORCASONAR_FALLBACK; + model_id = ORCASONAR_FALLBACK; + + try { + boost::property_tree::ptree tree; + std::istringstream stream(body); + boost::property_tree::read_xml(stream, tree, boost::property_tree::xml_parser::trim_whitespace); + const auto device = tree.get_child_optional("root.device"); + if (!device) + return false; + + std::string udn = device->get("UDN", ""); + boost::trim(udn); + if (boost::istarts_with(udn, "uuid:")) + device_id = udn.substr(5); + else + device_id = device->get("device_id", ""); + boost::trim(device_id); + if (device_id.empty()) + return false; + + device_name = device->get("friendlyName", ""); + if (device_name.empty()) + device_name = device->get("device_name", ORCASONAR_FALLBACK); + model_id = device->get("modelNumber", ""); + if (model_id.empty()) + model_id = device->get("model_id", ORCASONAR_FALLBACK); + boost::trim(device_name); + boost::trim(model_id); + if (device_name.empty()) + device_name = ORCASONAR_FALLBACK; + if (model_id.empty()) + model_id = ORCASONAR_FALLBACK; + return true; + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: failed to parse OrcaSonar device.xml: " << error.what(); + return false; + } +} + +bool probe_orcasonar_device_xml(const std::string& location, + std::string& device_id, + std::string& device_name, + std::string& model_id) +{ + std::string body; + return fetch_orcasonar_body(location, body) && parse_orcasonar_device_xml(body, device_id, device_name, model_id); +} + +// TEMP MOCK: OrcaSonar's push_status doesn't report storage state yet, so +// DevStorage::ParseV1_0() decodes NO_SDCARD and SelectMachineDialog blocks +// printing with "No SD card". Force the storage-present markers into every +// forwarded status document so the printer reports HAS_SDCARD_NORMAL. +// Remove once the firmware reports real storage state. +std::string force_sdcard_present(const std::string& payload) +{ + nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false); + if (!envelope.is_object()) + return payload; + + const auto print_it = envelope.find("print"); + if (print_it == envelope.end() || !print_it->is_object()) + return payload; + + // DevStorage::ParseV1_0() reads print.sdcard as a bool -> HAS_SDCARD_NORMAL. + (*print_it)["sdcard"] = true; + + // MachineObject::parse_home_flag() runs afterwards and re-derives the state + // from bits 8-9 of print.home_flag; rewrite them to 01 so it doesn't clobber + // the mock back to NO_SDCARD. + const auto home_flag_it = print_it->find("home_flag"); + if (home_flag_it != print_it->end() && home_flag_it->is_number_integer()) { + int flag = home_flag_it->get(); + flag = (flag & ~(0x3 << 8)) | (0x1 << 8); + *home_flag_it = flag; + } + + return envelope.dump(); +} + } // namespace class OrcaPrinterAgent::OrcaSonarDiscovery @@ -246,8 +334,7 @@ private: const std::size_t type_start = lower_usn.find(device_type, uuid_prefix.size()); if (type_start == std::string::npos) return false; - const std::string device_id = trim_ascii(usn.substr(uuid_prefix.size(), type_start - uuid_prefix.size())); - if (device_id.empty() || host.empty()) + if (host.empty()) return false; const std::string lower_location = lower_ascii(location); @@ -274,17 +361,34 @@ private: if (port.empty() || port.find_first_not_of("0123456789") != std::string::npos) return false; + // SSDP LOCATION commonly advertises the device's mDNS name (for + // example, http://orcasonar-123.local:8280/upnp/device.xml). The + // discovery response already gives us the sender's reachable address, + // so use that address for the HTTP probe instead of requiring the + // platform HTTP client to resolve .local. Keep the advertised path so + // this remains compatible with non-default device-description URLs. + const std::string location_path = authority_end == std::string::npos ? "/" : location.substr(authority_end); + const std::string probe_host = host.find(':') == std::string::npos ? host : "[" + host + "]"; + const std::string probe_url = location.substr(0, scheme_end + 3) + probe_host + ":" + port + location_path; + + std::string device_id; + std::string device_name; + std::string model_id; + if (!probe_orcasonar_device_xml(probe_url, device_id, device_name, model_id)) + return false; + nlohmann::json machine; - machine["dev_name"] = device_id; + machine["dev_name"] = device_name; machine["dev_id"] = device_id; + machine["dev_type"] = model_id; + machine["connection_name"] = device_id; machine["dev_ip"] = host + ":" + port; - machine["dev_type"] = "orcasonar"; + machine["dev_signal"] = "0"; machine["connect_type"] = "lan"; machine["bind_state"] = "free"; machine["sec_link"] = "secure"; machine["ssdp_version"] = "v1"; - machine["connection_name"] = device_id; json = machine.dump(); return true; } @@ -443,10 +547,8 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str fn = on_message_fn; q = queue_on_main_fn; } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id - << " payload_bytes=" << payload.size() - << " callback=" << (fn ? "set" : "null") - << " queue_on_main=" << (q ? "set" : "null"); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id << " payload_bytes=" << payload.size() + << " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null"); if (!fn) { BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set" << " dev_id=" << dev_id; @@ -469,10 +571,8 @@ void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const st fn = on_local_message_fn; q = queue_on_main_fn; } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering LAN message dev_id=" << dev_id - << " payload_bytes=" << payload.size() - << " callback=" << (fn ? "set" : "null") - << " queue_on_main=" << (q ? "set" : "null"); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering LAN message dev_id=" << dev_id << " payload_bytes=" << payload.size() + << " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null"); if (!fn) { BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping LAN message because on_local_message_fn is not set" << " dev_id=" << dev_id; @@ -494,10 +594,8 @@ void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_ queue = queue_on_main_fn; } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state - << " dev_id=" << dev_id << " message=" << message - << " callback=" << (callback ? "set" : "null") - << " queue_on_main=" << (queue ? "set" : "null"); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message + << " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null"); if (!callback) return; @@ -515,8 +613,7 @@ std::function OrcaPrinterAgent::ma deliver_to_local_sink(id, payload); else BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation - << " current_generation=" << m_lan_generation.load() - << " dev_id=" << id; + << " current_generation=" << m_lan_generation.load() << " dev_id=" << id; }; } @@ -612,8 +709,7 @@ int OrcaPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, return route_send(lan_mode, dev_id, j.dump()); } -int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, - int sequence_id, bool lan_mode) +int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) { nlohmann::json j; j["print"]["sequence_id"] = std::to_string(sequence_id); @@ -627,8 +723,7 @@ int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool return route_send(lan_mode, dev_id, j.dump()); } -int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, - int sequence_id, bool lan_mode) +int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, int sequence_id, bool lan_mode) { nlohmann::json j; j["print"]["command"] = "set_bed_temp"; @@ -647,9 +742,15 @@ int OrcaPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int seque return route_send(lan_mode, dev_id, j.dump()); } -int OrcaPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, - int /*speed*/, bool is_core_xy, bool /*supports_mqtt_axis_control*/, - int sequence_id, bool lan_mode) +int OrcaPrinterAgent::command_axis_control(std::string dev_id, + std::string axis, + double unit, + double input_val, + int /*speed*/, + bool is_core_xy, + bool /*supports_mqtt_axis_control*/, + int sequence_id, + bool lan_mode) { std::transform(axis.begin(), axis.end(), axis.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); if (axis != "X" && axis != "Y" && axis != "Z" && axis != "E") { @@ -659,8 +760,7 @@ int OrcaPrinterAgent::command_axis_control(std::string dev_id, std::string axis, const double requested_distance = input_val * unit; if (!std::isfinite(requested_distance) || requested_distance == 0.0) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val - << " unit=" << unit; + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val << " unit=" << unit; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } @@ -716,7 +816,7 @@ bool OrcaPrinterAgent::parse_nonnegative_command_id(const std::string& value, in if (value.empty()) return false; try { - std::size_t consumed = 0; + std::size_t consumed = 0; const long long parsed = std::stoll(value, &consumed); if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits::max()) return false; @@ -738,18 +838,18 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st return; const nlohmann::json& print = *print_it; - const auto command_it = print.find("command"); - const bool is_push_status = command_it != print.end() && command_it->is_string() && command_it->get() == "push_status"; - bool is_full_snapshot = false; + const auto command_it = print.find("command"); + const bool is_push_status = command_it != print.end() && command_it->is_string() && command_it->get() == "push_status"; + bool is_full_snapshot = false; if (is_push_status) { const auto msg_it = print.find("msg"); - is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get() == 0); + is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get() == 0); } CameraStreamMode stream_mode = CameraStreamMode::none; std::string stream_url; bool has_camera_update = false; - const auto ipcam_it = print.find("ipcam"); + const auto ipcam_it = print.find("ipcam"); if (ipcam_it != print.end() && ipcam_it->is_object()) { const auto stream_modes_it = ipcam_it->find("stream_mode"); if (stream_modes_it != ipcam_it->end() && stream_modes_it->is_array()) { @@ -758,7 +858,7 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st if (!stream.is_object()) continue; const auto mode_it = stream.find("mode"); - const auto url_it = stream.find("url"); + const auto url_it = stream.find("url"); if (mode_it == stream.end() || url_it == stream.end() || !mode_it->is_string() || !url_it->is_string()) continue; @@ -775,8 +875,7 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st stream_url = url_it->get(); break; // OrcaSonar orders entries by preference. } - } - else if (is_full_snapshot) { + } else if (is_full_snapshot) { has_camera_update = true; } } else if (is_full_snapshot) { @@ -795,11 +894,9 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st } m_camera_stream_mode = stream_mode; - m_camera_url = std::move(stream_url); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id - << " transport=LAN" - << " mode=" << static_cast(m_camera_stream_mode) - << " url=" << m_camera_url; + m_camera_url = std::move(stream_url); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id << " transport=LAN" + << " mode=" << static_cast(m_camera_stream_mode) << " url=" << m_camera_url; } std::string OrcaPrinterAgent::lan_connection_target() const @@ -845,9 +942,9 @@ void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnectio int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: connect_printer requested dev_id=" << dev_id - << " dev_ip=" << dev_ip << " username=" << (username.empty() ? "" : username) - << " password_present=" << (!password.empty()) << " use_ssl=" << use_ssl; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: connect_printer requested dev_id=" << dev_id << " dev_ip=" << dev_ip + << " username=" << (username.empty() ? "" : username) << " password_present=" << (!password.empty()) + << " use_ssl=" << use_ssl; (void) use_ssl; // OrcaSonar LAN is plaintext ws:// if (dev_id.empty() || dev_ip.empty()) { BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip"; @@ -869,16 +966,15 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st cfg.client_id = make_lan_client_id(dev_id); cfg.keepalive_seconds = 60; - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen - << " host=" << host << " port=" << port << " url=" << cfg.url - << " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty()) + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen << " host=" << host << " port=" << port + << " url=" << cfg.url << " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty()) << " client_id=" << cfg.client_id; OrcaMqttConnection* conn = nullptr; CurrentConn previous_connection; { std::lock_guard l(state_mutex); - previous_connection = m_current_connection; + previous_connection = m_current_connection; m_lan_dev_id = dev_id; m_lan_url = cfg.url; m_camera_stream_mode = CameraStreamMode::none; @@ -893,17 +989,16 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st if (m_lan_connect_thread.joinable()) m_lan_connect_thread.join(); // disconnect_printer() above already stopped the old conn, so this is fast m_lan_connect_thread = std::thread([this, conn, cfg, dev_id, gen] { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen - << " dev_id=" << dev_id << " url=" << cfg.url; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen << " dev_id=" << dev_id + << " url=" << cfg.url; if (gen != m_lan_generation.load()) { BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker abandoned before start generation=" << gen << " current_generation=" << m_lan_generation.load(); return; // superseded before we ran: never raise a socket nobody will tear down } const bool ok = conn->start(cfg, make_lan_message_handler(gen), [this, gen, dev_id, conn](bool connected, bool initial) { - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT state callback connected=" << connected - << " initial=" << initial << " generation=" << gen - << " current_generation=" << m_lan_generation.load() + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT state callback connected=" << connected << " initial=" << initial + << " generation=" << gen << " current_generation=" << m_lan_generation.load() << " connack_rc=" << conn->last_connack_rc(); if (gen != m_lan_generation.load()) { BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring stale LAN MQTT state callback generation=" << gen; @@ -916,21 +1011,19 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st dispatch_local_connect(ConnectStatusLost, dev_id, "connection_lost"); } }); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok - << " generation=" << gen << " current_generation=" << m_lan_generation.load() - << " connected=" << conn->is_connected() << " running=" << conn->is_running() - << " connack_rc=" << conn->last_connack_rc(); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok << " generation=" << gen + << " current_generation=" << m_lan_generation.load() << " connected=" << conn->is_connected() + << " running=" << conn->is_running() << " connack_rc=" << conn->last_connack_rc(); if (ok && gen == m_lan_generation.load()) { on_connected(dev_id, conn, gen); dispatch_local_connect(ConnectStatusOk, dev_id, "0"); } else if (!ok && gen == m_lan_generation.load() && !conn->is_running()) { // A refusal with rc 4/5 terminates the transport. Network errors keep // retrying in OrcaMqttConnection, so leave the UI in its connecting state. - const int rc = conn->last_connack_rc(); + const int rc = conn->last_connack_rc(); const std::string reason = rc >= 0 ? std::to_string(rc) : "initial_connect_failed"; BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT connection terminated before readiness" - << " generation=" << gen << " connack_rc=" << rc - << " reason=" << reason; + << " generation=" << gen << " connack_rc=" << rc << " reason=" << reason; dispatch_local_connect(ConnectStatusFailed, dev_id, reason); } else if (!ok && gen == m_lan_generation.load()) { BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT initial attempt failed but worker is retrying" @@ -954,8 +1047,8 @@ int OrcaPrinterAgent::disconnect_printer() { std::lock_guard l(state_mutex); previous_connection = m_current_connection; - doomed = std::move(lan_mqtt_connection); - prev_dev = m_lan_dev_id; + doomed = std::move(lan_mqtt_connection); + prev_dev = m_lan_dev_id; m_lan_dev_id.clear(); if (m_current_connection == LAN) { m_current_connection = NONE; @@ -964,8 +1057,8 @@ int OrcaPrinterAgent::disconnect_printer() } current_connection = m_current_connection; } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load() - << " previous_dev_id=" << prev_dev << " had_connection=" << (doomed ? "yes" : "no") + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load() << " previous_dev_id=" << prev_dev + << " had_connection=" << (doomed ? "yes" : "no") << " connected=" << (doomed && doomed->is_connected() ? "yes" : "no") << " transport=" << connection_type_name(previous_connection) << "->" << connection_type_name(current_connection); @@ -1005,16 +1098,16 @@ int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const s // Preserve the transport's existing behavior for malformed payloads; // the printer will report the protocol error asynchronously. } - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id - << " command=" << command << " payload_bytes=" << json_str.size(); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id << " command=" << command + << " payload_bytes=" << json_str.size(); if (dev_id.empty()) return BAMBU_NETWORK_ERR_INVALID_HANDLE; OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan); if (!conn) return BAMBU_NETWORK_ERR_INVALID_HANDLE; const bool queued = conn->send_request(dev_id, json_str); - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued - << " is_lan=" << is_lan << " dev_id=" << dev_id; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan + << " dev_id=" << dev_id; return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } @@ -1087,20 +1180,21 @@ int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string /*sec_link*/, } std::string device_id; - if (!probe_orcasonar_device_id(host, port, device_id) || device_id.empty()) { + std::string device_name; + std::string model_id; + if (!probe_orcasonar_landing_page(host, port, device_id, device_name, model_id) || device_id.empty()) { BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: no OrcaSonar reachable at " << host << ":" << port; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } detect.dev_id = device_id; - detect.dev_name = device_id; - detect.model_id = ""; // unknown; DeviceManager::insert_local_device defaults it + detect.dev_name = device_name; + detect.model_id = model_id; detect.version = ""; detect.connect_type = "lan"; // required by MachineObject::is_lan_mode_printer() detect.bind_state = "free"; // required by MachineObject::is_avaliable() detect.result_msg = ""; - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id - << " at " << host << ":" << port; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id << " at " << host << ":" << port; return BAMBU_NETWORK_SUCCESS; } @@ -1161,7 +1255,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) // selection. Conversely, selecting a cloud machine with the same id // while LAN is active is still a transport switch and must proceed. const bool same_selection = dev_id == selected_machine; - const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD; + const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD; if (same_selection && same_transport) { BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id << " transport=" << connection_type_name(m_current_connection); @@ -1183,8 +1277,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) current_connection = m_current_connection; } BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id - << " cloud=" << (cloud ? "set" : "") << " transport=" - << connection_type_name(previous_connection) << "->" + << " cloud=" << (cloud ? "set" : "") << " transport=" << connection_type_name(previous_connection) << "->" << connection_type_name(current_connection); if (!cloud) { BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; @@ -1240,13 +1333,16 @@ int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, // Upload one G-code file to the printer's `gcodes` root over OrcaSonar's // Moonraker-compatible HTTP facade. No print is started here (print=false); the // caller issues print.gcode_file over MQTT separately (start_sdcard_print). -int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn /*wait_fn*/) +int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn, + OnWaitFn /*wait_fn*/) { if (update_fn) update_fn(PrintingStageCreate, 0, "Preparing..."); const std::string local_path = resolve_local_gcode_path(params); - const fs::path source(local_path); + const fs::path source(local_path); boost::system::error_code ec; if (!fs::exists(source, ec) || !fs::is_regular_file(source, ec)) { BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; @@ -1265,7 +1361,7 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta std::string host, port, origin; if (parse_lan_endpoint(params.dev_ip, host, port)) - origin = "http://" + host + ":" + port; + origin = "http://" + host; else origin = http_origin_from_lan_ws(lan_connection_target()); if (origin.empty()) { @@ -1274,14 +1370,14 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta } const std::string upload_name = remote_gcode_name(params); - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin - << "/server/files/upload as " << upload_name << " (" << file_size << " bytes)"; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin << "/server/files/upload as " + << upload_name << " (" << file_size << " bytes)"; if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading..."); - bool canceled = false; - long http_status = 0; + bool canceled = false; + long http_status = 0; std::string http_error; std::string response_body; @@ -1322,8 +1418,8 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta // OrcaSonar's Moonraker facade returns 201 Created on a successful save. if (http_status != 200 && http_status != 201) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status - << " error=" << http_error << " body=" << response_body; + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status << " error=" << http_error + << " body=" << response_body; return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; } @@ -1363,8 +1459,7 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up // dst_file, when set, names a file already on the printer (print-from-SD flow); // otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`. - const std::string target = params.dst_file.empty() ? remote_gcode_name(params) - : fs::path(params.dst_file).filename().string(); + const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string(); nlohmann::json j; j["print"]["command"] = "gcode_file"; @@ -1375,10 +1470,10 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up update_fn(PrintingStageSending, 0, "Starting print..."); const bool is_lan = params.connection_type == "lan"; - const int rc = route_send(is_lan, params.dev_id, j.dump()); + const int rc = route_send(is_lan, params.dev_id, j.dump()); if (rc != BAMBU_NETWORK_SUCCESS) { - BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc - << " dev_id=" << params.dev_id << " param=" << target; + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc << " dev_id=" << params.dev_id + << " param=" << target; return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; }