feat: connect to cloud printer and monitor

This commit is contained in:
Ian Chua
2026-08-28 18:10:31 +08:00
parent d486db6459
commit 6a12aca495
9 changed files with 1020 additions and 32 deletions

View File

@@ -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<std::string> 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<std::string>());
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); });
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -34,6 +34,8 @@
#include "DeviceCore/DevManager.h"
#include <boost/log/trivial.hpp>
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() : "<null>");
if (!selected)
return;
set_default();

View File

@@ -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 : "<null>");
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<std::string> 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 : "<null>");
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<std::string> 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 : "<null>");
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;
}

View File

@@ -6,6 +6,8 @@
#include <boost/asio.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/uuid/uuid.hpp>
@@ -20,14 +22,18 @@
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/ssl.h>
#include <algorithm>
#include <cctype>
#include <condition_variable>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <optional>
#include <random>
#include <set>
#include <sstream>
#include <string>
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
bool connacked;
{
std::lock_guard<std::mutex> 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<std::string>& device_ids) {
{
std::lock_guard<std::mutex> 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<std::string>& device_ids) {
{
std::lock_guard<std::mutex> 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<std::mutex> 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<uint8_t>& packet, const std::string& value) {
if (value.size() > 0xffff)
throw std::runtime_error("MQTT string is too long");
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
packet.insert(packet.end(), value.begin(), value.end());
}
void OrcaCloudMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
std::vector<uint8_t> encoded;
do {
uint8_t byte = static_cast<uint8_t>(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<uint8_t> OrcaCloudMqttConnection::make_connect_packet() {
std::vector<uint8_t> 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<uint8_t> OrcaCloudMqttConnection::make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector<std::string>& device_ids) {
std::vector<uint8_t> packet{type};
packet.push_back(static_cast<uint8_t>(packet_id >> 8));
packet.push_back(static_cast<uint8_t>(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<uint8_t> OrcaCloudMqttConnection::make_ping_packet() { return {0xc0, 0}; }
void OrcaCloudMqttConnection::send(WebSocket& websocket, const std::vector<uint8_t>& 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<std::mutex> lock(write_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex
<< static_cast<unsigned int>(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<Connection>();
{
std::lock_guard<std::mutex> 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<boost::beast::http::string_body> 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<int>(static_cast<uint8_t>(connack[0])))
<< " return_code=" << (connack.size() > 3 ? static_cast<int>(static_cast<uint8_t>(connack[3])) : -1);
if (connack.size() != 4 || static_cast<uint8_t>(connack[0]) != 0x20 ||
static_cast<uint8_t>(connack[2]) != 0x00 || static_cast<uint8_t>(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<std::string> devices;
{
std::lock_guard<std::mutex> 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<std::string> subscribe_ids;
std::vector<std::string> unsubscribe_ids;
{
std::lock_guard<std::mutex> 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<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast<unsigned int>(packet_type)
<< " header=0x" << std::hex << static_cast<unsigned int>(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<unsigned int>(static_cast<uint8_t>(packet[index]));
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id="
<< ((static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(packet[index]) << 8) |
static_cast<uint8_t>(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<std::mutex> 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<std::mutex> 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<OrcaCloudMqttConnection>())
{
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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(state_mutex);
return is_connected;
}
@@ -974,16 +1580,59 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> 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<std::string> 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<std::mutex> 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<std::recursive_mutex> 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<std::mutex> 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<std::string>();
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;
}

View File

@@ -2,6 +2,12 @@
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__
#include "ICloudServiceAgent.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <cstdlib>
#include <string>
#include <map>
@@ -9,6 +15,9 @@
#include <atomic>
#include <chrono>
#include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory>
#include <thread>
#include <unordered_map>
@@ -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<std::string()>;
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
using StateHandler = std::function<void(bool connected, bool initial)>;
~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<std::string>& device_ids);
bool unsubscribe(const std::vector<std::string>& 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<boost::beast::tcp_stream>>;
struct Connection;
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
static std::vector<uint8_t> make_connect_packet();
static std::string report_topic(const std::string& device_id);
static std::vector<uint8_t> make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector<std::string>& device_ids);
static std::vector<uint8_t> make_ping_packet();
void send(WebSocket& websocket, const std::vector<uint8_t>& 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<Connection> 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<std::string> subscriptions;
std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions;
std::atomic<uint16_t> 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<std::string> 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/<id>/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<OrcaCloudMqttConnection> 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

View File

@@ -1,5 +1,8 @@
#include "OrcaPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include <boost/log/trivial.hpp>
#include <thread>
namespace Slic3r {
@@ -13,8 +16,35 @@ OrcaPrinterAgent::~OrcaPrinterAgent() = default;
void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
{
std::lock_guard<std::mutex> lock(state_mutex);
m_cloud_agent = cloud;
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: cloud=" << (cloud ? cloud->get_id() : "<null>");
{
std::lock_guard<std::mutex> lock(state_mutex);
m_cloud_agent = cloud;
m_orca_cloud = dynamic_cast<OrcaCloudServiceAgent*>(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/<id>/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<std::mutex> 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<ICloudServiceAgent> 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<ICloudServiceAgent> cloud;
{
std::lock_guard<std::mutex> 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() : "<null>");
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<OrcaCloudServiceAgent*>(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<std::mutex> lock(state_mutex);
selected_machine = dev_id;
std::shared_ptr<ICloudServiceAgent> cloud;
std::string previous;
{
std::lock_guard<std::mutex> 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() : "<null>");
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/<id>/report.
OnMessageFn fn;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(state_mutex);
on_message_fn = fn;
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_message_fn: callback=" << (fn ? "set" : "clear");
return BAMBU_NETWORK_SUCCESS;
}

View File

@@ -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<ICloudServiceAgent> 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;