refactor: move orcasonar changes to dedicated branch

This commit is contained in:
Ian Chua
2026-10-05 16:23:53 +08:00
parent 2b1dba272c
commit 797c2c1b33
11 changed files with 49 additions and 3463 deletions
+1 -5
View File
@@ -770,12 +770,8 @@ set(SLIC3R_GUI_SOURCES
Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaMqttConnection.cpp
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp
Utils/QidiPrinterAgent.hpp
Utils/SnapmakerPrinterAgent.cpp
@@ -915,7 +911,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
if (WIN32)
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include)
target_link_libraries(libslic3r_gui Advapi32 Crypt32)
target_link_libraries(libslic3r_gui Advapi32)
endif()
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
+1 -51
View File
@@ -31,19 +31,8 @@
#include <curl/curl.h>
#include <utility>
#include <openssl/err.h>
#include <openssl/ssl.h>
#ifdef OPENSSL_CERT_OVERRIDE
#include <openssl/x509.h>
#include <openssl/x509err.h>
#ifdef _WIN32
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <wincrypt.h>
// wincrypt.h uses this token for a certificate-name property identifier.
# undef X509_NAME
#endif
namespace fs = boost::filesystem;
@@ -967,45 +956,6 @@ std::string Http::tls_system_cert_store()
return ret;
}
void Http::add_platform_root_certificates(SSL_CTX* ssl_context)
{
#ifdef _WIN32
X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context);
if (!openssl_store)
throw std::runtime_error("unable to get OpenSSL certificate store");
const auto load_store = [&](DWORD location) {
HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0,
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
L"ROOT");
if (!windows_store)
return;
PCCERT_CONTEXT windows_certificate = nullptr;
while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) {
const unsigned char* encoded = windows_certificate->pbCertEncoded;
X509* certificate = d2i_X509(nullptr, &encoded, static_cast<long>(windows_certificate->cbCertEncoded));
if (!certificate) {
ERR_clear_error();
continue;
}
ERR_clear_error();
if (X509_STORE_add_cert(openssl_store, certificate) != 1)
ERR_clear_error();
X509_free(certificate);
}
CertCloseStore(windows_store, 0);
};
load_store(CERT_SYSTEM_STORE_CURRENT_USER);
load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE);
#else
(void)ssl_context;
#endif
}
std::string Http::url_encode(const std::string &str)
{
::CURL *curl = ::curl_easy_init();
-6
View File
@@ -13,8 +13,6 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r_version.h"
typedef struct ssl_ctx_st SSL_CTX;
#define MAX_SIZE_TO_FILE 3*1024
namespace Slic3r {
@@ -202,10 +200,6 @@ public:
// Return empty string on success or error message on fail.
static std::string tls_global_init();
static std::string tls_system_cert_store();
// Add platform root certificates to a standalone OpenSSL context. This
// supplements set_default_verify_paths() on platforms where OpenSSL does
// not use the native certificate store.
static void add_platform_root_certificates(SSL_CTX* ssl_context);
// converts the given string to an url_encoded_string
static std::string url_encode(const std::string &str);
+12 -296
View File
@@ -1,8 +1,6 @@
#include "OrcaCloudServiceAgent.hpp"
#include "CloudProvider.hpp"
#include "Http.hpp"
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudSignalingChannel.hpp"
#include "bambu_networking.hpp"
#include "ICloudServiceAgent.hpp"
#include "libslic3r/Utils.hpp"
@@ -14,8 +12,6 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/log/trivial.hpp>
@@ -41,18 +37,14 @@
#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>
@@ -520,7 +512,6 @@ 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<OrcaMqttConnection>())
{
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
pkce_bundle.loopback_port = choose_loopback_port();
@@ -531,8 +522,6 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
{
if (mqtt_connection)
mqtt_connection->stop();
if (refresh_thread.joinable()) {
refresh_thread.join();
}
@@ -975,45 +964,22 @@ 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);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code
<< " connected=" << connected << " response_bytes=" << response.size();
// connect_server() remains a REST health probe. The long-lived fleet MQTT socket
// is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt;
// subscriptions queued before that point are replayed when it starts.
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected;
}
invoke_server_connected_callback(connected ? 0 : -1, http_code);
return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
bool OrcaCloudServiceAgent::is_server_connected()
{
// The REST health probe is the signal; the per-printer MQTT socket does not gate
// whole-cloud connectivity (one printer reconnecting must not report the whole
// cloud as lost).
std::lock_guard<std::recursive_mutex> lock(state_mutex);
return is_connected;
}
@@ -1034,238 +1000,13 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
{
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;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->subscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
(void) dev_list;
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> 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;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->unsubscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler)
{
(void) dev_id;
if (!ensure_token_fresh("configure_selected_printer_mqtt"))
{
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
return BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
OrcaMqttConnection::Config cfg;
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
cfg.use_tls = true;
cfg.bearer_provider = [this] { return get_access_token(); };
cfg.client_id = "OrcaSlicer";
cfg.keepalive_seconds = 300;
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url = cfg.url;
}
if (mqtt_connection->is_running()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running";
return BAMBU_NETWORK_SUCCESS;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url;
// NOTE: no lock is held across start() — it blocks for the whole initial connect
// attempt (up to ~10s), and the message handler below re-enters callback_mutex on
// the MQTT worker thread.
const bool ok = mqtt_connection->start(
cfg,
[this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); },
std::move(state_handler));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok;
return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
void OrcaCloudServiceAgent::teardown_selected_printer_mqtt()
{
if (mqtt_connection) {
mqtt_connection->stop();
// The connection object is reused for the next printer; drop this printer's
// report topic so its 1:1 socket does not re-subscribe the previous device.
mqtt_connection->clear_subscriptions();
}
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url.clear();
}
std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
return m_selected_printer_mqtt_url;
}
void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload)
{
OnMessageFn callback;
{
std::lock_guard<std::mutex> lock(callback_mutex);
callback = printer_status_callback;
}
if (callback)
callback(dev_id, payload);
}
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;
}
int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn)
{
if (dev_id.empty() || local_gcode_path.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No
// metadata rides this request; filename/start are only relevant to the HTTP
// .../start finalize route, which this MQTT-driven flow does not call.
const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads";
std::string response;
unsigned int http_code = 0;
int result = http_post(uploads_path, "{}", &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
std::string upload_job_id;
std::string upload_url;
try {
const nlohmann::json j = nlohmann::json::parse(response);
upload_job_id = j.value("job_id", "");
upload_url = j.value("upload_url", "");
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what();
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (upload_job_id.empty() || upload_url.empty()) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url";
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This
// is a scoped, PUT-only, short-TTL capability with no bearer token of its own,
// so it bypasses http_put (which always prefixes api_base_url and attaches the
// cloud session's Authorization header - neither belongs on an R2 PUT).
bool canceled = false;
unsigned put_status = 0;
std::string put_error;
Http::put(upload_url)
.tls_verify(true)
.header("Content-Type", "text/x.gcode")
.set_put_body(boost::filesystem::path(local_gcode_path))
.timeout_connect(5)
.timeout_max(300) // large G-code over a slow link
.on_progress([&](Http::Progress progress, bool& cancel) {
if (cancel_fn && cancel_fn()) {
cancel = true;
canceled = true;
return;
}
if (update_fn && progress.ultotal > 0) {
const int percent = static_cast<int>((progress.ulnow * 100) / progress.ultotal);
update_fn(PrintingStageUpload, percent, "Uploading...");
}
})
.on_complete([&](std::string, unsigned status) { put_status = status; })
.on_error([&](std::string, std::string err, unsigned status) {
put_status = status;
put_error = std::move(err);
})
.perform_sync();
if (canceled)
return BAMBU_NETWORK_ERR_CANCELED;
if (put_status < 200 || put_status >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error;
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
}
if (job_id)
*job_id = std::move(upload_job_id);
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start)
{
if (dev_id.empty() || job_id.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
nlohmann::json body;
if (!filename.empty())
body["filename"] = filename;
body["start"] = start;
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" +
Http::url_encode(job_id) + "/start";
std::string response;
unsigned int http_code = 0;
const int result = http_post(path, body.dump(), &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
(void) dev_list;
return BAMBU_NETWORK_SUCCESS;
}
@@ -2300,10 +2041,6 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
void OrcaCloudServiceAgent::clear_session(bool all_backends)
{
if (mqtt_connection) {
mqtt_connection->stop();
mqtt_connection->clear_subscriptions();
}
{
std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{};
@@ -2419,11 +2156,7 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type)
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
{
std::string url = api_base_url + path;
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url;
@@ -2447,7 +2180,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path,
http.header("Authorization", "Bearer " + token);
}
http.header("Content-Type", content_type);
http.header("Content-Type", "application/json");
http.set_post_body(body);
http.on_complete([&](std::string resp_body, unsigned resp_status) {
@@ -2914,28 +2647,19 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
if (http_code)
*http_code = code;
if (result != 0 || code != 200) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response;
if (result != 0 || code != 200)
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;
std::string role = printer.value("access_role", "");
// A printer with the role "view" only has monitoring access for orca cloud.
// The printer is owned by a different person and was shared to the current user without
// any permission to control the printer so we discard this printer. Comment this out if
// OrcaSlicer wants to support view only printers.
const std::string role = printer.value("access_role", "");
if (role.empty() || role == "viewer")
continue;
nlohmann::json device;
device["dev_id"] = printer.value("id", "");
device["dev_name"] = printer.value("name", "");
if (printer.contains("model") && printer["model"].is_string())
@@ -2949,20 +2673,15 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
device["task_status"] = status["job"].value("state", "");
}
device["dev_online"] = online;
devices.push_back(device);
devices.push_back(std::move(device));
}
if (http_body) {
nlohmann::json out;
out["devices"] = devices;
out["devices"] = std::move(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();
} catch (const std::exception&) {
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
@@ -3047,9 +2766,6 @@ int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function<void
return BAMBU_NETWORK_SUCCESS;
}
std::unique_ptr<ICameraSignalingChannel> OrcaCloudServiceAgent::create_camera_signaling_channel(const std::string& dev_id)
{ return std::make_unique<OrcaCloudSignalingChannel>(this, dev_id); }
int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback)
{
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)";
+2 -57
View File
@@ -4,11 +4,6 @@
#include "CloudProvider.hpp"
#include "ICameraSignalingChannel.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 "bambu_networking.hpp"
#include <cstdlib>
#include "libslic3r/ProjectTask.hpp"
@@ -18,9 +13,6 @@
#include <atomic>
#include <chrono>
#include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory>
#include <thread>
#include <unordered_map>
@@ -28,8 +20,6 @@
#include <vector>
#include <nlohmann/json.hpp>
#include "OrcaMqttConnection.hpp"
class wxSecretStore;
namespace Slic3r {
@@ -166,8 +156,6 @@ public:
// Configuration
void configure_urls(AppConfig* app_config);
// Hostname only; cloud REST, MQTT, and WebRTC signaling use fixed TLS
// endpoints on port 443.
void set_api_base_url(const std::string& url);
void set_auth_base_url(const std::string& url);
void set_cloud_base_url(const std::string& url);
@@ -223,19 +211,6 @@ public:
int add_subscribe(std::vector<std::string> dev_list) override;
int del_subscribe(std::vector<std::string> dev_list) override;
void enable_multi_machine(bool enable) override;
int set_printer_status_callback(OnMessageFn fn);
int send_printer_command(const std::string& dev_id, const std::string& body);
int upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn);
int start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start = true);
// ========================================================================
// ICloudServiceAgent Interface Implementation - Settings Synchronization
@@ -271,7 +246,7 @@ public:
// ICloudServiceAgent Interface Implementation - Model Mall & Publishing
// ========================================================================
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override;
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
// std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) override;
int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override;
int get_model_publish_url(std::string* url) override;
@@ -377,29 +352,7 @@ public:
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
OrcaMqttConnection* get_mqtt_connection() noexcept {
return mqtt_connection.get();
}
const OrcaMqttConnection* get_mqtt_connection() const noexcept {
return mqtt_connection.get();
}
// Account-scoped cloud socket: wss://<api_base_url>/api/v1/printers/mqtt.
// configure_ blocks for the duration of the initial connect attempt, so callers
// drive it off the UI thread; teardown_ is synchronous. The dev_id argument is
// retained for source compatibility with the printer-agent lifecycle; it does
// not participate in endpoint construction.
int configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler = {});
void teardown_selected_printer_mqtt();
// Test hook: the wss:// URL of the current fleet socket ("" when none).
std::string selected_printer_mqtt_url() const;
private:
// Fans one inbound fleet MQTT message out to printer_status_callback.
void deliver_cloud_message(const std::string& dev_id, const std::string& payload);
// Sync protocol helpers
int sync_pull(
std::function<void(const SyncPullResponse&)> on_success,
@@ -423,11 +376,7 @@ private:
// HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type = "application/json");
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers();
@@ -485,9 +434,6 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state
std::unique_ptr<OrcaMqttConnection> mqtt_connection;
std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex
mutable std::mutex m_selected_url_mutex;
bool is_connected{false};
bool enable_track{false};
bool multi_machine_enabled{false};
@@ -501,7 +447,6 @@ 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
@@ -1,337 +0,0 @@
#include "OrcaCloudSignalingChannel.hpp"
#include "Http.hpp"
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/beast/core.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <openssl/ssl.h>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace Slic3r {
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id)
: m_cloud(cloud)
, m_dev_id(std::move(dev_id))
{
}
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
{
close();
}
void OrcaCloudSignalingChannel::open()
{
bool expected = false;
if (!m_open.compare_exchange_strong(expected, true))
return;
m_stop.store(false);
m_thread = std::thread([this] { run(); });
}
void OrcaCloudSignalingChannel::close()
{
m_stop.store(true);
std::shared_ptr<Connection> conn;
Http::Ptr request;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
request = m_inflight_requests;
}
// m_conn is installed only after the live-token request completes, so
// cancel the request independently before waiting for the worker.
if (request)
request->cancel();
if (conn) {
// Established session: close the socket on the io_context's own thread so
// the pending async_read completes and io_context.run() unwinds.
boost::asio::post(conn->io_context, [conn] {
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
});
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
// direct interruption.
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
}
if (m_thread.joinable())
m_thread.join();
m_open.store(false);
}
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
{
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
}
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
{
send_json(nlohmann::json{{"type", "webrtc.ice"},
{"candidate", std::move(candidate)},
{"sdpMid", std::move(mid)}}
.dump());
}
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
{
std::ostringstream encoded;
encoded << std::uppercase << std::hex;
for (unsigned char c : value) {
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
encoded << c;
else
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
}
return encoded.str();
}
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
{
if (on_unavailable)
on_unavailable(reason, std::move(detail));
}
void OrcaCloudSignalingChannel::run()
{
try {
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
m_open.store(false);
return;
}
const std::string token = m_cloud->get_access_token();
// OrcaCloud exposes a bare API hostname. WebRTC signaling uses the
// fixed HTTPS/WSS endpoints on port 443; custom schemes, ports, and
// base paths are not supported by this agent.
const std::string host = m_cloud->get_cloud_service_host();
if (token.empty() || host.empty()) {
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
m_open.store(false);
return;
}
const std::string live_token_url =
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
nlohmann::json token_response;
std::string token_body;
unsigned int http_code = 0;
auto request = std::make_shared<Slic3r::Http>(Http::post(live_token_url));
request->set_post_body(std::string("{}"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.tls_verify(true)
.timeout_max(30)
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
http_code = status;
token_body = std::move(body);
})
.on_error([&http_code](std::string, std::string, unsigned status) {
http_code = status;
});
{
std::lock_guard<std::mutex> lock(m_mutex);
m_inflight_requests = request;
}
// If close() raced with request setup, make sure this request is
// cancelled before entering the blocking call.
if (m_stop.load())
request->cancel();
try {
request->perform_sync();
} catch (...) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
throw;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
}
try {
token_response = nlohmann::json::parse(token_body);
} catch (const std::exception&) {
}
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
m_open.store(false);
return;
}
std::vector<CameraIceServer> ice_servers;
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
for (const auto& entry : token_response["ice_servers"]) {
if (entry.is_string()) {
ice_servers.push_back({entry.get<std::string>(), {}, {}});
} else if (entry.is_object()) {
// RTCIceServer.urls is "string | string[]" (Cloudflare
// Realtime returns an array). Emit one CameraIceServer per
// URL, sharing the credentials.
const std::string username = entry.value("username", std::string{});
const std::string credential = entry.value("credential", std::string{});
const auto add_url = [&](const nlohmann::json& url) {
if (url.is_string() && !url.get<std::string>().empty())
ice_servers.push_back({url.get<std::string>(), username, credential});
};
const auto urls = entry.find("urls");
if (urls != entry.end()) {
if (urls->is_array()) {
for (const auto& url : *urls)
add_url(url);
} else {
add_url(*urls);
}
}
}
}
}
auto conn = std::make_shared<Connection>();
conn->ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn->ssl_context.native_handle());
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn = conn;
}
auto& websocket = conn->websocket;
boost::asio::ip::tcp::resolver resolver(conn->io_context);
const auto endpoints = resolver.resolve(host, "443");
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
throw std::runtime_error("Unable to configure TLS server name");
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
websocket.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host));
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
"/camera/live?token=" +
encode_path_component(token_response["token"].get<std::string>());
websocket.handshake(host, ws_target);
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
<< " ice servers)";
if (on_ready)
on_ready(std::move(ice_servers));
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
// Async read loop, driven by the connection's own io_context. run()
// returns once close() has shut the socket down, giving a bounded,
// deadlock-free teardown from any thread.
do_read(conn);
conn->io_context.run();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, e.what());
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn.reset();
}
m_open.store(false);
}
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
{
auto buffer = std::make_shared<boost::beast::flat_buffer>();
conn->websocket.async_read(
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
if (ec) {
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
return; // do not re-arm; io_context.run() unwinds
}
const std::string raw = boost::beast::buffers_to_string(buffer->data());
// A malformed or unexpectedly-shaped message must not tear down the
// session: parse/dispatch is guarded.
try {
dispatch_message(nlohmann::json::parse(raw), raw);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
<< " raw=" << raw.substr(0, 256);
}
if (!m_stop.load())
do_read(conn);
});
}
// Returns the string at key, or "" if absent or not a string (JSON null included).
static std::string json_string(const nlohmann::json& object, const char* key)
{
const auto it = object.find(key);
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
}
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
{
const std::string type = json_string(message, "type");
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
if (type == "webrtc.answer") {
const std::string sdp = json_string(message, "sdp");
if (on_answer && !sdp.empty())
on_answer(sdp);
} else if (type == "webrtc.ice" && message.contains("candidate")) {
// The peer may send "candidate" as a flat string or as a nested
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
const nlohmann::json& candidate = message["candidate"];
std::string sdp_candidate;
std::string mid = json_string(message, "sdpMid");
if (candidate.is_string()) {
sdp_candidate = candidate.get<std::string>();
} else if (candidate.is_object()) {
sdp_candidate = json_string(candidate, "candidate");
std::string nested_mid = json_string(candidate, "sdpMid");
if (!nested_mid.empty())
mid = std::move(nested_mid);
}
if (on_ice && !sdp_candidate.empty())
on_ice(sdp_candidate, mid);
} else if (type == "webrtc.unavailable") {
const std::string reason = json_string(message, "reason");
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
: reason == "disabled" ? CameraUnavailableReason::Disabled
: CameraUnavailableReason::Error,
reason.empty() ? "error" : reason);
}
}
void OrcaCloudSignalingChannel::send_json(const std::string& message)
{
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
}
if (!conn || m_stop.load())
return;
// Serialize the write onto the io_context thread (same thread that runs
// async_read), so reads and writes never touch the stream concurrently.
auto payload = std::make_shared<std::string>(message);
boost::asio::post(conn->io_context, [this, conn, payload] {
if (m_stop.load())
return;
boost::system::error_code ec;
conn->websocket.write(boost::asio::buffer(*payload), ec);
if (ec && !m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
});
}
} // namespace Slic3r
@@ -1,66 +0,0 @@
#pragma once
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "Http.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
namespace Slic3r {
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
public:
OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id);
~OrcaCloudSignalingChannel() override;
void open() override;
void close() override;
void send_offer(std::string sdp) override;
void send_ice(std::string candidate, std::string mid) override;
private:
using WebSocket = boost::beast::websocket::stream<
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
// The io_context and ssl_context must outlive the websocket stream that
// references them. Bundling them here with the stream declared last makes
// the destruction order correct (stream first, then contexts), and lets a
// single shared_ptr own the whole set.
struct Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
WebSocket websocket{io_context, ssl_context};
};
void run();
void do_read(std::shared_ptr<Connection> conn);
void dispatch_message(const nlohmann::json& message, const std::string& raw);
void send_json(const std::string& message);
void unavailable(CameraUnavailableReason reason, std::string detail);
static std::string encode_path_component(const std::string& value);
OrcaCloudServiceAgent* m_cloud;
Http::Ptr m_inflight_requests{nullptr};
std::string m_dev_id;
std::atomic<bool> m_stop{false};
std::atomic<bool> m_open{false};
std::thread m_thread;
mutable std::mutex m_mutex;
std::shared_ptr<Connection> m_conn;
};
} // namespace Slic3r
-805
View File
@@ -1,805 +0,0 @@
#include "OrcaMqttConnection.hpp"
#include "Http.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 <openssl/ssl.h>
#include <algorithm>
#include <chrono>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <utility>
namespace Slic3r {
struct OrcaMqttConnection::Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context;
boost::asio::ip::tcp::resolver resolver;
boost::asio::steady_timer keepalive_timer;
boost::beast::flat_buffer read_buffer;
std::deque<std::shared_ptr<std::vector<uint8_t>>> outbound_packets;
boost::system::error_code terminal_error;
std::atomic_bool async_session_started{false};
bool write_in_progress{false};
// Exactly one of these is engaged once ws_handshake() has run: wss for
// wss:// endpoints, ws for plaintext ws://.
std::optional<TlsWebSocket> wss;
std::optional<PlainWebSocket> ws;
Connection()
: ssl_context(boost::asio::ssl::context::tls_client)
, resolver(io_context)
, keepalive_timer(io_context)
{}
};
namespace {
// Apply / clear a tcp_stream timeout on whichever websocket is engaged.
// Templated on the connection type only because Connection is a private nested
// type: a deduced parameter needs no (inaccessible) name for it.
template<class Conn> void expires_after(Conn& conn, std::chrono::seconds timeout) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout);
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout);
}
template<class Conn> void expires_never(Conn& conn) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never();
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never();
}
} // namespace
OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stop();
{
std::lock_guard<std::mutex> lock(mutex);
current_config = config;
this->on_message = std::move(on_message);
this->on_state = std::move(on_state);
initial_result = false;
initial_completed = false;
connected = false;
m_last_connack_rc.store(-1);
}
stopping.store(false);
worker = std::thread(&OrcaMqttConnection::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;
}
return initial_result;
}
void OrcaMqttConnection::stop() {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stopping.store(true);
state_cv.notify_all();
{
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection) {
if (active_connection->async_session_started.load()) {
// The worker owns the live WebSocket. Stop dispatching its
// asynchronous operations; the worker closes the socket after
// leaving the event loop.
active_connection->io_context.stop();
} else {
// Setup still uses synchronous operations on the worker. Wake a
// pending resolve/connect/CONNACK read without competing with a
// live asynchronous session.
auto shutdown_socket = [](auto& websocket) {
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
boost::system::error_code socket_error;
if (socket.cancel(socket_error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error))
return;
if (socket.close(socket_error))
return;
};
if (active_connection->wss)
shutdown_socket(*active_connection->wss);
else if (active_connection->ws)
shutdown_socket(*active_connection->ws);
active_connection->resolver.cancel();
}
}
}
if (worker.joinable())
worker.join();
{
std::lock_guard<std::mutex> lock(mutex);
connected = false;
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
if (!initial_completed) {
initial_completed = true;
initial_result = false;
}
}
initial_cv.notify_all();
}
bool OrcaMqttConnection::is_running() const {
return worker.joinable() && !stopping.load();
}
void OrcaMqttConnection::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
boost::asio::post(conn->io_context, [this, conn] {
if (!stopping.load() && connected.load())
send_pending_subscriptions(conn);
});
}
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
if (dev_id.empty()) {
return false;
}
const std::string topic = report_topic(dev_id);
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
return false;
}
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
return true;
}
subscriptions.insert(topic);
pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic);
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit SUBSCRIBE now (no reconnect)
return true;
}
bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
const std::string topic = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
subscriptions.erase(topic);
acknowledged_subscriptions.erase(topic);
pending_subscriptions.erase(topic);
pending_unsubscriptions.insert(topic);
for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) {
if (it->second == topic)
it = pending_subscribe_packets.erase(it);
else
++it;
}
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (it->first == dev_id)
it = pending_requests.erase(it);
else
++it;
}
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit UNSUBSCRIBE now (no reconnect)
return true;
}
void OrcaMqttConnection::clear_subscriptions() {
std::lock_guard<std::mutex> lock(mutex);
subscriptions.clear();
pending_subscriptions.clear();
pending_unsubscriptions.clear();
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
}
bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
std::string rest;
std::string default_port;
if (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; }
else if (url.rfind("ws://", 0) == 0) { rest = url.substr(5); default_port = "80"; }
else return false;
const auto slash = rest.find('/');
const std::string authority = rest.substr(0, slash);
endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash);
// host[:port] — leave an unbracketed IPv6 literal alone
const auto colon = authority.rfind(':');
if (colon != std::string::npos && authority.find(']') == std::string::npos) {
endpoint.host = authority.substr(0, colon);
endpoint.port = authority.substr(colon + 1);
} else {
endpoint.host = authority;
endpoint.port = default_port;
}
return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
}
void OrcaMqttConnection::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 OrcaMqttConnection::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> OrcaMqttConnection::make_connect_packet(
const std::string& client_id, const std::string& username,
const std::string& password, int keepalive_seconds) {
std::vector<uint8_t> packet{0x10};
append_string(packet, "MQTT");
packet.push_back(4); // protocol level 3.1.1
uint8_t flags = 0x02; // clean session
if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; }
packet.push_back(flags);
packet.push_back(static_cast<uint8_t>(keepalive_seconds >> 8));
packet.push_back(static_cast<uint8_t>(keepalive_seconds & 0xff));
append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id);
if (!username.empty()) {
append_string(packet, username);
if (!password.empty()) append_string(packet, password);
}
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; }
std::vector<uint8_t> OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) {
std::vector<uint8_t> packet{0x30}; // PUBLISH, QoS 0, no retain
append_string(packet, topic); // no packet id at QoS 0
packet.insert(packet.end(), payload.begin(), payload.end());
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) {
std::vector<uint8_t> packet{0x82};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
packet.push_back(qos);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) {
std::vector<uint8_t> packet{0xA2};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; }
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
return;
}
if (conn.wss) {
conn.wss->binary(true);
conn.wss->write(boost::asio::buffer(packet));
} else if (conn.ws) {
conn.ws->binary(true);
conn.ws->write(boost::asio::buffer(packet));
}
}
void OrcaMqttConnection::close_connection(Connection& conn) {
boost::system::error_code error;
if (conn.wss) {
auto& socket = boost::beast::get_lowest_layer(*conn.wss).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
} else if (conn.ws) {
auto& socket = boost::beast::get_lowest_layer(*conn.ws).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
}
}
void OrcaMqttConnection::enqueue_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
conn->outbound_packets.emplace_back(std::make_shared<std::vector<uint8_t>>(std::move(packet)));
start_async_write(conn);
}
void OrcaMqttConnection::start_async_write(const std::shared_ptr<Connection>& conn) {
if (!conn || conn->write_in_progress || conn->outbound_packets.empty() || stopping.load())
return;
conn->write_in_progress = true;
const auto packet = conn->outbound_packets.front();
auto on_write = [this, conn](const boost::system::error_code& error, std::size_t) {
conn->write_in_progress = false;
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
conn->outbound_packets.pop_front();
start_async_write(conn);
};
if (conn->wss) {
conn->wss->binary(true);
conn->wss->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else if (conn->ws) {
conn->ws->binary(true);
conn->ws->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else {
conn->write_in_progress = false;
conn->outbound_packets.pop_front();
}
}
void OrcaMqttConnection::start_async_read(const std::shared_ptr<Connection>& conn) {
if (!conn || stopping.load())
return;
auto on_read = [this, conn](const boost::system::error_code& error, std::size_t) {
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
const std::string packet = boost::beast::buffers_to_string(conn->read_buffer.data());
conn->read_buffer.consume(conn->read_buffer.size());
handle_packet(packet);
start_async_read(conn);
};
if (conn->wss)
conn->wss->async_read(conn->read_buffer, std::move(on_read));
else if (conn->ws)
conn->ws->async_read(conn->read_buffer, std::move(on_read));
}
void OrcaMqttConnection::schedule_keepalive(const std::shared_ptr<Connection>& conn) {
const int keepalive = current_config.keepalive_seconds;
if (!conn || keepalive <= 0 || stopping.load())
return;
conn->keepalive_timer.expires_after(std::chrono::seconds(std::max(1, keepalive / 2)));
conn->keepalive_timer.async_wait([this, conn](const boost::system::error_code& error) {
if (error || stopping.load())
return;
enqueue_packet(conn, make_ping_packet());
schedule_keepalive(conn);
});
}
void OrcaMqttConnection::post_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
boost::asio::post(conn->io_context, [this, conn, packet = std::move(packet)]() mutable {
if (!stopping.load())
enqueue_packet(conn, std::move(packet));
});
}
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
boost::system::error_code& ec) {
if (conn.wss)
return conn.wss->read(buffer, ec);
if (conn.ws)
return conn.ws->read(buffer, ec);
ec = boost::asio::error::not_connected;
return 0;
}
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
std::string token;
if (config.bearer_provider) {
token = config.bearer_provider();
}
auto decorator = [token](boost::beast::websocket::request_type& request) {
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
if (!token.empty())
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;
if (config.use_tls) {
// stop() inspects the engaged optional under connection_mutex; publish it
// under the same lock, then release before the blocking connect.
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.wss.emplace(conn.io_context, conn.ssl_context);
}
auto& websocket = *conn.wss;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
// 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");
if (!config.ca_file.empty())
conn.ssl_context.load_verify_file(config.ca_file);
else
conn.ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
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);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
} else {
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.ws.emplace(conn.io_context);
}
auto& websocket = *conn.ws;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
}
if (handshake_error) {
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
}
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
}
}
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
if (dev_id.empty()) {
return false;
}
if (!connected.load()) {
return false;
}
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
if (!conn) {
return false;
}
const std::string report = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
return true;
}
}
post_packet(conn, make_publish_packet(request_topic(dev_id), payload));
return true;
}
void OrcaMqttConnection::connect_and_read() {
auto connection = std::make_shared<Connection>();
{
std::lock_guard<std::mutex> lock(connection_mutex);
active_connection = connection;
if (stopping.load()) {
active_connection.reset();
return;
}
}
auto clear_connection = [this, connection] {
close_connection(*connection);
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection == connection)
active_connection.reset();
};
try {
Endpoint endpoint;
if (!parse_endpoint(current_config.url, endpoint)) {
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
}
ws_handshake(*connection, current_config, endpoint);
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
// CONNECT username/password fields are omitted entirely (the cloud form).
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
ws_write(*connection, make_connect_packet(current_config.client_id,
use_bearer ? std::string() : current_config.username,
use_bearer ? std::string() : current_config.password,
current_config.keepalive_seconds));
boost::beast::flat_buffer buffer;
expires_after(*connection, std::chrono::seconds(10));
boost::system::error_code connack_error;
ws_read(*connection, buffer, connack_error);
if (connack_error)
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
// Beast leaves this expiry armed after the synchronous CONNACK read above;
// disable it before starting the long-lived async WebSocket session.
expires_never(*connection);
const std::string connack = boost::beast::buffers_to_string(buffer.data());
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
? static_cast<int>(static_cast<uint8_t>(connack[3]))
: -1;
m_last_connack_rc.store(rc);
if (rc != 0) {
if (rc == 4 || rc == 5) {
// Bad credentials / not authorized — retrying cannot help. Make run()'s
// loop exit and unblock any waiting start().
stopping.store(true);
{
std::lock_guard<std::mutex> lock(mutex);
initial_completed = true;
initial_result = false;
}
initial_cv.notify_all();
}
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
}
// The subscription acknowledgement belongs to this MQTT session. Clear
// the previous session's state before notifying the owner, because the
// reconnect callback immediately queues the printer's initial requests.
{
std::lock_guard<std::mutex> lock(mutex);
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
}
connection->async_session_started.store(true);
notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
send_current_subscriptions(connection);
start_async_read(connection);
schedule_keepalive(connection);
const std::size_t handlers_run = connection->io_context.run();
if (handlers_run == 0 && !stopping.load())
throw std::runtime_error("Orca MQTT event loop stopped unexpectedly");
if (connection->terminal_error && !stopping.load())
throw boost::system::system_error(connection->terminal_error, "read Orca MQTT message");
clear_connection();
if (!stopping.load())
notify_state(false);
} catch (...) {
clear_connection();
throw;
}
}
void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> topics;
{
std::lock_guard<std::mutex> lock(mutex);
topics.assign(subscriptions.begin(), subscriptions.end());
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
for (const std::string& topic : topics)
pending_subscriptions.erase(topic);
}
for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
}
void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> subscribe_topics;
std::vector<std::string> unsubscribe_topics;
{
std::lock_guard<std::mutex> lock(mutex);
subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end());
unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
pending_subscriptions.clear();
pending_unsubscriptions.clear();
}
for (const std::string& topic : subscribe_topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
for (const std::string& topic : unsubscribe_topics) {
const uint16_t packet_id = next_packet_id++;
enqueue_packet(conn, make_unsubscribe_packet(packet_id, topic));
}
}
void OrcaMqttConnection::handle_packet(const std::string& packet) {
if (packet.size() < 2) {
return;
}
const uint8_t header = static_cast<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
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]));
}
// Each production SUBSCRIBE packet currently contains one topic.
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
// means the subscription was rejected.
const uint8_t result = static_cast<uint8_t>(packet[4]);
std::string topic;
std::deque<std::pair<std::string, std::string>> requests;
{
std::lock_guard<std::mutex> lock(mutex);
auto pending = pending_subscribe_packets.find(packet_id);
if (pending != pending_subscribe_packets.end()) {
topic = pending->second;
pending_subscribe_packets.erase(pending);
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (report_topic(it->first) == topic) {
requests.push_back(std::move(*it));
it = pending_requests.erase(it);
} else {
++it;
}
}
if (result == 0 || result == 1) {
acknowledged_subscriptions.insert(topic);
}
}
}
if (topic.empty()) {
} else if (result == 0 || result == 1) {
for (const auto& request : requests) {
if (!send_request(request.first, request.second)) {
}
}
} else {
}
}
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) {
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) {
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) {
return;
}
const std::string topic(packet.data() + index, topic_length);
index += topic_length;
if (((header >> 1) & 0x03) != 0) {
if (index + 2 > remaining_end) {
return;
}
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
}
const size_t payload_size = remaining_end - index;
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
std::string dev_id;
if (topic.rfind("device/", 0) == 0) {
const size_t id_start = 7;
const size_t id_end = topic.rfind('/');
if (id_end != std::string::npos && id_end > id_start)
dev_id = topic.substr(id_start, id_end - id_start);
}
if (dev_id.empty()) {
} else if (on_message) {
on_message(dev_id, packet.substr(index, remaining_end - index));
} else {
}
}
void OrcaMqttConnection::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;
}
if (initial)
initial_cv.notify_all();
else if (callback)
callback(is_now_connected, false);
}
void OrcaMqttConnection::run() {
while (!stopping.load()) {
const int retry_seconds = reconnect_delay_seconds.load();
const uint64_t attempt = ++m_attempt_number;
try {
connect_and_read();
} catch (const std::exception& error) {
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);
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
}
}
} // namespace Slic3r
-155
View File
@@ -1,155 +0,0 @@
#ifndef slic3r_OrcaMqttConnection_hpp_
#define slic3r_OrcaMqttConnection_hpp_
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <cstddef>
#include <cstdint>
namespace Slic3r {
// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the
// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH
// commands to device/<id>/request and SUBSCRIBE device/<id>/report; Config is the
// only per-transport difference.
class OrcaMqttConnection
{
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)>;
struct Endpoint { std::string host; std::string port; std::string target; };
struct Config {
std::string url;
bool use_tls = false;
std::string ca_file;
TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted
std::string username;
std::string password;
std::string client_id = "OrcaSlicer";
int keepalive_seconds = 60;
};
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
// Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the
// username/password connect flags and payload fields are added only when
// username is non-empty (the cloud form authenticates via a bearer on the
// WebSocket upgrade and omits CONNECT credentials). Public for unit tests.
static std::vector<uint8_t> make_connect_packet(const std::string& client_id,
const std::string& username,
const std::string& password,
int keepalive_seconds);
// Topic-string helpers for the per-device request/report channels and the
// MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public
// for unit tests. make_publish_packet emits QoS 0 (no packet identifier).
static std::string request_topic(const std::string& dev_id); // "device/<id>/request"
static std::string report_topic(const std::string& dev_id); // "device/<id>/report"
static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload);
static std::vector<uint8_t> make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos);
static std::vector<uint8_t> make_unsubscribe_packet(uint16_t packet_id, const std::string& topic);
~OrcaMqttConnection();
bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
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::string& dev_id);
bool unsubscribe(const std::string& dev_id);
// Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt.
int last_connack_rc() const { return m_last_connack_rc.load(); }
void clear_subscriptions();
bool send_request(const std::string& dev_id, const std::string& payload);
private:
// The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket;
// Connection holds whichever one is engaged and the ws_* helpers below
// dispatch on it.
using TlsWebSocket = boost::beast::websocket::stream<
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
using PlainWebSocket = boost::beast::websocket::stream<boost::beast::tcp_stream>;
struct Connection;
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_ping_packet();
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
// These synchronous operations are called only by the MQTT worker during
// connection setup. Once the MQTT session is established, all socket I/O is
// asynchronous and owned by that worker's io_context.
void ws_write(Connection& conn, const std::vector<uint8_t>& packet);
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
void close_connection(Connection& conn);
void enqueue_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
void start_async_write(const std::shared_ptr<Connection>& conn);
void start_async_read(const std::shared_ptr<Connection>& conn);
void schedule_keepalive(const std::shared_ptr<Connection>& conn);
void post_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
// Ask the MQTT worker to emit subscription changes on its own io_context.
void flush_subscription_change();
void connect_and_read();
void send_current_subscriptions(const std::shared_ptr<Connection>& conn);
void send_pending_subscriptions(const std::shared_ptr<Connection>& conn);
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};
// Serialises the whole of start() and stop() against each other, so the UI
// thread's stop() (disconnect / dtor) cannot race the connect thread's start()
// into a concurrent worker.join(). Recursive because start() calls stop().
std::recursive_mutex lifecycle_mutex;
std::thread worker;
std::mutex mutex;
std::mutex connection_mutex;
std::shared_ptr<Connection> active_connection;
std::condition_variable initial_cv;
std::condition_variable state_cv;
Config current_config;
MessageHandler on_message;
StateHandler on_state;
// Full report-topic strings ("device/<id>/report"), not bare device ids.
std::set<std::string> subscriptions;
std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions;
// Requests for a subscribed device wait until the corresponding SUBACK is
// received. Otherwise an immediate pushall response can be published by
// the broker before this client is actually subscribed to the report topic.
std::set<std::string> acknowledged_subscriptions;
std::map<uint16_t, std::string> pending_subscribe_packets;
std::deque<std::pair<std::string, std::string>> pending_requests;
std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
bool initial_result{false};
bool initial_completed{false};
std::atomic_bool connected{false};
};
} // namespace Slic3r
#endif // slic3r_OrcaMqttConnection_hpp_
File diff suppressed because it is too large Load Diff
+4 -136
View File
@@ -3,29 +3,20 @@
#include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <cstdint>
#include <functional>
#include "bambu_networking.hpp"
#include <string>
#include <mutex>
#include <memory>
#include <thread>
namespace Slic3r {
class OrcaCloudServiceAgent;
/**
* OrcaPrinterAgent - OrcaSonar MQTT printer agent.
* OrcaPrinterAgent - Stub implementation for printer operations.
*
* LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* MQTT connection selected by route_send() differs.
* All printer-related operations are currently stubs that return success.
* Actual printer connectivity requires the BBL SDK or future Orca implementation.
*/
class OrcaPrinterAgent : public IPrinterAgent
{
class OrcaPrinterAgent : public IPrinterAgent {
public:
explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override;
@@ -35,8 +26,6 @@ public:
// ========================================================================
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -80,131 +69,10 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
std::string axis,
double unit,
double input_val,
int speed,
bool is_core_xy,
bool supports_mqtt_axis_control,
int sequence_id,
bool lan_mode) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
}
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// already handles, so parse_json needs no Orca-specific changes. Self-contained
// (its cache is a function-local static) and deletable together with its call site
// once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
// Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by
// connect_printer(), which only starts the worker.
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
// The LAN inbound-message handler for one connection generation: forwards to
// deliver_to_sink only while `generation` is still the live epoch.
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
// The post-connect command sequence, factored behind a seam so a test can
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
virtual void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> subscribe,
std::function<void(const std::string&)> request);
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
static std::string build_pushing_start(const std::string& sequence_id);
static std::string build_pushing_stop(const std::string& sequence_id);
static std::string build_pushall(const std::string& sequence_id);
static std::string build_get_version(const std::string& sequence_id);
static std::string build_get_capabilities(const std::string& sequence_id);
private:
class OrcaSonarDiscovery;
std::string log_dir;
std::string selected_machine;
enum CurrentConn { NONE, CLOUD, LAN };
static const char* connection_type_name(CurrentConn connection);
// The transport for the printer currently selected by the UI. LAN and
// cloud sessions have separate connection objects, so this is selection
// state rather than an inference from whichever socket happens to exist.
CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
std::unique_ptr<OrcaMqttConnection> lan_mqtt_connection;
// Two independent epochs: a cloud (de)selection must not fence the live LAN
// feed, and vice versa. Each transport's connect thread and inbound handler
// compare against their own counter only.
std::atomic<uint64_t> m_lan_generation{0};
std::atomic<uint64_t> m_cloud_generation{0};
// The short-lived threads that run the blocking initial connect for the current
// LAN / cloud session. Joined members (never detached) so they cannot outlive
// *this or the connection they hold a raw pointer to.
std::thread m_lan_connect_thread;
std::thread m_cloud_connect_thread;
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
bool m_lan_use_ssl = false; // guarded by state_mutex
std::string m_lan_ca_file; // guarded by state_mutex
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the shared
// cloud connection. The uniform send path for both send_message* overrides.
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
// Callbacks
OnMsgArrivedFn on_ssdp_msg_fn;