mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 05:27:50 +00:00
feat: connect to cloud printer and monitor
This commit is contained in:
@@ -572,6 +572,19 @@ namespace Slic3r
|
|||||||
<< " cur_selected=" << selected_machine;
|
<< " cur_selected=" << selected_machine;
|
||||||
auto my_machine_list = get_my_machine_list();
|
auto my_machine_list = get_my_machine_list();
|
||||||
auto it = my_machine_list.find(dev_id);
|
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
|
// disconnect last if dev_id difference from previous one
|
||||||
auto last_selected = my_machine_list.find(selected_machine);
|
auto last_selected = my_machine_list.find(selected_machine);
|
||||||
@@ -582,7 +595,9 @@ namespace Slic3r
|
|||||||
m_agent->disconnect_printer();
|
m_agent->disconnect_printer();
|
||||||
}
|
}
|
||||||
else if (last_selected->second->connection_type() == "cloud") {
|
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)
|
// diff dev_id, cloud => set_user_selected_machine(new)
|
||||||
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id;
|
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();
|
it->second->reset();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -662,6 +679,8 @@ namespace Slic3r
|
|||||||
|
|
||||||
selected_machine = dev_id;
|
selected_machine = dev_id;
|
||||||
record_user_last_machine(selected_machine);
|
record_user_last_machine(selected_machine);
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: DeviceManager selection complete selected_machine="
|
||||||
|
<< selected_machine;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,7 +711,9 @@ namespace Slic3r
|
|||||||
dev_list.push_back(it->first);
|
dev_list.push_back(it->first);
|
||||||
BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << 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);
|
dev_list.push_back(it->first);
|
||||||
BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << 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)
|
void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list)
|
||||||
@@ -869,6 +892,12 @@ namespace Slic3r
|
|||||||
|
|
||||||
if (!obj) continue;
|
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())
|
if (!elem["dev_id"].is_null())
|
||||||
obj->set_dev_id(elem["dev_id"].get<std::string>());
|
obj->set_dev_id(elem["dev_id"].get<std::string>());
|
||||||
if (!elem["dev_name"].is_null())
|
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());
|
acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end());
|
||||||
obj->set_access_code(acc_code);
|
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
|
//remove MachineObject from userMachineList
|
||||||
@@ -915,6 +950,9 @@ namespace Slic3r
|
|||||||
iterat++;
|
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)
|
catch (std::exception& e)
|
||||||
@@ -931,10 +969,14 @@ namespace Slic3r
|
|||||||
unsigned int http_code;
|
unsigned int http_code;
|
||||||
std::string body;
|
std::string body;
|
||||||
int result = m_agent->get_user_print_info(&http_code, &body, provider);
|
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)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
// parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map.
|
// 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.
|
// 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); });
|
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4971,11 +4971,12 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*
|
|||||||
return false;
|
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.
|
std::string provider = preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent");
|
||||||
//
|
if (provider.empty())
|
||||||
return BBL_CLOUD_PROVIDER;
|
provider = ORCA_CLOUD_PROVIDER;
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ public:
|
|||||||
bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||||
void get_login_info(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);
|
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_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);
|
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
|
|
||||||
#include "DeviceCore/DevManager.h"
|
#include "DeviceCore/DevManager.h"
|
||||||
|
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
namespace GUI {
|
namespace GUI {
|
||||||
|
|
||||||
@@ -259,6 +261,7 @@ void MonitorPanel::msw_rescale()
|
|||||||
|
|
||||||
void MonitorPanel::select_machine(std::string machine_sn)
|
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);
|
wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED);
|
||||||
event->SetString(machine_sn);
|
event->SetString(machine_sn);
|
||||||
wxQueueEvent(this, event);
|
wxQueueEvent(this, event);
|
||||||
@@ -276,13 +279,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event)
|
|||||||
void MonitorPanel::on_select_printer(wxCommandEvent& event)
|
void MonitorPanel::on_select_printer(wxCommandEvent& event)
|
||||||
{
|
{
|
||||||
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
|
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) return;
|
||||||
|
|
||||||
if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) {
|
if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) {
|
||||||
m_hms_panel->clear_hms_tag();
|
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;
|
return;
|
||||||
|
|
||||||
set_default();
|
set_default();
|
||||||
|
|||||||
@@ -925,8 +925,14 @@ std::string NetworkAgent::get_user_selected_machine()
|
|||||||
|
|
||||||
int NetworkAgent::set_user_selected_machine(std::string dev_id)
|
int NetworkAgent::set_user_selected_machine(std::string dev_id)
|
||||||
{
|
{
|
||||||
if (m_printer_agent)
|
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id
|
||||||
return m_printer_agent->set_user_selected_machine(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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,15 +952,27 @@ int NetworkAgent::stop_subscribe(std::string module)
|
|||||||
|
|
||||||
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
|
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
|
||||||
{
|
{
|
||||||
if (m_printer_agent)
|
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
|
||||||
return m_printer_agent->add_subscribe(std::move(dev_list));
|
<< " 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
|
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
|
||||||
{
|
{
|
||||||
if (m_printer_agent)
|
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
|
||||||
return m_printer_agent->del_subscribe(std::move(dev_list));
|
<< " 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/beast/core/detail/base64.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.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/uuid/uuid.hpp>
|
#include <boost/uuid/uuid.hpp>
|
||||||
@@ -20,14 +22,18 @@
|
|||||||
#include <openssl/hmac.h>
|
#include <openssl/hmac.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
#include <openssl/sha.h>
|
#include <openssl/sha.h>
|
||||||
|
#include <openssl/ssl.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -59,6 +65,522 @@ using json = nlohmann::json;
|
|||||||
|
|
||||||
namespace Slic3r {
|
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 {
|
namespace {
|
||||||
constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com";
|
constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com";
|
||||||
constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.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_MINE = "/api/v1/plugins/mine";
|
||||||
constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins";
|
constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins";
|
||||||
constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download";
|
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";
|
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)
|
, api_base_url(ORCA_DEFAULT_API_URL)
|
||||||
, auth_base_url(ORCA_DEFAULT_AUTH_URL)
|
, auth_base_url(ORCA_DEFAULT_AUTH_URL)
|
||||||
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
|
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
|
||||||
|
, mqtt_connection(std::make_unique<OrcaCloudMqttConnection>())
|
||||||
{
|
{
|
||||||
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
|
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
|
||||||
pkce_bundle.loopback_port = choose_loopback_port();
|
pkce_bundle.loopback_port = choose_loopback_port();
|
||||||
@@ -500,6 +1024,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
|
|||||||
|
|
||||||
OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
|
OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
|
||||||
{
|
{
|
||||||
|
if (mqtt_connection)
|
||||||
|
mqtt_connection->stop();
|
||||||
if (refresh_thread.joinable()) {
|
if (refresh_thread.joinable()) {
|
||||||
refresh_thread.join();
|
refresh_thread.join();
|
||||||
}
|
}
|
||||||
@@ -938,22 +1464,102 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu
|
|||||||
|
|
||||||
int OrcaCloudServiceAgent::connect_server()
|
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;
|
std::string response;
|
||||||
unsigned int http_code = 0;
|
unsigned int http_code = 0;
|
||||||
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
|
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
|
||||||
|
|
||||||
bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300);
|
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();
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||||
is_connected = connected;
|
is_connected = connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoke_server_connected_callback(connected ? 0 : -1, http_code);
|
invoke_server_connected_callback(connected ? 0 : -1, http_code);
|
||||||
return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
}
|
||||||
|
|
||||||
|
return (connected || mqtt_alive) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OrcaCloudServiceAgent::is_server_connected()
|
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);
|
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||||
return is_connected;
|
return is_connected;
|
||||||
}
|
}
|
||||||
@@ -974,16 +1580,59 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
|
|||||||
|
|
||||||
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
|
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
|
||||||
{
|
{
|
||||||
(void) dev_list;
|
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
|
||||||
return BAMBU_NETWORK_SUCCESS;
|
<< " 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)
|
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;
|
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)
|
void OrcaCloudServiceAgent::enable_multi_machine(bool enable)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
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()
|
void OrcaCloudServiceAgent::clear_session()
|
||||||
{
|
{
|
||||||
|
if (mqtt_connection) {
|
||||||
|
mqtt_connection->stop();
|
||||||
|
mqtt_connection->clear_subscriptions();
|
||||||
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(session_mutex);
|
std::lock_guard<std::mutex> lock(session_mutex);
|
||||||
session = SessionInfo{};
|
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)
|
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)
|
if (http_code)
|
||||||
*http_code = 200;
|
*http_code = code;
|
||||||
if (http_body)
|
|
||||||
*http_body = "{}";
|
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;
|
return BAMBU_NETWORK_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__
|
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__
|
||||||
|
|
||||||
#include "ICloudServiceAgent.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 <cstdlib>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -9,6 +15,9 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <set>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -21,6 +30,78 @@ namespace Slic3r {
|
|||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class AppConfig;
|
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 BundleMetadata;
|
||||||
struct PluginDescriptor;
|
struct PluginDescriptor;
|
||||||
struct PluginChangelog;
|
struct PluginChangelog;
|
||||||
@@ -207,6 +288,18 @@ public:
|
|||||||
int del_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;
|
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
|
// ICloudServiceAgent Interface Implementation - Settings Synchronization
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -423,6 +516,7 @@ private:
|
|||||||
std::chrono::system_clock::now().time_since_epoch()).count()};
|
std::chrono::system_clock::now().time_since_epoch()).count()};
|
||||||
|
|
||||||
// Member variables - connection state
|
// Member variables - connection state
|
||||||
|
std::unique_ptr<OrcaCloudMqttConnection> mqtt_connection;
|
||||||
bool is_connected{false};
|
bool is_connected{false};
|
||||||
bool enable_track{false};
|
bool enable_track{false};
|
||||||
bool multi_machine_enabled{false};
|
bool multi_machine_enabled{false};
|
||||||
@@ -436,6 +530,7 @@ private:
|
|||||||
AppOnHttpErrorFn on_http_error_fn;
|
AppOnHttpErrorFn on_http_error_fn;
|
||||||
GetCountryCodeFn get_country_code_fn;
|
GetCountryCodeFn get_country_code_fn;
|
||||||
QueueOnMainFn queue_on_main_fn;
|
QueueOnMainFn queue_on_main_fn;
|
||||||
|
OnMessageFn printer_status_callback;
|
||||||
mutable std::mutex callback_mutex;
|
mutable std::mutex callback_mutex;
|
||||||
|
|
||||||
// Thread safety
|
// Thread safety
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "OrcaPrinterAgent.hpp"
|
#include "OrcaPrinterAgent.hpp"
|
||||||
#include "NetworkAgentFactory.hpp"
|
#include "NetworkAgentFactory.hpp"
|
||||||
|
#include "OrcaCloudServiceAgent.hpp"
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
@@ -12,9 +15,36 @@ OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_
|
|||||||
OrcaPrinterAgent::~OrcaPrinterAgent() = default;
|
OrcaPrinterAgent::~OrcaPrinterAgent() = default;
|
||||||
|
|
||||||
void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
|
void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: cloud=" << (cloud ? cloud->get_id() : "<null>");
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex);
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
m_cloud_agent = cloud;
|
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)
|
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;
|
return BAMBU_NETWORK_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,11 +180,68 @@ std::string OrcaPrinterAgent::get_user_selected_machine()
|
|||||||
}
|
}
|
||||||
|
|
||||||
int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||||
|
{
|
||||||
|
std::shared_ptr<ICloudServiceAgent> cloud;
|
||||||
|
std::string previous;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex);
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
selected_machine = dev_id;
|
if (dev_id == selected_machine) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id;
|
||||||
return BAMBU_NETWORK_SUCCESS;
|
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
|
// Agent Information
|
||||||
@@ -197,6 +312,7 @@ int OrcaPrinterAgent::set_on_message_fn(OnMessageFn fn)
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex);
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
on_message_fn = fn;
|
on_message_fn = fn;
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_message_fn: callback=" << (fn ? "set" : "clear");
|
||||||
return BAMBU_NETWORK_SUCCESS;
|
return BAMBU_NETWORK_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class OrcaCloudServiceAgent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OrcaPrinterAgent - Stub implementation for printer operations.
|
* OrcaPrinterAgent - Stub implementation for printer operations.
|
||||||
*
|
*
|
||||||
@@ -81,6 +83,12 @@ private:
|
|||||||
std::string log_dir;
|
std::string log_dir;
|
||||||
std::string selected_machine;
|
std::string selected_machine;
|
||||||
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
|
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
|
// Callbacks
|
||||||
OnMsgArrivedFn on_ssdp_msg_fn;
|
OnMsgArrivedFn on_ssdp_msg_fn;
|
||||||
|
|||||||
Reference in New Issue
Block a user