From eb9cfe0ecb047f57fad5bef0b4e4a515ac7e5e6e Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 28 Aug 2026 18:10:31 +0800 Subject: [PATCH] feat: connect to cloud printer and monitor --- src/slic3r/GUI/DeviceCore/DevManager.cpp | 50 +- src/slic3r/GUI/GUI_App.cpp | 9 +- src/slic3r/GUI/GUI_App.hpp | 2 +- src/slic3r/GUI/Monitor.cpp | 12 +- src/slic3r/Utils/NetworkAgent.cpp | 30 +- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 722 ++++++++++++++++++++- src/slic3r/Utils/OrcaCloudServiceAgent.hpp | 95 +++ src/slic3r/Utils/OrcaPrinterAgent.cpp | 124 +++- src/slic3r/Utils/OrcaPrinterAgent.hpp | 8 + 9 files changed, 1020 insertions(+), 32 deletions(-) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 8f7953005a..1046377e9f 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -572,6 +572,19 @@ namespace Slic3r << " cur_selected=" << selected_machine; auto my_machine_list = get_my_machine_list(); auto it = my_machine_list.find(dev_id); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: set_selected_machine lookup dev_id=" << dev_id + << " found=" << (it != my_machine_list.end()) + << " my_machine_count=" << my_machine_list.size() + << " current_agent=" << get_current_printer_agent_id() + << " provider=" << GUI::wxGetApp().get_printer_cloud_provider(); + if (it != my_machine_list.end() && it->second) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: target machine dev_id=" << it->second->get_dev_id() + << " printer_agent_id=" << it->second->printer_agent_id + << " connection_type=" << it->second->connection_type() + << " dev_connection_type=" << it->second->dev_connection_type; + } else { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: target machine was not found in the current agent's machine list"; + } // disconnect last if dev_id difference from previous one auto last_selected = my_machine_list.find(selected_machine); @@ -582,7 +595,9 @@ namespace Slic3r m_agent->disconnect_printer(); } else if (last_selected->second->connection_type() == "cloud") { - m_agent->set_user_selected_machine(""); + const int result = m_agent->set_user_selected_machine(""); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cleared previous cloud selection dev_id=" + << selected_machine << " result=" << result; } } @@ -634,7 +649,9 @@ namespace Slic3r { // diff dev_id, cloud => set_user_selected_machine(new) BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id; - m_agent->set_user_selected_machine(dev_id); + const int result = m_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: set new cloud selection dev_id=" + << dev_id << " result=" << result; it->second->reset(); } else @@ -662,6 +679,8 @@ namespace Slic3r selected_machine = dev_id; record_user_last_machine(selected_machine); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: DeviceManager selection complete selected_machine=" + << selected_machine; return true; } @@ -692,7 +711,9 @@ namespace Slic3r dev_list.push_back(it->first); BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first; } - m_agent->add_subscribe(dev_list); + const int result = m_agent->add_subscribe(dev_list); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size() + << " result=" << result; } @@ -705,7 +726,9 @@ namespace Slic3r dev_list.push_back(it->first); BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first; } - m_agent->del_subscribe(dev_list); + const int result = m_agent->del_subscribe(dev_list); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size() + << " result=" << result; } void DeviceManager::subscribe_device_list(std::vector dev_list) @@ -869,6 +892,12 @@ namespace Slic3r if (!obj) continue; + // Orca cloud printers are only ever delivered through this REST + // account list; tag them so DeviceManager's cloud/lan branches + // (subscribe + deselect in set_selected_machine) treat them right. + if (provider == "orca") + obj->dev_connection_type = "cloud"; + if (!elem["dev_id"].is_null()) obj->set_dev_id(elem["dev_id"].get()); if (!elem["dev_name"].is_null()) @@ -900,6 +929,12 @@ namespace Slic3r acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end()); obj->set_access_code(acc_code); } + + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: parsed cloud machine dev_id=" << dev_id + << " name=" << obj->get_dev_name() + << " agent_id=" << obj->printer_agent_id + << " connection_type=" << obj->connection_type() + << " online=" << obj->m_is_online; } //remove MachineObject from userMachineList @@ -915,6 +950,9 @@ namespace Slic3r iterat++; } } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: parse_user_print_info complete provider=" << provider + << " parsed_count=" << new_list.size() + << " stored_count=" << userMachineList.size(); } } catch (std::exception& e) @@ -931,10 +969,14 @@ namespace Slic3r unsigned int http_code; std::string body; int result = m_agent->get_user_print_info(&http_code, &body, provider); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: get_user_print_info provider=" << provider + << " result=" << result << " http_code=" << http_code + << " body_bytes=" << body.size(); if (result == 0) { // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: queueing parse_user_print_info on UI thread"; Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); } } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index eb536d93df..4165b071cb 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -4971,11 +4971,12 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER* return false; } -const std::string& GUI_App::get_printer_cloud_provider() const +std::string GUI_App::get_printer_cloud_provider() const { - // Orca todo: this need to be revisted. currently it is mainly used for device manager and related clausses and only bambu machines use them. - // - return BBL_CLOUD_PROVIDER; + std::string provider = preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent"); + if (provider.empty()) + provider = ORCA_CLOUD_PROVIDER; + return provider; } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8bf32df64c..67c82129c8 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -494,7 +494,7 @@ public: bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER); void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER); bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER); - const std::string& get_printer_cloud_provider() const; + std::string get_printer_cloud_provider() const; void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 1a6d969988..dd0350ae0e 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -34,6 +34,8 @@ #include "DeviceCore/DevManager.h" +#include + namespace Slic3r { namespace GUI { @@ -259,6 +261,7 @@ void MonitorPanel::msw_rescale() void MonitorPanel::select_machine(std::string machine_sn) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn; wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED); event->SetString(machine_sn); wxQueueEvent(this, event); @@ -276,13 +279,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event) void MonitorPanel::on_select_printer(wxCommandEvent& event) { Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + const std::string requested_dev_id = event.GetString().ToStdString(); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id=" + << requested_dev_id << " device_manager=" << (dev ? "set" : "null"); if (!dev) return; if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) { m_hms_panel->clear_hms_tag(); } - if (!dev->set_selected_machine(event.GetString().ToStdString())) + const bool selected = dev->set_selected_machine(requested_dev_id); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer set_selected_machine result=" + << selected << " selected_dev_id=" + << (dev->get_selected_machine() ? dev->get_selected_machine()->get_dev_id() : ""); + if (!selected) return; set_default(); diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index e71b57a1b7..01ed03f84f 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -925,8 +925,14 @@ std::string NetworkAgent::get_user_selected_machine() int NetworkAgent::set_user_selected_machine(std::string dev_id) { - if (m_printer_agent) - return m_printer_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent"; return -1; } @@ -946,15 +952,27 @@ int NetworkAgent::stop_subscribe(std::string module) int NetworkAgent::add_subscribe(std::vector dev_list) { - if (m_printer_agent) - return m_printer_agent->add_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: count=" << dev_list.size() + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->add_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent"; return -1; } int NetworkAgent::del_subscribe(std::vector dev_list) { - if (m_printer_agent) - return m_printer_agent->del_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: count=" << dev_list.size() + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->del_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent"; return -1; } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 4419395b4d..4a6c545a82 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include @@ -20,14 +22,18 @@ #include #include #include +#include #include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -59,6 +65,522 @@ using json = nlohmann::json; namespace Slic3r { +struct OrcaCloudMqttConnection::Connection { + boost::asio::io_context io_context; + boost::asio::ssl::context ssl_context; + WebSocket websocket; + boost::asio::ip::tcp::resolver resolver; + + Connection() + : ssl_context(boost::asio::ssl::context::tls_client) + , websocket(io_context, ssl_context) + , resolver(io_context) + {} +}; + +OrcaCloudMqttConnection::~OrcaCloudMqttConnection() { stop(); } + +bool OrcaCloudMqttConnection::start(const std::string& endpoint, TokenProvider token_provider, MessageHandler message_handler, StateHandler state_handler) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start endpoint=" << endpoint + << " token_callback=" << (token_provider ? "set" : "null") + << " message_callback=" << (message_handler ? "set" : "null") + << " state_callback=" << (state_handler ? "set" : "null"); + stop(); + { + std::lock_guard lock(mutex); + endpoint_url = endpoint; + get_token = std::move(token_provider); + on_message = std::move(message_handler); + on_state = std::move(state_handler); + initial_result = false; + initial_completed = false; + connected = false; + } + stopping.store(false); + worker = std::thread(&OrcaCloudMqttConnection::run, this); + + std::unique_lock lock(mutex); + if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) { + initial_completed = true; + initial_result = false; + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds; worker will retry"; + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result + << " initial_completed=" << initial_completed; + return initial_result; +} + +void OrcaCloudMqttConnection::stop() { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested"; + stopping.store(true); + state_cv.notify_all(); + { + std::lock_guard lock(connection_mutex); + if (active_connection) { + auto& socket = boost::beast::get_lowest_layer(active_connection->websocket).socket(); + boost::system::error_code socket_error; + socket.cancel(socket_error); + socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error); + socket.close(socket_error); + active_connection->resolver.cancel(); + } + } + if (worker.joinable()) + worker.join(); + + { + std::lock_guard lock(mutex); + connected = false; + if (!initial_completed) { + initial_completed = true; + initial_result = false; + } + } + initial_cv.notify_all(); +} + +bool OrcaCloudMqttConnection::is_running() const { + return worker.joinable() && !stopping.load(); +} + +void OrcaCloudMqttConnection::flush_subscription_change() { + std::shared_ptr conn; + { + std::lock_guard lock(connection_mutex); + conn = active_connection; + } + bool connacked; + { + std::lock_guard lock(mutex); + connacked = connected; + } + if (!conn || !connacked) + return; // no live MQTT session yet — the worker sends the set on CONNACK + + // beast permits a concurrent writer while the worker is blocked in + // websocket.read(); every write is serialised by write_mutex inside send(). + try { + send_pending_subscriptions(conn->websocket); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: direct subscription write failed (" << e.what() + << "); worker will resend the full set on reconnect"; + } +} + +bool OrcaCloudMqttConnection::subscribe(const std::vector& device_ids) { + { + std::lock_guard lock(mutex); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe requested count=" << device_ids.size() + << " connected=" << connected.load(); + for (const std::string& device_id : device_ids) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe requested dev_id=" << device_id; + if (device_id.empty() || report_topic(device_id).size() > 96) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected invalid dev_id=" << device_id; + return false; + } + } + for (const std::string& device_id : device_ids) { + subscriptions.insert(device_id); + pending_subscriptions.insert(device_id); + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued total_subscriptions=" << subscriptions.size() + << " pending_subscriptions=" << pending_subscriptions.size(); + } + state_cv.notify_all(); + flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect) + return true; +} + +bool OrcaCloudMqttConnection::unsubscribe(const std::vector& device_ids) { + { + std::lock_guard lock(mutex); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe requested count=" << device_ids.size() + << " connected=" << connected.load(); + for (const std::string& device_id : device_ids) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe requested dev_id=" << device_id; + subscriptions.erase(device_id); + pending_subscriptions.erase(device_id); + pending_unsubscriptions.insert(device_id); + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued total_subscriptions=" << subscriptions.size() + << " pending_unsubscriptions=" << pending_unsubscriptions.size(); + } + state_cv.notify_all(); + flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect) + return true; +} + +void OrcaCloudMqttConnection::clear_subscriptions() { + std::lock_guard lock(mutex); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT clear subscriptions count=" << subscriptions.size(); + subscriptions.clear(); + pending_subscriptions.clear(); + pending_unsubscriptions.clear(); +} + +bool OrcaCloudMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) { + constexpr const char* scheme = "wss://"; + constexpr size_t scheme_length = 6; + if (url.compare(0, scheme_length, scheme) != 0) + return false; + + const size_t authority_start = scheme_length; + const size_t path_start = url.find('/', authority_start); + const std::string authority = url.substr(authority_start, path_start - authority_start); + if (authority.empty()) + return false; + + const size_t port_start = authority.rfind(':'); + if (port_start != std::string::npos && authority.find(']') == std::string::npos) { + endpoint.host = authority.substr(0, port_start); + endpoint.port = authority.substr(port_start + 1); + } else { + endpoint.host = authority; + endpoint.port = "443"; + } + endpoint.target = path_start == std::string::npos ? "/" : url.substr(path_start); + return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty(); +} + +void OrcaCloudMqttConnection::append_string(std::vector& packet, const std::string& value) { + if (value.size() > 0xffff) + throw std::runtime_error("MQTT string is too long"); + packet.push_back(static_cast(value.size() >> 8)); + packet.push_back(static_cast(value.size() & 0xff)); + packet.insert(packet.end(), value.begin(), value.end()); +} + +void OrcaCloudMqttConnection::prepend_remaining_length(std::vector& packet, size_t length) { + std::vector encoded; + do { + uint8_t byte = static_cast(length % 128); + length /= 128; + if (length != 0) + byte |= 0x80; + encoded.push_back(byte); + } while (length != 0); + packet.insert(packet.begin() + 1, encoded.begin(), encoded.end()); +} + +std::vector OrcaCloudMqttConnection::make_connect_packet() { + std::vector packet{0x10}; + append_string(packet, "MQTT"); + packet.insert(packet.end(), {4, 2, 0, 60}); // level 4, clean session, 60 s keepalive + append_string(packet, "OrcaSlicer"); + prepend_remaining_length(packet, packet.size() - 1); + return packet; +} + +std::string OrcaCloudMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; } + +std::vector OrcaCloudMqttConnection::make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector& device_ids) { + std::vector packet{type}; + packet.push_back(static_cast(packet_id >> 8)); + packet.push_back(static_cast(packet_id & 0xff)); + for (const std::string& device_id : device_ids) { + append_string(packet, report_topic(device_id)); + if (type == 0x82) // SUBSCRIBE, QoS 0 is sufficient for printer reports. + packet.push_back(0); + } + prepend_remaining_length(packet, packet.size() - 1); + return packet; +} + +std::vector OrcaCloudMqttConnection::make_ping_packet() { return {0xc0, 0}; } + +void OrcaCloudMqttConnection::send(WebSocket& websocket, const std::vector& packet) { + if (packet.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: attempted to send empty MQTT packet"; + return; + } + // Writes come from the worker thread AND, for dynamic (un)subscribes, the + // caller thread. Serialise them; the worker's concurrent read is fine (beast + // allows one reader + one writer). + std::lock_guard lock(write_mutex); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex + << static_cast(packet[0] >> 4) << std::dec + << " bytes=" << packet.size(); + websocket.binary(true); + websocket.write(boost::asio::buffer(packet)); +} + +void OrcaCloudMqttConnection::connect_and_read() { + auto connection = std::make_shared(); + { + std::lock_guard lock(connection_mutex); + active_connection = connection; + if (stopping.load()) + return; + } + + Endpoint endpoint; + if (!parse_endpoint(endpoint_url, endpoint)) { + BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << endpoint_url; + throw std::runtime_error("invalid Orca Cloud WebSocket endpoint"); + } + + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connecting host=" << endpoint.host + << " port=" << endpoint.port << " target=" << endpoint.target; + + auto& websocket = connection->websocket; + const auto results = connection->resolver.resolve(endpoint.host, endpoint.port); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host; + auto& stream = boost::beast::get_lowest_layer(websocket); + stream.expires_after(std::chrono::seconds(10)); + stream.connect(results); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host + << " port=" << endpoint.port; + + // The aggregate viewer is a TLS WebSocket endpoint. Set SNI before the + // TLS handshake so the cloud edge selects the correct certificate. + auto& tls_stream = websocket.next_layer(); + if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str())) + throw std::runtime_error("failed to set Orca Cloud TLS server name"); + connection->ssl_context.set_default_verify_paths(); + tls_stream.set_verify_mode(boost::asio::ssl::verify_peer); + tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host)); + tls_stream.handshake(boost::asio::ssl::stream_base::client); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TLS handshake completed host=" << endpoint.host; + + const std::string token = get_token ? get_token() : std::string(); + if (token.empty()) { + BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT token callback returned an empty token"; + throw std::runtime_error("no access token for Orca Cloud WebSocket"); + } + + websocket.set_option(boost::beast::websocket::stream_base::decorator( + [token](boost::beast::websocket::request_type& request) { + request.set(boost::beast::http::field::user_agent, "OrcaSlicer"); + request.set(boost::beast::http::field::authorization, "Bearer " + token); + request.set("Sec-WebSocket-Protocol", "mqtt"); + })); + boost::beast::http::response response; + boost::system::error_code handshake_error; + websocket.handshake(response, endpoint.host, endpoint.target, handshake_error); + if (handshake_error) { + // Surface the server's HTTP status so a persistent rejection (stale token, + // missing api key, wrong route) is diagnosable from the log rather than an + // opaque "handshake declined". + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudMqttConnection: handshake rejected, http=" + << response.result_int() << " (" << response.reason() << "), " + << handshake_error.message(); + throw boost::system::system_error(handshake_error, "Orca Cloud WebSocket handshake"); + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket handshake completed http=" << response.result_int() + << " negotiated_protocol=" << response["Sec-WebSocket-Protocol"]; + if (response["Sec-WebSocket-Protocol"] != "mqtt") { + BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: WebSocket handshake did not negotiate MQTT"; + throw std::runtime_error("Orca Cloud WebSocket did not negotiate MQTT"); + } + + stream.expires_never(); + send(websocket, make_connect_packet()); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent"; + + boost::beast::flat_buffer buffer; + stream.expires_after(std::chrono::seconds(10)); + websocket.read(buffer); + const std::string connack = boost::beast::buffers_to_string(buffer.data()); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNACK received bytes=" << connack.size() + << " header=" << (connack.empty() ? -1 : static_cast(static_cast(connack[0]))) + << " return_code=" << (connack.size() > 3 ? static_cast(static_cast(connack[3])) : -1); + if (connack.size() != 4 || static_cast(connack[0]) != 0x20 || + static_cast(connack[2]) != 0x00 || static_cast(connack[3]) != 0x00) { + BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT was refused or malformed"; + throw std::runtime_error("Orca Cloud MQTT CONNECT was refused"); + } + + notify_state(true); + reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions"; + send_current_subscriptions(websocket); + std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30); + + while (!stopping.load()) { + send_pending_subscriptions(websocket); + buffer.consume(buffer.size()); + stream.expires_after(std::chrono::seconds(1)); + boost::system::error_code error; + websocket.read(buffer, error); + if (error == boost::beast::error::timeout) { + if (std::chrono::steady_clock::now() >= next_ping) { + send(websocket, make_ping_packet()); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent"; + next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30); + } + continue; + } + if (error) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT WebSocket read failed code=" << error.value() + << " message=" << error.message(); + throw boost::system::system_error(error, "read Orca Cloud MQTT message"); + } + handle_packet(boost::beast::buffers_to_string(buffer.data())); + } + + boost::system::error_code close_error; + websocket.close(boost::beast::websocket::close_code::normal, close_error); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection closed code=" << close_error.value() + << " message=" << close_error.message(); + if (!stopping.load()) + notify_state(false); +} + +void OrcaCloudMqttConnection::send_current_subscriptions(WebSocket& websocket) { + std::vector devices; + { + std::lock_guard lock(mutex); + devices.assign(subscriptions.begin(), subscriptions.end()); + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << devices.size(); + if (!devices.empty()) { + const uint16_t packet_id = next_packet_id++; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE packet_id=" << packet_id; + send(websocket, make_topic_packet(0x82, packet_id, devices)); + } +} + +void OrcaCloudMqttConnection::send_pending_subscriptions(WebSocket& websocket) { + std::vector subscribe_ids; + std::vector unsubscribe_ids; + { + std::lock_guard lock(mutex); + subscribe_ids.assign(pending_subscriptions.begin(), pending_subscriptions.end()); + unsubscribe_ids.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end()); + pending_subscriptions.clear(); + pending_unsubscriptions.clear(); + } + if (!subscribe_ids.empty()) { + const uint16_t packet_id = next_packet_id++; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE count=" << subscribe_ids.size() + << " packet_id=" << packet_id; + for (const std::string& device_id : subscribe_ids) + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: SUBSCRIBE topic=" << report_topic(device_id); + send(websocket, make_topic_packet(0x82, packet_id, subscribe_ids)); + } + if (!unsubscribe_ids.empty()) { + const uint16_t packet_id = next_packet_id++; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE count=" << unsubscribe_ids.size() + << " packet_id=" << packet_id; + for (const std::string& device_id : unsubscribe_ids) + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: UNSUBSCRIBE topic=" << report_topic(device_id); + send(websocket, make_topic_packet(0xa2, packet_id, unsubscribe_ids)); + } +} + +void OrcaCloudMqttConnection::handle_packet(const std::string& packet) { + if (packet.size() < 2) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: received undersized MQTT packet bytes=" << packet.size(); + return; + } + const uint8_t header = static_cast(packet[0]); + const uint8_t packet_type = header >> 4; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast(packet_type) + << " header=0x" << std::hex << static_cast(header) << std::dec + << " bytes=" << packet.size(); + if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status. + if (packet_type == 9 && packet.size() >= 5) { + std::ostringstream result_codes; + for (size_t index = 4; index < packet.size(); ++index) { + if (index != 4) + result_codes << ','; + result_codes << "0x" << std::hex << static_cast(static_cast(packet[index])); + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id=" + << ((static_cast(static_cast(packet[2])) << 8) | + static_cast(static_cast(packet[3]))) + << " result_codes=" << result_codes.str(); + } + return; + } + size_t index = 1; + size_t multiplier = 1; + size_t remaining = 0; + uint8_t encoded = 0; + do { + if (index >= packet.size() || multiplier > 128 * 128 * 128) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH remaining length"; + return; + } + encoded = static_cast(packet[index++]); + remaining += (encoded & 0x7f) * multiplier; + multiplier *= 128; + } while ((encoded & 0x80) != 0); + const size_t remaining_end = index + remaining; + if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH body remaining=" << remaining + << " packet_bytes=" << packet.size(); + return; + } + const uint16_t topic_length = (static_cast(packet[index]) << 8) | + static_cast(packet[index + 1]); + index += 2; + if (topic_length > packet.size() - index) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH topic length=" << topic_length; + return; + } + const std::string topic(packet.data() + index, topic_length); + index += topic_length; + if (((header >> 1) & 0x03) != 0) { + if (index + 2 > remaining_end) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH packet identifier"; + return; + } + index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0. + } + const size_t payload_size = remaining_end - index; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received PUBLISH topic=" << topic + << " payload_bytes=" << payload_size + << " message_callback=" << (on_message ? "set" : "null"); + if (on_message) + on_message(topic, packet.substr(index, remaining_end - index)); + else + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set"; +} + +void OrcaCloudMqttConnection::notify_state(bool is_now_connected) { + StateHandler callback; + bool initial = false; + { + std::lock_guard lock(mutex); + connected = is_now_connected; + initial = !initial_completed; + if (initial) { + initial_result = is_now_connected; + initial_completed = true; + } + callback = on_state; + } + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT state changed connected=" << is_now_connected + << " initial=" << initial << " state_callback=" << (callback ? "set" : "null"); + if (initial) + initial_cv.notify_all(); + else if (callback) + callback(is_now_connected, false); +} + +void OrcaCloudMqttConnection::run() { + while (!stopping.load()) { + const int retry_seconds = reconnect_delay_seconds.load(); + try { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt retry_delay=" << retry_seconds; + connect_and_read(); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt failed: " << error.what(); + if (!stopping.load()) + notify_state(false); + } + if (stopping.load()) + break; + // Grow the backoff only across attempts that never reached CONNACK; a + // successful connection resets reconnect_delay_seconds to 1 (connect_and_read). + reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30)); + std::unique_lock lock(mutex); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT waiting before reconnect seconds=" << retry_seconds; + state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); }); + } +} + namespace { constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com"; constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com"; @@ -82,6 +604,7 @@ constexpr const char* ORCA_UNSUBSCRIBE_PLUGINS = "/api/v1/plugins/subscriptions" constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine"; constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins"; constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download"; +constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers"; constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login"; @@ -490,6 +1013,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) , api_base_url(ORCA_DEFAULT_API_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL) + , mqtt_connection(std::make_unique()) { auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; pkce_bundle.loopback_port = choose_loopback_port(); @@ -500,6 +1024,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) OrcaCloudServiceAgent::~OrcaCloudServiceAgent() { + if (mqtt_connection) + mqtt_connection->stop(); if (refresh_thread.joinable()) { refresh_thread.join(); } @@ -938,22 +1464,102 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu int OrcaCloudServiceAgent::connect_server() { + const bool logged_in = is_user_login(); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in + << " api_base_url=" << api_base_url; + if (!logged_in) { + if (mqtt_connection) + mqtt_connection->stop(); + { + std::lock_guard lock(state_mutex); + is_connected = false; + } + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user"; + invoke_server_connected_callback(-1, 401); + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + std::string response; unsigned int http_code = 0; int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); - { - std::lock_guard lock(state_mutex); - is_connected = connected; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code + << " connected=" << connected << " response_bytes=" << response.size(); + + if (connected && mqtt_connection && !mqtt_connection->is_running()) { + // Only (re)start when the worker isn't already alive. connect_server() is + // also called every ~5s by DeviceManagerRefresher::on_timer via + // refresh_connection(); start() begins with stop(), so calling it + // unconditionally tears down and rebuilds a healthy socket every tick. + const std::string endpoint = "wss://" + api_base_url + "/api/v1/printers/mqtt"; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: starting aggregate MQTT endpoint=" << endpoint; + // Fire-and-forget: start() spawns a worker that reconnects with exponential + // backoff. A failed *initial* attempt (token/network not ready yet during a + // startup gap) must NOT gate the socket's lifetime here — folding it into + // `connected` trips the stop() below and kills the retry loop for the whole + // session. The socket is torn down only on logout / clear_session. + const bool mqtt_started = mqtt_connection->start( + endpoint, + [this] { return get_access_token(); }, + [this](const std::string& topic, const std::string& message) { + constexpr const char* prefix = "device/"; + constexpr const char* suffix = "/report"; + if (topic.compare(0, 7, prefix) != 0 || topic.size() <= 14 || + topic.compare(topic.size() - 7, 7, suffix) != 0) + return; + const std::string device_id = topic.substr(7, topic.size() - 14); + OnMessageFn callback; + { + std::lock_guard lock(callback_mutex); + callback = printer_status_callback; + } + if (callback) + callback(device_id, message); + }, + [this](bool socket_connected, bool initial) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: aggregate MQTT state callback connected=" + << socket_connected << " initial=" << initial; + if (initial) + return; + { + std::lock_guard lock(state_mutex); + is_connected = socket_connected; + } + invoke_server_connected_callback(socket_connected ? 0 : -1, 0); + }); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: aggregate MQTT start returned=" << mqtt_started; + } + if (!connected) { + // Transient health-check failure (DNS blip / brief 5xx). Do NOT stop the + // MQTT worker — it owns its own reconnect loop, and connect_server() runs + // on the 5s refresher tick. The socket is torn down only on logout (the + // !logged_in branch above) and clear_session(). + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health check failed; leaving aggregate MQTT running"; } - invoke_server_connected_callback(connected ? 0 : -1, http_code); - return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; + // While the aggregate MQTT worker is alive it owns is_connected via its + // StateHandler. Don't let the 5s health probe overwrite it (a DNS blip would + // otherwise flap the "server connected" state and the Device tab). + const bool mqtt_alive = mqtt_connection && mqtt_connection->is_running(); + if (!mqtt_alive) { + { + std::lock_guard lock(state_mutex); + is_connected = connected; + } + invoke_server_connected_callback(connected ? 0 : -1, http_code); + } + + return (connected || mqtt_alive) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } bool OrcaCloudServiceAgent::is_server_connected() { + // The aggregate MQTT socket is the real signal. While its worker is alive, + // report its actual CONNACK state — immune to the 5s health probe's DNS blips. + // Fall back to the last health-check result only when there is no socket. + if (mqtt_connection && mqtt_connection->is_running()) + return mqtt_connection->is_connected(); std::lock_guard lock(state_mutex); return is_connected; } @@ -974,16 +1580,59 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module) int OrcaCloudServiceAgent::add_subscribe(std::vector dev_list) { - (void) dev_list; - return BAMBU_NETWORK_SUCCESS; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size() + << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null"); + if (!is_user_login() || !mqtt_connection) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const bool queued = mqtt_connection->subscribe(dev_list); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued; + return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) { - (void) dev_list; + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size() + << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null"); + if (!is_user_login() || !mqtt_connection) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + const bool queued = mqtt_connection->unsubscribe(dev_list); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued; + return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; +} + +int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn) +{ + std::lock_guard lock(callback_mutex); + printer_status_callback = std::move(fn); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear"); return BAMBU_NETWORK_SUCCESS; } +int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body) +{ + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id + << " body_bytes=" << body.size() << " logged_in=" << is_user_login(); + if (dev_id.empty() || !is_user_login()) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands"; + std::string response; + unsigned int http_code = 0; + int result = http_post(path, body, &response, &http_code); + BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id + << " http=" << http_code << " result=" << result + << " response_bytes=" << response.size(); + return (result == BAMBU_NETWORK_SUCCESS && 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); @@ -2021,6 +2670,10 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti void OrcaCloudServiceAgent::clear_session() { + if (mqtt_connection) { + mqtt_connection->stop(); + mqtt_connection->clear_subscriptions(); + } { std::lock_guard lock(session_mutex); session = SessionInfo{}; @@ -2620,11 +3273,56 @@ int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable) int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; + std::string response; + unsigned int code = 0; + int result = http_get(ORCA_CLOUD_PRINTER, &response, &code); + if (http_code) - *http_code = 200; - if (http_body) - *http_body = "{}"; + *http_code = code; + + if (result != 0 || code != 200) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response; + return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; + } + + BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response; + + try { + auto resp_json = nlohmann::json::parse(response); + nlohmann::json devices = nlohmann::json::array(); + + for (const auto& printer : resp_json.value("data", nlohmann::json::array())) { + nlohmann::json device; + device["dev_id"] = printer.value("id", ""); + device["dev_name"] = printer.value("name", ""); + if (printer.contains("model") && printer["model"].is_string()) + device["dev_model_name"] = printer["model"].get(); + + bool online = false; + if (printer.contains("status_snapshot") && printer["status_snapshot"].is_object()) { + const auto& status = printer["status_snapshot"].value("status", nlohmann::json::object()); + online = status.value("connection", nlohmann::json::object()).value("state", "") == "online"; + if (status.contains("job") && status["job"].is_object()) + device["task_status"] = status["job"].value("state", ""); + } + device["dev_online"] = online; + + devices.push_back(device); + } + + if (http_body) { + nlohmann::json out; + out["devices"] = devices; + *http_body = out.dump(); + } + + BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size() + << ", devices=" << devices.dump(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what(); + return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; + } + return BAMBU_NETWORK_SUCCESS; } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index 3ae86ec27c..95f95b7d92 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -2,6 +2,12 @@ #define __ORCA_CLOUD_SERVICE_AGENT_HPP__ #include "ICloudServiceAgent.hpp" + +#include +#include +#include +#include +#include #include #include #include @@ -9,6 +15,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -21,6 +30,78 @@ namespace Slic3r { // Forward declarations class AppConfig; +// MQTT 3.1.1 over the aggregate WebSocket is deliberately kept here instead +// of using the printer SDK. The endpoint is a read-only status stream; MQTT +// PUBLISH must never be sent on it because the cloud closes such sessions. +class OrcaCloudMqttConnection +{ +public: + using TokenProvider = std::function; + using MessageHandler = std::function; + using StateHandler = std::function; + + ~OrcaCloudMqttConnection(); + + bool start(const std::string& endpoint, TokenProvider token_provider, MessageHandler message_handler, StateHandler state_handler); + void stop(); + // True while the worker thread is alive (connected OR retrying). Lets callers + // avoid restarting a healthy connection. + bool is_running() const; + // True once CONNACK has been received and the socket has not since dropped. + bool is_connected() const { return connected.load(); } + bool subscribe(const std::vector& device_ids); + bool unsubscribe(const std::vector& device_ids); + void clear_subscriptions(); + +private: + struct Endpoint { std::string host; std::string port; std::string target; }; + using WebSocket = boost::beast::websocket::stream< + boost::asio::ssl::stream>; + struct Connection; + + static bool parse_endpoint(const std::string& url, Endpoint& endpoint); + static void append_string(std::vector& packet, const std::string& value); + static void prepend_remaining_length(std::vector& packet, size_t length); + static std::vector make_connect_packet(); + static std::string report_topic(const std::string& device_id); + static std::vector make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector& device_ids); + static std::vector make_ping_packet(); + + void send(WebSocket& websocket, const std::vector& packet); + // Emit a queued SUBSCRIBE/UNSUBSCRIBE on the live socket right now (from the + // caller thread), so a selection change is applied without waiting for the + // blocking read loop to next return. No-op if no CONNACKed socket exists yet + // (the worker sends the set on connect). The aggregate viewer is dynamic — the + // WebSocket is never dropped for a subscription change. + void flush_subscription_change(); + void connect_and_read(); + void send_current_subscriptions(WebSocket& websocket); + void send_pending_subscriptions(WebSocket& websocket); + void handle_packet(const std::string& packet); + void notify_state(bool is_now_connected); + void run(); + + std::atomic_bool stopping{true}; + std::atomic_int reconnect_delay_seconds{1}; + std::thread worker; + std::mutex mutex; + std::mutex connection_mutex; + std::mutex write_mutex; // serialises every websocket write (worker + caller threads) + std::shared_ptr active_connection; + std::condition_variable initial_cv; + std::condition_variable state_cv; + std::string endpoint_url; + TokenProvider get_token; + MessageHandler on_message; + StateHandler on_state; + std::set subscriptions; + std::set pending_subscriptions; + std::set pending_unsubscriptions; + std::atomic next_packet_id{1}; + bool initial_result{false}; + bool initial_completed{false}; + std::atomic_bool connected{false}; +}; struct BundleMetadata; struct PluginDescriptor; struct PluginChangelog; @@ -207,6 +288,18 @@ public: int del_subscribe(std::vector dev_list) override; void enable_multi_machine(bool enable) override; + // The aggregate printer socket is status-only. OrcaPrinterAgent registers + // its normal message callback here and adds/removes device report topics + // through add_subscribe()/del_subscribe(). Printer commands continue to + // use the REST commands endpoint; they must never be published here. + int set_printer_status_callback(OnMessageFn fn); + + // Send a Bambu-dialect command to one printer via the cloud relay's REST + // endpoint (POST /api/v1/printers//commands). Synchronous - wraps http_post, + // so it carries the standard apikey + bearer headers and token refresh. Callers + // that need non-blocking behaviour run it on their own thread. + int send_printer_command(const std::string& dev_id, const std::string& body); + // ======================================================================== // ICloudServiceAgent Interface Implementation - Settings Synchronization // ======================================================================== @@ -423,6 +516,7 @@ private: std::chrono::system_clock::now().time_since_epoch()).count()}; // Member variables - connection state + std::unique_ptr mqtt_connection; bool is_connected{false}; bool enable_track{false}; bool multi_machine_enabled{false}; @@ -436,6 +530,7 @@ private: AppOnHttpErrorFn on_http_error_fn; GetCountryCodeFn get_country_code_fn; QueueOnMainFn queue_on_main_fn; + OnMessageFn printer_status_callback; mutable std::mutex callback_mutex; // Thread safety diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index cb70dafcca..93cadd9abd 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,5 +1,8 @@ #include "OrcaPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" +#include "OrcaCloudServiceAgent.hpp" +#include +#include namespace Slic3r { @@ -13,8 +16,35 @@ OrcaPrinterAgent::~OrcaPrinterAgent() = default; void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud) { - std::lock_guard lock(state_mutex); - m_cloud_agent = cloud; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: cloud=" << (cloud ? cloud->get_id() : ""); + { + std::lock_guard lock(state_mutex); + m_cloud_agent = cloud; + m_orca_cloud = dynamic_cast(cloud.get()); + } + if (!m_orca_cloud) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_cloud_agent: cloud is not OrcaCloudServiceAgent"; + return; // BBL provider active - nothing to bridge + } + + // OrcaCloudServiceAgent owns the aggregate MQTT socket; it already strips the + // device//report topic and hands us (dev_id, raw_json). Forward to the + // standard sink. message_arrive_fn self-marshals to the UI thread via CallAfter, + // so being called from the MQTT worker thread is fine. + const int callback_result = m_orca_cloud->set_printer_status_callback([this](std::string dev_id, std::string payload) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: received cloud status dev_id=" << dev_id + << " payload_bytes=" << payload.size(); + OnMessageFn fn; + { + std::lock_guard lock(state_mutex); + fn = on_message_fn; + } + if (fn) + fn(std::move(dev_id), std::move(payload)); + else + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: cloud status has no registered on_message callback"; + }); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result; } // ============================================================================ @@ -23,6 +53,34 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { + (void) qos; + (void) flag; // MQTT concepts; N/A for the REST command endpoint + + std::shared_ptr cloud; + { + std::lock_guard lock(state_mutex); + cloud = m_cloud_agent; + } + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::send_message: dev_id=" << dev_id + << " payload_bytes=" << json_str.size() << " qos=" << qos << " flag=" << flag + << " cloud=" << (cloud ? cloud->get_id() : ""); + if (!cloud || dev_id.empty()) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::send_message: rejected due to missing cloud or device ID"; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + // Detached worker so the UI thread is never blocked on HTTP. Capture a shared_ptr + // copy (keeps the cloud agent alive) - never `this`. + std::thread([cloud, dev_id, body = std::move(json_str)]() { + if (auto* orca = dynamic_cast(cloud.get())) { + const int result = orca->send_printer_command(dev_id, body); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::send_message: cloud command result=" << result + << " dev_id=" << dev_id; + } else { + BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent::send_message: cloud agent is not OrcaCloudServiceAgent"; + } + }).detach(); + return BAMBU_NETWORK_SUCCESS; } @@ -123,11 +181,68 @@ std::string OrcaPrinterAgent::get_user_selected_machine() int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) { - std::lock_guard lock(state_mutex); - selected_machine = dev_id; + std::shared_ptr cloud; + std::string previous; + { + std::lock_guard lock(state_mutex); + if (dev_id == selected_machine) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id; + return BAMBU_NETWORK_SUCCESS; + } + previous = selected_machine; + selected_machine = dev_id; + cloud = m_cloud_agent; + } + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous + << " new=" << dev_id << " cloud=" << (cloud ? cloud->get_id() : ""); + if (!cloud) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no cloud agent"; + return BAMBU_NETWORK_SUCCESS; + } + + // One report topic at a time. add_subscribe/del_subscribe only mutate a set and + // wake the MQTT worker, so they are safe to call synchronously on the UI thread. + if (!previous.empty()) { + const int result = cloud->del_subscribe({previous}); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unsubscribe dev_id=" << previous + << " result=" << result; + } + if (!dev_id.empty()) { + const int result = cloud->add_subscribe({dev_id}); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: subscribe dev_id=" << dev_id + << " result=" << result; + // Relay retains nothing: ask the printer for a full snapshot. Async inside + // send_message; returns immediately. + send_message(dev_id, + R"({"pushing":{"command":"pushall","sequence_id":"20001","version":1,"push_target":1}})", + 0, 0); + deliver_mock_get_version(dev_id); + } return BAMBU_NETWORK_SUCCESS; } +void OrcaPrinterAgent::deliver_mock_get_version(const std::string& dev_id) +{ + // The printer would answer an info.get_version request with its firmware/module + // list; OrcaCloud does not relay that yet, so MachineObject::module_vers stays + // empty and is_info_ready(check_version) never passes (StatusPanel bails, every + // field renders N/A). Synthesize the reply and push it through the same sink as + // real report messages so parse_json handles it identically. Remove once the + // backend answers info.get_version on device//report. + OnMessageFn fn; + { + std::lock_guard lock(state_mutex); + fn = on_message_fn; + } + if (!fn) + return; + static const std::string kMockGetVersion = + R"({"info":{"command":"get_version","sequence_id":"0","module":[)" + R"({"name":"ota","product_name":"OrcaCloud Printer","hw_ver":"","sw_ver":"01.00.00.00","sn":""}]}})"; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: delivering mock info.get_version for dev_id=" << dev_id; + fn(dev_id, kMockGetVersion); +} + // ============================================================================ // Agent Information // ============================================================================ @@ -197,6 +312,7 @@ int OrcaPrinterAgent::set_on_message_fn(OnMessageFn fn) { std::lock_guard lock(state_mutex); on_message_fn = fn; + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_message_fn: callback=" << (fn ? "set" : "clear"); return BAMBU_NETWORK_SUCCESS; } diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index a1613420a5..9cf2b29638 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -9,6 +9,8 @@ namespace Slic3r { +class OrcaCloudServiceAgent; + /** * OrcaPrinterAgent - Stub implementation for printer operations. * @@ -81,6 +83,12 @@ private: std::string log_dir; std::string selected_machine; std::shared_ptr m_cloud_agent; + OrcaCloudServiceAgent* m_orca_cloud = nullptr; // == m_cloud_agent.get() when the Orca provider is active + + // MOCK: OrcaCloud does not yet relay the printer's info.get_version reply, so + // synthesize it and feed it through on_message_fn (same sink as real report + // messages). Delete once the backend answers info.get_version. + void deliver_mock_get_version(const std::string& dev_id); // Callbacks OnMsgArrivedFn on_ssdp_msg_fn;