diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 74bb2209d7..d28c392d59 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -971,9 +971,9 @@ int OrcaCloudServiceAgent::connect_server() BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code << " connected=" << connected << " response_bytes=" << response.size(); - // connect_server() is a REST health probe only. The long-lived MQTT socket is - // per-printer now, driven by set_user_selected_machine -> - // configure_selected_printer_mqtt; this method must not touch mqtt_connection. + // connect_server() remains a REST health probe. The long-lived fleet MQTT socket + // is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt; + // subscriptions queued before that point are replayed when it starts. { std::lock_guard lock(state_mutex); is_connected = connected; @@ -1035,10 +1035,12 @@ int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } -int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id) +int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id, + OrcaMqttConnection::StateHandler state_handler) { + (void) dev_id; OrcaMqttConnection::Config cfg; - cfg.url = "wss://" + api_base_url + "/api/v1/printers/" + dev_id + "/mqtt"; + cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt"; cfg.use_tls = true; cfg.bearer_provider = [this] { return get_access_token(); }; cfg.client_id = "OrcaSlicer"; @@ -1048,7 +1050,13 @@ int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& de std::lock_guard lock(m_selected_url_mutex); m_selected_printer_mqtt_url = cfg.url; } - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring per-printer MQTT endpoint=" << cfg.url; + + if (mqtt_connection->is_running()) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running"; + return BAMBU_NETWORK_SUCCESS; + } + + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url; // NOTE: no lock is held across start() — it blocks for the whole initial connect // attempt (up to ~10s), and the message handler below re-enters callback_mutex on @@ -1056,8 +1064,8 @@ int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& de const bool ok = mqtt_connection->start( cfg, [this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); }, - [this](bool, bool) {}); - BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: per-printer MQTT start returned=" << ok; + std::move(state_handler)); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok; return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } @@ -1119,6 +1127,23 @@ int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const : BAMBU_NETWORK_ERR_CONNECT_FAILED; } +int OrcaCloudServiceAgent::send_print_job(const std::string& dev_id, + const std::string& filename, + const std::string& gcode, + bool start) +{ + if (dev_id.empty() || filename.empty() || !is_user_login()) + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + + const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs?filename=" + + Http::url_encode(filename) + "&start=" + (start ? "true" : "false"); + unsigned int http_code = 0; + const int result = http_post(path, gcode, nullptr, &http_code, "text/plain"); + if (result != BAMBU_NETWORK_SUCCESS) + return result; + return http_code >= 200 && http_code < 300 ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + void OrcaCloudServiceAgent::enable_multi_machine(bool enable) { std::lock_guard lock(state_mutex); @@ -2275,7 +2300,11 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } -int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) +int OrcaCloudServiceAgent::http_post(const std::string& path, + const std::string& body, + std::string* response_body, + unsigned int* http_code, + const std::string& content_type) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; @@ -2299,7 +2328,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& http.header("Authorization", "Bearer " + token); } - http.header("Content-Type", "application/json"); + http.header("Content-Type", content_type); http.set_post_body(body); http.on_complete([&](std::string resp_body, unsigned resp_status) { diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index a435c9668f..bd62169904 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -218,7 +218,7 @@ public: int del_subscribe(std::vector dev_list) override; void enable_multi_machine(bool enable) override; - // The per-printer MQTT socket carries both directions: inbound reports from + // The fleet MQTT socket carries both directions: inbound reports from // device//report and commands PUBLISHed to device//request on this // socket. OrcaPrinterAgent registers its message callback here to receive the // inbound half; pass an empty fn to clear it before the agent is destroyed. @@ -230,6 +230,12 @@ public: // that need non-blocking behaviour run it on their own thread. int send_printer_command(const std::string& dev_id, const std::string& body); + // Submit raw sliced G-code to the cloud printer print-jobs endpoint. + int send_print_job(const std::string& dev_id, + const std::string& filename, + const std::string& gcode, + bool start = true); + // ======================================================================== // ICloudServiceAgent Interface Implementation - Settings Synchronization // ======================================================================== @@ -377,16 +383,19 @@ public: return mqtt_connection.get(); } - // Per-printer cloud socket: wss:///api/v1/printers//mqtt. + // Account-scoped cloud socket: wss:///api/v1/printers/mqtt. // configure_ blocks for the duration of the initial connect attempt, so callers - // drive it off the UI thread; teardown_ is synchronous. - int configure_selected_printer_mqtt(const std::string& dev_id); + // drive it off the UI thread; teardown_ is synchronous. The dev_id argument is + // retained for source compatibility with the printer-agent lifecycle; it does + // not participate in endpoint construction. + int configure_selected_printer_mqtt(const std::string& dev_id, + OrcaMqttConnection::StateHandler state_handler = {}); void teardown_selected_printer_mqtt(); - // Test hook: the wss:// URL of the current per-printer socket ("" when none). + // Test hook: the wss:// URL of the current fleet socket ("" when none). std::string selected_printer_mqtt_url() const; private: - // Fans one inbound per-printer MQTT message out to printer_status_callback. + // Fans one inbound fleet MQTT message out to printer_status_callback. void deliver_cloud_message(const std::string& dev_id, const std::string& payload); // Sync protocol helpers @@ -412,7 +421,11 @@ private: // HTTP request helpers int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); - int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); + int http_post(const std::string& path, + const std::string& body, + std::string* response_body, + unsigned int* http_code, + const std::string& content_type = "application/json"); int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); std::map data_headers(); diff --git a/src/slic3r/Utils/OrcaMqttConnection.hpp b/src/slic3r/Utils/OrcaMqttConnection.hpp index 6131ee86f1..e103e90167 100644 --- a/src/slic3r/Utils/OrcaMqttConnection.hpp +++ b/src/slic3r/Utils/OrcaMqttConnection.hpp @@ -23,7 +23,7 @@ namespace Slic3r { // Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the -// LAN (OrcaSonar) and cloud (per-printer) printer connections. Both PUBLISH +// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH // commands to device//request and SUBSCRIBE device//report; Config is the // only per-transport difference. class OrcaMqttConnection diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 9459721da2..8f70ce00ef 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -19,11 +19,14 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -1353,24 +1356,48 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) const uint64_t gen = ++m_cloud_generation; auto* conn = cloud->get_mqtt_connection(); - if (!previous.empty() && conn && conn->is_connected()) - conn->send_request(previous, build_pushing_stop(seq(5))); - cloud->teardown_selected_printer_mqtt(); - - if (m_cloud_connect_thread.joinable()) - m_cloud_connect_thread.join(); // teardown_selected_printer_mqtt() above stopped the old cloud conn - if (!dev_id.empty()) { - m_cloud_connect_thread = std::thread([this, cloud, dev_id, gen] { - if (gen != m_cloud_generation.load()) - return; // superseded before we ran: do not raise a socket nobody tears down - if (cloud->configure_selected_printer_mqtt(dev_id) == BAMBU_NETWORK_SUCCESS && gen == m_cloud_generation.load()) - on_connected(dev_id, cloud->get_mqtt_connection(), gen); - }); - } else { - // Deselect: the joined thread may have raised a fresh socket between the - // teardown above and the join. stop() is not sticky, so tear down again. - cloud->teardown_selected_printer_mqtt(); + if (!previous.empty()) { + cloud->del_subscribe({previous}); + if (conn && conn->is_connected()) + conn->send_request(previous, build_pushing_stop(seq(5))); } + + // A previous initial-connect worker may still be blocking in start(). Stop it + // only when no fleet connection has reached CONNACK; an established fleet + // socket survives printer selection changes. + if (m_cloud_connect_thread.joinable()) { + if (conn && !conn->is_connected()) + conn->stop(); + m_cloud_connect_thread.join(); + } + + if (dev_id.empty()) + return BAMBU_NETWORK_SUCCESS; + + if (conn && conn->is_running()) { + on_connected(dev_id, conn, gen); + return BAMBU_NETWORK_SUCCESS; + } + + m_cloud_connect_thread = std::thread([this, cloud, dev_id, gen] { + if (gen != m_cloud_generation.load()) + return; // superseded before we ran: do not raise a socket nobody owns + + auto state_handler = [this](bool connected, bool initial) { + if (!connected || initial) + return; + auto* current_cloud = get_orca_cloud_agent(); + OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr; + const std::string selected = get_user_selected_machine(); + if (current_conn && !selected.empty()) + on_connected(selected, current_conn, m_cloud_generation.load()); + }; + + if (cloud->configure_selected_printer_mqtt(dev_id, std::move(state_handler)) == BAMBU_NETWORK_SUCCESS && + gen == m_cloud_generation.load()) { + on_connected(dev_id, cloud->get_mqtt_connection(), gen); + } + }); return BAMBU_NETWORK_SUCCESS; } @@ -1385,7 +1412,47 @@ AgentInfo OrcaPrinterAgent::get_agent_info_static() // ============================================================================ int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) -{ return BAMBU_NETWORK_SUCCESS; } +{ + (void) wait_fn; + + if (update_fn) + update_fn(PrintingStageCreate, 0, "Preparing..."); + + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + auto* cloud = get_orca_cloud_agent(); + if (!cloud || params.dev_id.empty()) + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + + const std::string local_path = resolve_local_gcode_path(params); + const fs::path source(local_path); + boost::filesystem::ifstream input(source, std::ios::in | std::ios::binary); + if (!input) { + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; + return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; + } + + const std::string gcode((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + if (input.bad()) { + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: failed to read G-code file: " << local_path; + return BAMBU_NETWORK_ERR_PRINT_WR_POST_TASK_FAILED; + } + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + if (update_fn) + update_fn(PrintingStageUpload, 0, "Uploading G-code..."); + + const int result = cloud->send_print_job(params.dev_id, remote_gcode_name(params), gcode, true); + if (result != BAMBU_NETWORK_SUCCESS) + return result; + + if (update_fn) + update_fn(PrintingStageFinished, 100, "Print started"); + + return BAMBU_NETWORK_SUCCESS; +} int OrcaPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index 0c89dd0a42..1f50fb7a81 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -214,8 +214,8 @@ private: OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true); static bool parse_nonnegative_command_id(const std::string& value, int& result); - // Route one command payload to device//request on the LAN or the cloud - // per-printer connection. The uniform send path for both send_message* overrides. + // Route one command payload to device//request on the LAN or the shared + // cloud connection. The uniform send path for both send_message* overrides. int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str); // Callbacks diff --git a/tests/slic3rutils/test_orca_printer_agent.cpp b/tests/slic3rutils/test_orca_printer_agent.cpp index fec41384b2..7ffc4f3d2f 100644 --- a/tests/slic3rutils/test_orca_printer_agent.cpp +++ b/tests/slic3rutils/test_orca_printer_agent.cpp @@ -118,7 +118,7 @@ TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaP } // Hidden: spawns the connect worker and attempts a real (failing) connect. -TEST_CASE("selecting a cloud printer configures the per-printer socket", "[OrcaPrinterAgent][.integration]") { +TEST_CASE("selecting a cloud printer configures the fleet socket", "[OrcaPrinterAgent][.integration]") { auto cloud = std::make_shared("/tmp"); cloud->set_api_base_url("api.example.com"); OrcaPrinterAgent agent("/tmp"); @@ -132,10 +132,10 @@ TEST_CASE("selecting a cloud printer configures the per-printer socket", "[OrcaP if (!url.empty()) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - CHECK(url == "wss://api.example.com/api/v1/printers/printer-uuid-1/mqtt"); + CHECK(url == "wss://api.example.com/api/v1/printers/mqtt"); - agent.set_user_selected_machine(""); // teardown is synchronous - CHECK(cloud->selected_printer_mqtt_url().empty()); + agent.set_user_selected_machine(""); // selection changes do not tear down the fleet socket + CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/mqtt"); } TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") {