mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
fix: orcaprinteragent refactor
This commit is contained in:
@@ -725,6 +725,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/IPrinterAgent.hpp
|
||||
Utils/OrcaCloudServiceAgent.cpp
|
||||
Utils/OrcaCloudServiceAgent.hpp
|
||||
Utils/OrcaMqttConnection.cpp
|
||||
Utils/OrcaMqttConnection.hpp
|
||||
Utils/OrcaPrinterAgent.cpp
|
||||
Utils/OrcaPrinterAgent.hpp
|
||||
Utils/QidiPrinterAgent.cpp
|
||||
|
||||
@@ -35,7 +35,9 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons
|
||||
sizer_connect = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_textCtrl_code = new TextInput(this, wxEmptyString);
|
||||
m_textCtrl_code->GetTextCtrl()->SetMaxLength(10);
|
||||
// OrcaSonar uses a 12-character base32 access code. Keep this field long
|
||||
// enough for it while retaining the existing validation for LAN codes.
|
||||
m_textCtrl_code->GetTextCtrl()->SetMaxLength(12);
|
||||
m_textCtrl_code->SetFont(Label::Body_14);
|
||||
m_textCtrl_code->SetCornerRadius(FromDIP(5));
|
||||
m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40)));
|
||||
|
||||
@@ -3996,6 +3996,7 @@ void GUI_App::switch_printer_agent()
|
||||
|
||||
std::string log_dir = data_dir();
|
||||
std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << agent_info.id;
|
||||
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
|
||||
|
||||
// Create new printer agent via registry
|
||||
|
||||
@@ -246,7 +246,7 @@ public:
|
||||
virtual std::string get_user_selected_machine() = 0;
|
||||
|
||||
/**
|
||||
* Update the selected machine preference.
|
||||
* Update the selected cloud machine preference.
|
||||
*/
|
||||
virtual int set_user_selected_machine(std::string dev_id) = 0;
|
||||
|
||||
|
||||
@@ -65,522 +65,6 @@ using json = nlohmann::json;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct OrcaCloudMqttConnection::Connection {
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::ssl::context ssl_context;
|
||||
WebSocket websocket;
|
||||
boost::asio::ip::tcp::resolver resolver;
|
||||
|
||||
Connection()
|
||||
: ssl_context(boost::asio::ssl::context::tls_client)
|
||||
, websocket(io_context, ssl_context)
|
||||
, resolver(io_context)
|
||||
{}
|
||||
};
|
||||
|
||||
OrcaCloudMqttConnection::~OrcaCloudMqttConnection() { stop(); }
|
||||
|
||||
bool OrcaCloudMqttConnection::start(const std::string& endpoint, TokenProvider token_provider, MessageHandler message_handler, StateHandler state_handler) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start endpoint=" << endpoint
|
||||
<< " token_callback=" << (token_provider ? "set" : "null")
|
||||
<< " message_callback=" << (message_handler ? "set" : "null")
|
||||
<< " state_callback=" << (state_handler ? "set" : "null");
|
||||
stop();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
endpoint_url = endpoint;
|
||||
get_token = std::move(token_provider);
|
||||
on_message = std::move(message_handler);
|
||||
on_state = std::move(state_handler);
|
||||
initial_result = false;
|
||||
initial_completed = false;
|
||||
connected = false;
|
||||
}
|
||||
stopping.store(false);
|
||||
worker = std::thread(&OrcaCloudMqttConnection::run, this);
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds; worker will retry";
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result
|
||||
<< " initial_completed=" << initial_completed;
|
||||
return initial_result;
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::stop() {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested";
|
||||
stopping.store(true);
|
||||
state_cv.notify_all();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection) {
|
||||
auto& socket = boost::beast::get_lowest_layer(active_connection->websocket).socket();
|
||||
boost::system::error_code socket_error;
|
||||
socket.cancel(socket_error);
|
||||
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error);
|
||||
socket.close(socket_error);
|
||||
active_connection->resolver.cancel();
|
||||
}
|
||||
}
|
||||
if (worker.joinable())
|
||||
worker.join();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connected = false;
|
||||
if (!initial_completed) {
|
||||
initial_completed = true;
|
||||
initial_result = false;
|
||||
}
|
||||
}
|
||||
initial_cv.notify_all();
|
||||
}
|
||||
|
||||
bool OrcaCloudMqttConnection::is_running() const {
|
||||
return worker.joinable() && !stopping.load();
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::flush_subscription_change() {
|
||||
std::shared_ptr<Connection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn = active_connection;
|
||||
}
|
||||
bool connacked;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connacked = connected;
|
||||
}
|
||||
if (!conn || !connacked)
|
||||
return; // no live MQTT session yet — the worker sends the set on CONNACK
|
||||
|
||||
// beast permits a concurrent writer while the worker is blocked in
|
||||
// websocket.read(); every write is serialised by write_mutex inside send().
|
||||
try {
|
||||
send_pending_subscriptions(conn->websocket);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: direct subscription write failed (" << e.what()
|
||||
<< "); worker will resend the full set on reconnect";
|
||||
}
|
||||
}
|
||||
|
||||
bool OrcaCloudMqttConnection::subscribe(const std::vector<std::string>& device_ids) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe requested count=" << device_ids.size()
|
||||
<< " connected=" << connected.load();
|
||||
for (const std::string& device_id : device_ids) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe requested dev_id=" << device_id;
|
||||
if (device_id.empty() || report_topic(device_id).size() > 96) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected invalid dev_id=" << device_id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const std::string& device_id : device_ids) {
|
||||
subscriptions.insert(device_id);
|
||||
pending_subscriptions.insert(device_id);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued total_subscriptions=" << subscriptions.size()
|
||||
<< " pending_subscriptions=" << pending_subscriptions.size();
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrcaCloudMqttConnection::unsubscribe(const std::vector<std::string>& device_ids) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe requested count=" << device_ids.size()
|
||||
<< " connected=" << connected.load();
|
||||
for (const std::string& device_id : device_ids) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe requested dev_id=" << device_id;
|
||||
subscriptions.erase(device_id);
|
||||
pending_subscriptions.erase(device_id);
|
||||
pending_unsubscriptions.insert(device_id);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued total_subscriptions=" << subscriptions.size()
|
||||
<< " pending_unsubscriptions=" << pending_unsubscriptions.size();
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::clear_subscriptions() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT clear subscriptions count=" << subscriptions.size();
|
||||
subscriptions.clear();
|
||||
pending_subscriptions.clear();
|
||||
pending_unsubscriptions.clear();
|
||||
}
|
||||
|
||||
bool OrcaCloudMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
|
||||
constexpr const char* scheme = "wss://";
|
||||
constexpr size_t scheme_length = 6;
|
||||
if (url.compare(0, scheme_length, scheme) != 0)
|
||||
return false;
|
||||
|
||||
const size_t authority_start = scheme_length;
|
||||
const size_t path_start = url.find('/', authority_start);
|
||||
const std::string authority = url.substr(authority_start, path_start - authority_start);
|
||||
if (authority.empty())
|
||||
return false;
|
||||
|
||||
const size_t port_start = authority.rfind(':');
|
||||
if (port_start != std::string::npos && authority.find(']') == std::string::npos) {
|
||||
endpoint.host = authority.substr(0, port_start);
|
||||
endpoint.port = authority.substr(port_start + 1);
|
||||
} else {
|
||||
endpoint.host = authority;
|
||||
endpoint.port = "443";
|
||||
}
|
||||
endpoint.target = path_start == std::string::npos ? "/" : url.substr(path_start);
|
||||
return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::append_string(std::vector<uint8_t>& packet, const std::string& value) {
|
||||
if (value.size() > 0xffff)
|
||||
throw std::runtime_error("MQTT string is too long");
|
||||
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
|
||||
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
|
||||
packet.insert(packet.end(), value.begin(), value.end());
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
|
||||
std::vector<uint8_t> encoded;
|
||||
do {
|
||||
uint8_t byte = static_cast<uint8_t>(length % 128);
|
||||
length /= 128;
|
||||
if (length != 0)
|
||||
byte |= 0x80;
|
||||
encoded.push_back(byte);
|
||||
} while (length != 0);
|
||||
packet.insert(packet.begin() + 1, encoded.begin(), encoded.end());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaCloudMqttConnection::make_connect_packet() {
|
||||
std::vector<uint8_t> packet{0x10};
|
||||
append_string(packet, "MQTT");
|
||||
packet.insert(packet.end(), {4, 2, 0, 60}); // level 4, clean session, 60 s keepalive
|
||||
append_string(packet, "OrcaSlicer");
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::string OrcaCloudMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
|
||||
|
||||
std::vector<uint8_t> OrcaCloudMqttConnection::make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector<std::string>& device_ids) {
|
||||
std::vector<uint8_t> packet{type};
|
||||
packet.push_back(static_cast<uint8_t>(packet_id >> 8));
|
||||
packet.push_back(static_cast<uint8_t>(packet_id & 0xff));
|
||||
for (const std::string& device_id : device_ids) {
|
||||
append_string(packet, report_topic(device_id));
|
||||
if (type == 0x82) // SUBSCRIBE, QoS 0 is sufficient for printer reports.
|
||||
packet.push_back(0);
|
||||
}
|
||||
prepend_remaining_length(packet, packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> OrcaCloudMqttConnection::make_ping_packet() { return {0xc0, 0}; }
|
||||
|
||||
void OrcaCloudMqttConnection::send(WebSocket& websocket, const std::vector<uint8_t>& packet) {
|
||||
if (packet.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: attempted to send empty MQTT packet";
|
||||
return;
|
||||
}
|
||||
// Writes come from the worker thread AND, for dynamic (un)subscribes, the
|
||||
// caller thread. Serialise them; the worker's concurrent read is fine (beast
|
||||
// allows one reader + one writer).
|
||||
std::lock_guard<std::mutex> lock(write_mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex
|
||||
<< static_cast<unsigned int>(packet[0] >> 4) << std::dec
|
||||
<< " bytes=" << packet.size();
|
||||
websocket.binary(true);
|
||||
websocket.write(boost::asio::buffer(packet));
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::connect_and_read() {
|
||||
auto connection = std::make_shared<Connection>();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
active_connection = connection;
|
||||
if (stopping.load())
|
||||
return;
|
||||
}
|
||||
|
||||
Endpoint endpoint;
|
||||
if (!parse_endpoint(endpoint_url, endpoint)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << endpoint_url;
|
||||
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connecting host=" << endpoint.host
|
||||
<< " port=" << endpoint.port << " target=" << endpoint.target;
|
||||
|
||||
auto& websocket = connection->websocket;
|
||||
const auto results = connection->resolver.resolve(endpoint.host, endpoint.port);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host;
|
||||
auto& stream = boost::beast::get_lowest_layer(websocket);
|
||||
stream.expires_after(std::chrono::seconds(10));
|
||||
stream.connect(results);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
|
||||
<< " port=" << endpoint.port;
|
||||
|
||||
// The aggregate viewer is a TLS WebSocket endpoint. Set SNI before the
|
||||
// TLS handshake so the cloud edge selects the correct certificate.
|
||||
auto& tls_stream = websocket.next_layer();
|
||||
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
|
||||
throw std::runtime_error("failed to set Orca Cloud TLS server name");
|
||||
connection->ssl_context.set_default_verify_paths();
|
||||
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
|
||||
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
|
||||
tls_stream.handshake(boost::asio::ssl::stream_base::client);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TLS handshake completed host=" << endpoint.host;
|
||||
|
||||
const std::string token = get_token ? get_token() : std::string();
|
||||
if (token.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT token callback returned an empty token";
|
||||
throw std::runtime_error("no access token for Orca Cloud WebSocket");
|
||||
}
|
||||
|
||||
websocket.set_option(boost::beast::websocket::stream_base::decorator(
|
||||
[token](boost::beast::websocket::request_type& request) {
|
||||
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
|
||||
request.set(boost::beast::http::field::authorization, "Bearer " + token);
|
||||
request.set("Sec-WebSocket-Protocol", "mqtt");
|
||||
}));
|
||||
boost::beast::http::response<boost::beast::http::string_body> response;
|
||||
boost::system::error_code handshake_error;
|
||||
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
|
||||
if (handshake_error) {
|
||||
// Surface the server's HTTP status so a persistent rejection (stale token,
|
||||
// missing api key, wrong route) is diagnosable from the log rather than an
|
||||
// opaque "handshake declined".
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudMqttConnection: handshake rejected, http="
|
||||
<< response.result_int() << " (" << response.reason() << "), "
|
||||
<< handshake_error.message();
|
||||
throw boost::system::system_error(handshake_error, "Orca Cloud WebSocket handshake");
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket handshake completed http=" << response.result_int()
|
||||
<< " negotiated_protocol=" << response["Sec-WebSocket-Protocol"];
|
||||
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: WebSocket handshake did not negotiate MQTT";
|
||||
throw std::runtime_error("Orca Cloud WebSocket did not negotiate MQTT");
|
||||
}
|
||||
|
||||
stream.expires_never();
|
||||
send(websocket, make_connect_packet());
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent";
|
||||
|
||||
boost::beast::flat_buffer buffer;
|
||||
stream.expires_after(std::chrono::seconds(10));
|
||||
websocket.read(buffer);
|
||||
const std::string connack = boost::beast::buffers_to_string(buffer.data());
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNACK received bytes=" << connack.size()
|
||||
<< " header=" << (connack.empty() ? -1 : static_cast<int>(static_cast<uint8_t>(connack[0])))
|
||||
<< " return_code=" << (connack.size() > 3 ? static_cast<int>(static_cast<uint8_t>(connack[3])) : -1);
|
||||
if (connack.size() != 4 || static_cast<uint8_t>(connack[0]) != 0x20 ||
|
||||
static_cast<uint8_t>(connack[2]) != 0x00 || static_cast<uint8_t>(connack[3]) != 0x00) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT was refused or malformed";
|
||||
throw std::runtime_error("Orca Cloud MQTT CONNECT was refused");
|
||||
}
|
||||
|
||||
notify_state(true);
|
||||
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions";
|
||||
send_current_subscriptions(websocket);
|
||||
std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
|
||||
while (!stopping.load()) {
|
||||
send_pending_subscriptions(websocket);
|
||||
buffer.consume(buffer.size());
|
||||
stream.expires_after(std::chrono::seconds(1));
|
||||
boost::system::error_code error;
|
||||
websocket.read(buffer, error);
|
||||
if (error == boost::beast::error::timeout) {
|
||||
if (std::chrono::steady_clock::now() >= next_ping) {
|
||||
send(websocket, make_ping_packet());
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent";
|
||||
next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (error) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT WebSocket read failed code=" << error.value()
|
||||
<< " message=" << error.message();
|
||||
throw boost::system::system_error(error, "read Orca Cloud MQTT message");
|
||||
}
|
||||
handle_packet(boost::beast::buffers_to_string(buffer.data()));
|
||||
}
|
||||
|
||||
boost::system::error_code close_error;
|
||||
websocket.close(boost::beast::websocket::close_code::normal, close_error);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection closed code=" << close_error.value()
|
||||
<< " message=" << close_error.message();
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::send_current_subscriptions(WebSocket& websocket) {
|
||||
std::vector<std::string> devices;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
devices.assign(subscriptions.begin(), subscriptions.end());
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << devices.size();
|
||||
if (!devices.empty()) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE packet_id=" << packet_id;
|
||||
send(websocket, make_topic_packet(0x82, packet_id, devices));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::send_pending_subscriptions(WebSocket& websocket) {
|
||||
std::vector<std::string> subscribe_ids;
|
||||
std::vector<std::string> unsubscribe_ids;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
subscribe_ids.assign(pending_subscriptions.begin(), pending_subscriptions.end());
|
||||
unsubscribe_ids.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
|
||||
pending_subscriptions.clear();
|
||||
pending_unsubscriptions.clear();
|
||||
}
|
||||
if (!subscribe_ids.empty()) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE count=" << subscribe_ids.size()
|
||||
<< " packet_id=" << packet_id;
|
||||
for (const std::string& device_id : subscribe_ids)
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: SUBSCRIBE topic=" << report_topic(device_id);
|
||||
send(websocket, make_topic_packet(0x82, packet_id, subscribe_ids));
|
||||
}
|
||||
if (!unsubscribe_ids.empty()) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE count=" << unsubscribe_ids.size()
|
||||
<< " packet_id=" << packet_id;
|
||||
for (const std::string& device_id : unsubscribe_ids)
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: UNSUBSCRIBE topic=" << report_topic(device_id);
|
||||
send(websocket, make_topic_packet(0xa2, packet_id, unsubscribe_ids));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::handle_packet(const std::string& packet) {
|
||||
if (packet.size() < 2) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: received undersized MQTT packet bytes=" << packet.size();
|
||||
return;
|
||||
}
|
||||
const uint8_t header = static_cast<uint8_t>(packet[0]);
|
||||
const uint8_t packet_type = header >> 4;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast<unsigned int>(packet_type)
|
||||
<< " header=0x" << std::hex << static_cast<unsigned int>(header) << std::dec
|
||||
<< " bytes=" << packet.size();
|
||||
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
|
||||
if (packet_type == 9 && packet.size() >= 5) {
|
||||
std::ostringstream result_codes;
|
||||
for (size_t index = 4; index < packet.size(); ++index) {
|
||||
if (index != 4)
|
||||
result_codes << ',';
|
||||
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id="
|
||||
<< ((static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
|
||||
static_cast<unsigned int>(static_cast<uint8_t>(packet[3])))
|
||||
<< " result_codes=" << result_codes.str();
|
||||
}
|
||||
return;
|
||||
}
|
||||
size_t index = 1;
|
||||
size_t multiplier = 1;
|
||||
size_t remaining = 0;
|
||||
uint8_t encoded = 0;
|
||||
do {
|
||||
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH remaining length";
|
||||
return;
|
||||
}
|
||||
encoded = static_cast<uint8_t>(packet[index++]);
|
||||
remaining += (encoded & 0x7f) * multiplier;
|
||||
multiplier *= 128;
|
||||
} while ((encoded & 0x80) != 0);
|
||||
const size_t remaining_end = index + remaining;
|
||||
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH body remaining=" << remaining
|
||||
<< " packet_bytes=" << packet.size();
|
||||
return;
|
||||
}
|
||||
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
|
||||
static_cast<uint8_t>(packet[index + 1]);
|
||||
index += 2;
|
||||
if (topic_length > packet.size() - index) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH topic length=" << topic_length;
|
||||
return;
|
||||
}
|
||||
const std::string topic(packet.data() + index, topic_length);
|
||||
index += topic_length;
|
||||
if (((header >> 1) & 0x03) != 0) {
|
||||
if (index + 2 > remaining_end) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH packet identifier";
|
||||
return;
|
||||
}
|
||||
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
|
||||
}
|
||||
const size_t payload_size = remaining_end - index;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received PUBLISH topic=" << topic
|
||||
<< " payload_bytes=" << payload_size
|
||||
<< " message_callback=" << (on_message ? "set" : "null");
|
||||
if (on_message)
|
||||
on_message(topic, packet.substr(index, remaining_end - index));
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set";
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::notify_state(bool is_now_connected) {
|
||||
StateHandler callback;
|
||||
bool initial = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connected = is_now_connected;
|
||||
initial = !initial_completed;
|
||||
if (initial) {
|
||||
initial_result = is_now_connected;
|
||||
initial_completed = true;
|
||||
}
|
||||
callback = on_state;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT state changed connected=" << is_now_connected
|
||||
<< " initial=" << initial << " state_callback=" << (callback ? "set" : "null");
|
||||
if (initial)
|
||||
initial_cv.notify_all();
|
||||
else if (callback)
|
||||
callback(is_now_connected, false);
|
||||
}
|
||||
|
||||
void OrcaCloudMqttConnection::run() {
|
||||
while (!stopping.load()) {
|
||||
const int retry_seconds = reconnect_delay_seconds.load();
|
||||
try {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt retry_delay=" << retry_seconds;
|
||||
connect_and_read();
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt failed: " << error.what();
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
}
|
||||
if (stopping.load())
|
||||
break;
|
||||
// Grow the backoff only across attempts that never reached CONNACK; a
|
||||
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
|
||||
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT waiting before reconnect seconds=" << retry_seconds;
|
||||
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com";
|
||||
constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com";
|
||||
@@ -1013,7 +497,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
|
||||
, api_base_url(ORCA_DEFAULT_API_URL)
|
||||
, auth_base_url(ORCA_DEFAULT_AUTH_URL)
|
||||
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
|
||||
, mqtt_connection(std::make_unique<OrcaCloudMqttConnection>())
|
||||
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
|
||||
{
|
||||
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
|
||||
pkce_bundle.loopback_port = choose_loopback_port();
|
||||
@@ -1487,79 +971,22 @@ int OrcaCloudServiceAgent::connect_server()
|
||||
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;
|
||||
// connect_server() is a REST health probe only. The long-lived MQTT socket is
|
||||
// per-printer now, driven by set_user_selected_machine ->
|
||||
// configure_selected_printer_mqtt; this method must not touch mqtt_connection.
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(state_mutex);
|
||||
is_connected = connected;
|
||||
}
|
||||
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);
|
||||
is_connected = connected;
|
||||
}
|
||||
invoke_server_connected_callback(connected ? 0 : -1, http_code);
|
||||
}
|
||||
|
||||
return (connected || mqtt_alive) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
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 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();
|
||||
// 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;
|
||||
}
|
||||
@@ -1586,7 +1013,9 @@ int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
|
||||
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);
|
||||
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;
|
||||
}
|
||||
@@ -1599,11 +1028,68 @@ int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list)
|
||||
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);
|
||||
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::Config cfg;
|
||||
cfg.url = "wss://" + api_base_url + "/api/v1/printers/" + dev_id + "/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;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring per-printer 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); },
|
||||
[this](bool, bool) {});
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: per-printer 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);
|
||||
@@ -3293,6 +2779,15 @@ int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::str
|
||||
|
||||
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.
|
||||
if (role.empty() || role == "viewer")
|
||||
continue;
|
||||
|
||||
device["dev_id"] = printer.value("id", "");
|
||||
device["dev_name"] = printer.value("name", "");
|
||||
if (printer.contains("model") && printer["model"].is_string())
|
||||
|
||||
@@ -24,84 +24,14 @@
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
|
||||
class wxSecretStore;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Forward declarations
|
||||
class AppConfig;
|
||||
// MQTT 3.1.1 over the aggregate WebSocket is deliberately kept here instead
|
||||
// of using the printer SDK. The endpoint is a read-only status stream; MQTT
|
||||
// PUBLISH must never be sent on it because the cloud closes such sessions.
|
||||
class OrcaCloudMqttConnection
|
||||
{
|
||||
public:
|
||||
using TokenProvider = std::function<std::string()>;
|
||||
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
|
||||
using StateHandler = std::function<void(bool connected, bool initial)>;
|
||||
|
||||
~OrcaCloudMqttConnection();
|
||||
|
||||
bool start(const std::string& endpoint, TokenProvider token_provider, MessageHandler message_handler, StateHandler state_handler);
|
||||
void stop();
|
||||
// True while the worker thread is alive (connected OR retrying). Lets callers
|
||||
// avoid restarting a healthy connection.
|
||||
bool is_running() const;
|
||||
// True once CONNACK has been received and the socket has not since dropped.
|
||||
bool is_connected() const { return connected.load(); }
|
||||
bool subscribe(const std::vector<std::string>& device_ids);
|
||||
bool unsubscribe(const std::vector<std::string>& device_ids);
|
||||
void clear_subscriptions();
|
||||
|
||||
private:
|
||||
struct Endpoint { std::string host; std::string port; std::string target; };
|
||||
using WebSocket = boost::beast::websocket::stream<
|
||||
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
struct Connection;
|
||||
|
||||
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
|
||||
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
|
||||
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
|
||||
static std::vector<uint8_t> make_connect_packet();
|
||||
static std::string report_topic(const std::string& device_id);
|
||||
static std::vector<uint8_t> make_topic_packet(uint8_t type, uint16_t packet_id, const std::vector<std::string>& device_ids);
|
||||
static std::vector<uint8_t> make_ping_packet();
|
||||
|
||||
void send(WebSocket& websocket, const std::vector<uint8_t>& packet);
|
||||
// Emit a queued SUBSCRIBE/UNSUBSCRIBE on the live socket right now (from the
|
||||
// caller thread), so a selection change is applied without waiting for the
|
||||
// blocking read loop to next return. No-op if no CONNACKed socket exists yet
|
||||
// (the worker sends the set on connect). The aggregate viewer is dynamic — the
|
||||
// WebSocket is never dropped for a subscription change.
|
||||
void flush_subscription_change();
|
||||
void connect_and_read();
|
||||
void send_current_subscriptions(WebSocket& websocket);
|
||||
void send_pending_subscriptions(WebSocket& websocket);
|
||||
void handle_packet(const std::string& packet);
|
||||
void notify_state(bool is_now_connected);
|
||||
void run();
|
||||
|
||||
std::atomic_bool stopping{true};
|
||||
std::atomic_int reconnect_delay_seconds{1};
|
||||
std::thread worker;
|
||||
std::mutex mutex;
|
||||
std::mutex connection_mutex;
|
||||
std::mutex write_mutex; // serialises every websocket write (worker + caller threads)
|
||||
std::shared_ptr<Connection> active_connection;
|
||||
std::condition_variable initial_cv;
|
||||
std::condition_variable state_cv;
|
||||
std::string endpoint_url;
|
||||
TokenProvider get_token;
|
||||
MessageHandler on_message;
|
||||
StateHandler on_state;
|
||||
std::set<std::string> subscriptions;
|
||||
std::set<std::string> pending_subscriptions;
|
||||
std::set<std::string> pending_unsubscriptions;
|
||||
std::atomic<uint16_t> next_packet_id{1};
|
||||
bool initial_result{false};
|
||||
bool initial_completed{false};
|
||||
std::atomic_bool connected{false};
|
||||
};
|
||||
struct BundleMetadata;
|
||||
struct PluginDescriptor;
|
||||
struct PluginChangelog;
|
||||
@@ -288,10 +218,10 @@ public:
|
||||
int del_subscribe(std::vector<std::string> dev_list) override;
|
||||
void enable_multi_machine(bool enable) override;
|
||||
|
||||
// The aggregate printer socket is status-only. OrcaPrinterAgent registers
|
||||
// its normal message callback here and adds/removes device report topics
|
||||
// through add_subscribe()/del_subscribe(). Printer commands continue to
|
||||
// use the REST commands endpoint; they must never be published here.
|
||||
// The per-printer MQTT socket carries both directions: inbound reports from
|
||||
// device/<id>/report and commands PUBLISHed to device/<id>/request on this
|
||||
// socket. OrcaPrinterAgent registers its message callback here to receive the
|
||||
// inbound half; pass an empty fn to clear it before the agent is destroyed.
|
||||
int set_printer_status_callback(OnMessageFn fn);
|
||||
|
||||
// Send a Bambu-dialect command to one printer via the cloud relay's REST
|
||||
@@ -439,7 +369,26 @@ 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();
|
||||
}
|
||||
|
||||
// Per-printer cloud socket: wss://<api_base_url>/api/v1/printers/<dev_id>/mqtt.
|
||||
// configure_ blocks for the duration of the initial connect attempt, so callers
|
||||
// drive it off the UI thread; teardown_ is synchronous.
|
||||
int configure_selected_printer_mqtt(const std::string& dev_id);
|
||||
void teardown_selected_printer_mqtt();
|
||||
// Test hook: the wss:// URL of the current per-printer socket ("" when none).
|
||||
std::string selected_printer_mqtt_url() const;
|
||||
|
||||
private:
|
||||
// Fans one inbound per-printer 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,
|
||||
@@ -516,7 +465,9 @@ private:
|
||||
std::chrono::system_clock::now().time_since_epoch()).count()};
|
||||
|
||||
// Member variables - connection state
|
||||
std::unique_ptr<OrcaCloudMqttConnection> mqtt_connection;
|
||||
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};
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
#include "OrcaMqttConnection.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 <boost/log/trivial.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;
|
||||
// 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)
|
||||
{}
|
||||
};
|
||||
|
||||
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);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start url=" << config.url
|
||||
<< " use_tls=" << config.use_tls
|
||||
<< " bearer_provider=" << (config.bearer_provider ? "set" : "null")
|
||||
<< " username_present=" << (!config.username.empty())
|
||||
<< " password_present=" << (!config.password.empty())
|
||||
<< " client_id=" << config.client_id
|
||||
<< " keepalive_seconds=" << config.keepalive_seconds
|
||||
<< " message_callback=" << (on_message ? "set" : "null")
|
||||
<< " state_callback=" << (on_state ? "set" : "null");
|
||||
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;
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds"
|
||||
<< " url=" << current_config.url
|
||||
<< " last_connack_rc=" << m_last_connack_rc.load()
|
||||
<< " connected=" << connected.load()
|
||||
<< "; worker will retry";
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result
|
||||
<< " initial_completed=" << initial_completed
|
||||
<< " last_connack_rc=" << m_last_connack_rc.load()
|
||||
<< " worker_running=" << (worker.joinable() && !stopping.load());
|
||||
return initial_result;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::stop() {
|
||||
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested"
|
||||
<< " url=" << current_config.url
|
||||
<< " connected=" << connected.load()
|
||||
<< " worker_joinable=" << worker.joinable()
|
||||
<< " last_connack_rc=" << m_last_connack_rc.load();
|
||||
stopping.store(true);
|
||||
state_cv.notify_all();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
if (active_connection) {
|
||||
// Generic so it accepts either the TLS or the plaintext websocket.
|
||||
auto shutdown_socket = [](auto& websocket) {
|
||||
auto& socket = boost::beast::get_lowest_layer(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);
|
||||
};
|
||||
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;
|
||||
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
|
||||
|
||||
// beast permits a concurrent writer while the worker is blocked in
|
||||
// websocket.read(); every write is serialised by write_mutex inside ws_write().
|
||||
try {
|
||||
send_pending_subscriptions(*conn);
|
||||
} 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 OrcaMqttConnection::subscribe(const std::string& dev_id) {
|
||||
if (dev_id.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected empty dev_id";
|
||||
return false;
|
||||
}
|
||||
const std::string topic = report_topic(dev_id);
|
||||
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected oversized topic=" << topic;
|
||||
return false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
subscriptions.insert(topic);
|
||||
pending_unsubscriptions.erase(topic);
|
||||
pending_subscriptions.insert(topic);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued topic=" << topic
|
||||
<< " total_subscriptions=" << subscriptions.size()
|
||||
<< " connected=" << connected.load();
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // emit SUBSCRIBE now on the live socket (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);
|
||||
pending_subscriptions.erase(topic);
|
||||
pending_unsubscriptions.insert(topic);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic
|
||||
<< " total_subscriptions=" << subscriptions.size()
|
||||
<< " connected=" << connected.load();
|
||||
}
|
||||
state_cv.notify_all();
|
||||
flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect)
|
||||
return true;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::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 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()) {
|
||||
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();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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_close(Connection& conn) {
|
||||
boost::system::error_code close_error;
|
||||
if (conn.wss)
|
||||
conn.wss->close(boost::beast::websocket::close_code::normal, close_error);
|
||||
else if (conn.ws)
|
||||
conn.ws->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();
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket resolve starting host=" << endpoint.host
|
||||
<< " port=" << endpoint.port << " target=" << endpoint.target
|
||||
<< " tls=" << config.use_tls;
|
||||
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host;
|
||||
|
||||
std::string token;
|
||||
if (config.bearer_provider) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: requesting bearer token for WebSocket upgrade";
|
||||
token = config.bearer_provider();
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: bearer token callback completed token_present=" << !token.empty();
|
||||
}
|
||||
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);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
|
||||
<< " port=" << endpoint.port;
|
||||
// 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");
|
||||
conn.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;
|
||||
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending TLS WebSocket upgrade target=" << endpoint.target
|
||||
<< " bearer_header=" << (!token.empty());
|
||||
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);
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
|
||||
<< " port=" << endpoint.port << " (plaintext)";
|
||||
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending plaintext WebSocket upgrade target=" << endpoint.target
|
||||
<< " bearer_header=" << (!token.empty());
|
||||
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) << "Orca diagnostic: WS handshake rejected http="
|
||||
<< response.result_int() << " (" << response.reason() << "), "
|
||||
<< handshake_error.message();
|
||||
throw boost::system::system_error(handshake_error, "Orca 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 WebSocket did not negotiate MQTT");
|
||||
}
|
||||
}
|
||||
|
||||
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
|
||||
if (dev_id.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected empty dev_id";
|
||||
return false;
|
||||
}
|
||||
if (!connected.load()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because connection is not ready"
|
||||
<< " dev_id=" << dev_id << " last_connack_rc=" << m_last_connack_rc.load();
|
||||
return false;
|
||||
}
|
||||
std::shared_ptr<Connection> conn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
conn = active_connection;
|
||||
}
|
||||
if (!conn) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because active connection is null"
|
||||
<< " dev_id=" << dev_id;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// ws_write() serialises the write via write_mutex; do not lock it here.
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PUBLISH request dev_id=" << dev_id
|
||||
<< " topic=" << request_topic(dev_id)
|
||||
<< " payload_bytes=" << payload.size();
|
||||
ws_write(*conn, make_publish_packet(request_topic(dev_id), payload));
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_request failed dev_id=" << dev_id
|
||||
<< " (" << e.what() << ")";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::connect_and_read() {
|
||||
m_connection_stage = "creating connection";
|
||||
auto connection = std::make_shared<Connection>();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(connection_mutex);
|
||||
active_connection = connection;
|
||||
if (stopping.load())
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection_stage = "parsing endpoint";
|
||||
Endpoint endpoint;
|
||||
if (!parse_endpoint(current_config.url, endpoint)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << current_config.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;
|
||||
|
||||
m_connection_stage = "WebSocket handshake";
|
||||
ws_handshake(*connection, current_config, endpoint);
|
||||
|
||||
m_connection_stage = "sending MQTT CONNECT";
|
||||
expires_never(*connection);
|
||||
// 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_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent";
|
||||
|
||||
m_connection_stage = "waiting for MQTT CONNACK";
|
||||
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");
|
||||
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);
|
||||
// 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) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT refused rc=" << rc;
|
||||
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));
|
||||
}
|
||||
|
||||
m_connection_stage = "reading MQTT messages";
|
||||
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(*connection);
|
||||
std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
|
||||
while (!stopping.load()) {
|
||||
send_pending_subscriptions(*connection);
|
||||
// Keepalive is driven every iteration, not only from the read-timeout branch:
|
||||
// a printer pushing faster than the 1s read deadline would otherwise keep the
|
||||
// read hot and the broker would drop us at 1.5 x keepalive.
|
||||
if (std::chrono::steady_clock::now() >= next_ping) {
|
||||
ws_write(*connection, make_ping_packet());
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent";
|
||||
next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
}
|
||||
buffer.consume(buffer.size());
|
||||
m_connection_stage = "reading MQTT frame";
|
||||
expires_after(*connection, std::chrono::seconds(1));
|
||||
boost::system::error_code error;
|
||||
ws_read(*connection, buffer, error);
|
||||
if (error == boost::beast::error::timeout)
|
||||
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 MQTT message");
|
||||
}
|
||||
handle_packet(boost::beast::buffers_to_string(buffer.data()));
|
||||
}
|
||||
|
||||
ws_close(*connection);
|
||||
if (!stopping.load())
|
||||
notify_state(false);
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_current_subscriptions(Connection& conn) {
|
||||
std::vector<std::string> topics;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
topics.assign(subscriptions.begin(), subscriptions.end());
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << topics.size();
|
||||
for (const std::string& topic : topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << packet_id;
|
||||
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::send_pending_subscriptions(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++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic
|
||||
<< " packet_id=" << packet_id;
|
||||
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
|
||||
}
|
||||
for (const std::string& topic : unsubscribe_topics) {
|
||||
const uint16_t packet_id = next_packet_id++;
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE topic=" << topic
|
||||
<< " packet_id=" << packet_id;
|
||||
ws_write(conn, make_unsubscribe_packet(packet_id, topic));
|
||||
}
|
||||
}
|
||||
|
||||
void OrcaMqttConnection::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");
|
||||
// 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()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH on unrecognized topic=" << topic;
|
||||
} else if (on_message) {
|
||||
on_message(dev_id, packet.substr(index, remaining_end - index));
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 OrcaMqttConnection::run() {
|
||||
while (!stopping.load()) {
|
||||
const int retry_seconds = reconnect_delay_seconds.load();
|
||||
const uint64_t attempt = ++m_attempt_number;
|
||||
m_connection_stage = "starting attempt";
|
||||
try {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt=" << attempt
|
||||
<< " retry_delay=" << retry_seconds
|
||||
<< " url=" << current_config.url;
|
||||
connect_and_read();
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt=" << attempt
|
||||
<< " failed stage=" << m_connection_stage
|
||||
<< " error=" << error.what()
|
||||
<< " last_connack_rc=" << m_last_connack_rc.load()
|
||||
<< " stopping=" << stopping.load();
|
||||
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 Slic3r
|
||||
@@ -0,0 +1,144 @@
|
||||
#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 <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#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 (per-printer) 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;
|
||||
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).
|
||||
void ws_write(Connection& conn, const std::vector<uint8_t>& packet); // locks write_mutex
|
||||
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 ws_close(Connection& conn);
|
||||
// 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 WebSocket is never dropped for a
|
||||
// subscription change.
|
||||
void flush_subscription_change();
|
||||
void connect_and_read();
|
||||
void send_current_subscriptions(Connection& conn);
|
||||
void send_pending_subscriptions(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::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;
|
||||
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;
|
||||
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
|
||||
std::string m_connection_stage; // worker-thread diagnostic stage
|
||||
bool initial_result{false};
|
||||
bool initial_completed{false};
|
||||
std::atomic_bool connected{false};
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_OrcaMqttConnection_hpp_
|
||||
@@ -1,18 +1,362 @@
|
||||
#include "OrcaPrinterAgent.hpp"
|
||||
#include "NetworkAgentFactory.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include <algorithm>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
const std::string OrcaPrinterAgent_VERSION = "0.0.1";
|
||||
|
||||
OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir))
|
||||
class OrcaPrinterAgent::OrcaSonarDiscovery
|
||||
{
|
||||
public:
|
||||
using EmitFn = std::function<void(const std::string&)>;
|
||||
|
||||
explicit OrcaSonarDiscovery(EmitFn emit) : m_emit(std::move(emit)) {}
|
||||
~OrcaSonarDiscovery() { stop(); }
|
||||
|
||||
OrcaSonarDiscovery(const OrcaSonarDiscovery&) = delete;
|
||||
OrcaSonarDiscovery& operator=(const OrcaSonarDiscovery&) = delete;
|
||||
|
||||
void start()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_lifecycle_mutex);
|
||||
if (m_running.exchange(true))
|
||||
return;
|
||||
m_thread = std::thread(&OrcaSonarDiscovery::browse_loop, this);
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_lifecycle_mutex);
|
||||
if (!m_running.exchange(false))
|
||||
return;
|
||||
m_wait_cv.notify_all();
|
||||
if (m_thread.joinable())
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string lower_ascii(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string trim_ascii(const std::string& value)
|
||||
{
|
||||
const auto first = value.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos)
|
||||
return {};
|
||||
const auto last = value.find_last_not_of(" \t\r\n");
|
||||
return value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
static bool get_header(const std::string& datagram, const std::string& header_name, std::string& value)
|
||||
{
|
||||
std::size_t line_start = 0;
|
||||
while (line_start < datagram.size()) {
|
||||
const std::size_t line_end = datagram.find('\n', line_start);
|
||||
const std::string line = datagram.substr(line_start, line_end == std::string::npos ? std::string::npos : line_end - line_start);
|
||||
line_start = line_end == std::string::npos ? datagram.size() : line_end + 1;
|
||||
|
||||
const std::size_t colon = line.find(':');
|
||||
if (colon == std::string::npos)
|
||||
continue;
|
||||
if (lower_ascii(trim_ascii(line.substr(0, colon))) != lower_ascii(header_name))
|
||||
continue;
|
||||
value = trim_ascii(line.substr(colon + 1));
|
||||
return !value.empty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool make_machine_alive_json(const std::string& usn, const std::string& host, const std::string& location, std::string& json)
|
||||
{
|
||||
const std::string lower_usn = lower_ascii(usn);
|
||||
static const std::string uuid_prefix = "uuid:";
|
||||
static const std::string device_type = "::urn:schemas-upnp-org:device:basic:1";
|
||||
if (lower_usn.rfind(uuid_prefix, 0) != 0)
|
||||
return false;
|
||||
|
||||
const std::size_t type_start = lower_usn.find(device_type, uuid_prefix.size());
|
||||
if (type_start == std::string::npos)
|
||||
return false;
|
||||
const std::string device_id = trim_ascii(usn.substr(uuid_prefix.size(), type_start - uuid_prefix.size()));
|
||||
if (device_id.empty() || host.empty())
|
||||
return false;
|
||||
|
||||
const std::string lower_location = lower_ascii(location);
|
||||
const std::size_t scheme_end = lower_location.find("://");
|
||||
if (scheme_end == std::string::npos)
|
||||
return false;
|
||||
const std::size_t authority_start = scheme_end + 3;
|
||||
const std::size_t authority_end = location.find_first_of("/ ?#", authority_start);
|
||||
const std::string authority = location.substr(authority_start, authority_end == std::string::npos ?
|
||||
std::string::npos :
|
||||
authority_end - authority_start);
|
||||
std::string port = "8280";
|
||||
if (!authority.empty()) {
|
||||
if (authority.front() == '[') {
|
||||
const std::size_t bracket = authority.find(']');
|
||||
if (bracket != std::string::npos && bracket + 1 < authority.size() && authority[bracket + 1] == ':')
|
||||
port = authority.substr(bracket + 2);
|
||||
} else {
|
||||
const std::size_t colon = authority.rfind(':');
|
||||
if (colon != std::string::npos && colon + 1 < authority.size())
|
||||
port = authority.substr(colon + 1);
|
||||
}
|
||||
}
|
||||
if (port.empty() || port.find_first_not_of("0123456789") != std::string::npos)
|
||||
return false;
|
||||
|
||||
nlohmann::json machine;
|
||||
machine["dev_name"] = device_id;
|
||||
machine["dev_id"] = device_id;
|
||||
machine["dev_ip"] = host + ":" + port;
|
||||
machine["dev_type"] = "orcasonar";
|
||||
machine["dev_signal"] = "0";
|
||||
machine["connect_type"] = "lan";
|
||||
machine["bind_state"] = "free";
|
||||
machine["sec_link"] = "secure";
|
||||
machine["ssdp_version"] = "v1";
|
||||
machine["connection_name"] = device_id;
|
||||
json = machine.dump();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ssdp_round()
|
||||
{
|
||||
namespace asio = boost::asio;
|
||||
using asio::ip::udp;
|
||||
try {
|
||||
asio::io_context io_context;
|
||||
udp::socket socket(io_context);
|
||||
socket.open(udp::v4());
|
||||
socket.set_option(udp::socket::reuse_address(true));
|
||||
socket.bind(udp::endpoint(udp::v4(), 0));
|
||||
socket.non_blocking(true);
|
||||
|
||||
static constexpr char search_request[] = "M-SEARCH * HTTP/1.1\r\n"
|
||||
"HOST: 239.255.255.250:1900\r\n"
|
||||
"MAN: \"ssdp:discover\"\r\n"
|
||||
"MX: 2\r\n"
|
||||
"ST: urn:schemas-upnp-org:device:Basic:1\r\n"
|
||||
"\r\n";
|
||||
const auto multicast = asio::ip::make_address_v4("239.255.255.250");
|
||||
socket.send_to(asio::buffer(search_request, sizeof(search_request) - 1), udp::endpoint(multicast, 1900));
|
||||
|
||||
std::array<char, 4096> buffer{};
|
||||
udp::endpoint sender;
|
||||
std::set<std::string> seen_ids;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||
while (m_running.load() && std::chrono::steady_clock::now() < deadline) {
|
||||
boost::system::error_code error;
|
||||
const std::size_t received = socket.receive_from(asio::buffer(buffer), sender, 0, error);
|
||||
if (!error) {
|
||||
const std::string datagram(buffer.data(), received);
|
||||
std::string usn;
|
||||
std::string location;
|
||||
std::string server;
|
||||
if (get_header(datagram, "USN", usn) && get_header(datagram, "LOCATION", location) &&
|
||||
(!get_header(datagram, "SERVER", server) || lower_ascii(server).find("orcasonar") != std::string::npos)) {
|
||||
std::string machine_alive;
|
||||
if (make_machine_alive_json(usn, sender.address().to_string(), location, machine_alive)) {
|
||||
nlohmann::json machine = nlohmann::json::parse(machine_alive);
|
||||
const std::string device_id = machine["dev_id"].get<std::string>();
|
||||
if (seen_ids.insert(device_id).second && m_emit)
|
||||
m_emit(machine_alive);
|
||||
}
|
||||
}
|
||||
} else if (error != asio::error::would_block && error != asio::error::try_again) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaSonarDiscovery: SSDP receive failed: " << error.message();
|
||||
break;
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaSonarDiscovery: SSDP round failed: " << error.what();
|
||||
}
|
||||
}
|
||||
|
||||
void browse_loop()
|
||||
{
|
||||
while (m_running.load()) {
|
||||
ssdp_round();
|
||||
std::unique_lock<std::mutex> lock(m_wait_mutex);
|
||||
m_wait_cv.wait_for(lock, std::chrono::seconds(5), [this] { return !m_running.load(); });
|
||||
}
|
||||
}
|
||||
|
||||
EmitFn m_emit;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_thread;
|
||||
std::mutex m_lifecycle_mutex;
|
||||
std::mutex m_wait_mutex;
|
||||
std::condition_variable m_wait_cv;
|
||||
};
|
||||
|
||||
OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) {}
|
||||
|
||||
OrcaPrinterAgent::~OrcaPrinterAgent()
|
||||
{
|
||||
start_discovery(false, false);
|
||||
++m_lan_generation; // fence any late worker callback
|
||||
++m_cloud_generation;
|
||||
|
||||
// Drop the cloud status callback before anything else: it holds `this`, and the
|
||||
// cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps
|
||||
// the printer agent while m_cloud_agents persist).
|
||||
if (auto* cloud = get_orca_cloud_agent())
|
||||
cloud->set_printer_status_callback(nullptr);
|
||||
|
||||
// Stop the LAN connection so the connect thread's start() returns, but keep the
|
||||
// object alive until that thread is joined (the thread holds a raw conn pointer).
|
||||
OrcaMqttConnection* live_lan = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
live_lan = lan_mqtt_connection.get();
|
||||
}
|
||||
if (live_lan)
|
||||
live_lan->stop();
|
||||
|
||||
// Same for the cloud per-printer connection the cloud connect thread may hold.
|
||||
if (auto* cloud = get_orca_cloud_agent())
|
||||
cloud->teardown_selected_printer_mqtt();
|
||||
|
||||
if (m_lan_connect_thread.joinable())
|
||||
m_lan_connect_thread.join();
|
||||
if (m_cloud_connect_thread.joinable())
|
||||
m_cloud_connect_thread.join();
|
||||
|
||||
// stop() is not sticky: a connect thread that had not yet reached start() when the
|
||||
// teardown above ran could have raised a fresh socket in between. Tear down once
|
||||
// more now that both threads are joined, so no live socket survives *this.
|
||||
if (auto* cloud = get_orca_cloud_agent())
|
||||
cloud->teardown_selected_printer_mqtt();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
lan_mqtt_connection.reset();
|
||||
}
|
||||
}
|
||||
|
||||
OrcaPrinterAgent::~OrcaPrinterAgent() = default;
|
||||
OrcaCloudServiceAgent* OrcaPrinterAgent::get_orca_cloud_agent()
|
||||
{
|
||||
if (!m_cloud_agent)
|
||||
return nullptr;
|
||||
|
||||
return dynamic_cast<OrcaCloudServiceAgent*>(m_cloud_agent.get());
|
||||
}
|
||||
|
||||
OrcaMqttConnection* OrcaPrinterAgent::get_appropriate_mqtt_connection(bool is_lan)
|
||||
{
|
||||
if (is_lan) {
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
return lan_mqtt_connection.get();
|
||||
}
|
||||
auto* cloud = get_orca_cloud_agent();
|
||||
return cloud ? cloud->get_mqtt_connection() : nullptr;
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload)
|
||||
{
|
||||
OnMessageFn fn;
|
||||
QueueOnMainFn q;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
fn = on_message_fn;
|
||||
q = queue_on_main_fn;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id
|
||||
<< " payload_bytes=" << payload.size()
|
||||
<< " callback=" << (fn ? "set" : "null")
|
||||
<< " queue_on_main=" << (q ? "set" : "null");
|
||||
if (!fn) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set"
|
||||
<< " dev_id=" << dev_id;
|
||||
return;
|
||||
}
|
||||
if (q)
|
||||
q([fn, dev_id, payload] { fn(dev_id, payload); });
|
||||
else
|
||||
fn(dev_id, payload);
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const std::string& payload)
|
||||
{
|
||||
OnMessageFn fn;
|
||||
QueueOnMainFn q;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
fn = on_local_message_fn;
|
||||
q = queue_on_main_fn;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering LAN message dev_id=" << dev_id
|
||||
<< " payload_bytes=" << payload.size()
|
||||
<< " callback=" << (fn ? "set" : "null")
|
||||
<< " queue_on_main=" << (q ? "set" : "null");
|
||||
if (!fn) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping LAN message because on_local_message_fn is not set"
|
||||
<< " dev_id=" << dev_id;
|
||||
return;
|
||||
}
|
||||
if (q)
|
||||
q([fn, dev_id, payload] { fn(dev_id, payload); });
|
||||
else
|
||||
fn(dev_id, payload);
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& message)
|
||||
{
|
||||
OnLocalConnectedFn callback;
|
||||
QueueOnMainFn queue;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
callback = on_local_connect_fn;
|
||||
queue = queue_on_main_fn;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state
|
||||
<< " dev_id=" << dev_id << " message=" << message
|
||||
<< " callback=" << (callback ? "set" : "null")
|
||||
<< " queue_on_main=" << (queue ? "set" : "null");
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
auto dispatch = [callback, state, dev_id, message] { callback(state, dev_id, message); };
|
||||
if (queue)
|
||||
queue(dispatch);
|
||||
else
|
||||
dispatch();
|
||||
}
|
||||
|
||||
std::function<void(const std::string&, const std::string&)> OrcaPrinterAgent::make_lan_message_handler(uint64_t generation)
|
||||
{
|
||||
return [this, generation](const std::string& id, const std::string& payload) {
|
||||
if (generation == m_lan_generation.load())
|
||||
deliver_to_local_sink(id, payload);
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation
|
||||
<< " current_generation=" << m_lan_generation.load()
|
||||
<< " dev_id=" << id;
|
||||
};
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
|
||||
{
|
||||
@@ -20,30 +364,18 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
m_cloud_agent = cloud;
|
||||
m_orca_cloud = dynamic_cast<OrcaCloudServiceAgent*>(cloud.get());
|
||||
}
|
||||
if (!m_orca_cloud) {
|
||||
if (!get_orca_cloud_agent()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_cloud_agent: cloud is not OrcaCloudServiceAgent";
|
||||
return; // BBL provider active - nothing to bridge
|
||||
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";
|
||||
});
|
||||
const int callback_result = get_orca_cloud_agent()->set_printer_status_callback(
|
||||
[this](std::string dev_id, std::string payload) { deliver_to_sink(dev_id, payload); });
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result;
|
||||
}
|
||||
|
||||
@@ -51,73 +383,270 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
|
||||
// Communication - All Stubs
|
||||
// ============================================================================
|
||||
|
||||
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*/)
|
||||
{ return route_send(/*is_lan=*/false, dev_id, json_str); }
|
||||
|
||||
bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port)
|
||||
{
|
||||
(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;
|
||||
std::string s = dev_ip;
|
||||
if (s.rfind("http://", 0) == 0)
|
||||
s.erase(0, 7);
|
||||
else if (s.rfind("https://", 0) == 0)
|
||||
s.erase(0, 8);
|
||||
if (const auto slash = s.find('/'); slash != std::string::npos)
|
||||
s.erase(slash);
|
||||
if (s.empty())
|
||||
return false;
|
||||
port = "8280";
|
||||
// split a trailing :port only for host:port, not an unbracketed IPv6 literal
|
||||
if (const auto colon = s.rfind(':'); colon != std::string::npos && s.find(']') == std::string::npos) {
|
||||
port = s.substr(colon + 1);
|
||||
s.erase(colon);
|
||||
}
|
||||
if (s.empty() || port.empty())
|
||||
return false;
|
||||
host = s;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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();
|
||||
std::string OrcaPrinterAgent::make_lan_client_id(const std::string& dev_id)
|
||||
{
|
||||
static const std::string suffix = [] {
|
||||
std::random_device rd;
|
||||
char buf[9];
|
||||
std::snprintf(buf, sizeof(buf), "%08x", static_cast<unsigned>(rd()));
|
||||
return std::string(buf);
|
||||
}();
|
||||
return "orcaslicer-lan-" + dev_id + "-" + suffix;
|
||||
}
|
||||
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
std::string OrcaPrinterAgent::lan_connection_target() const
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
return m_lan_url;
|
||||
}
|
||||
|
||||
std::string OrcaPrinterAgent::seq(int n) { return std::to_string(20000 + (n % 10000)); }
|
||||
|
||||
std::string OrcaPrinterAgent::build_pushing_start(const std::string& sid)
|
||||
{ return R"({"pushing":{"command":"start","sequence_id":")" + sid + R"("}})"; }
|
||||
std::string OrcaPrinterAgent::build_pushing_stop(const std::string& sid)
|
||||
{ return R"({"pushing":{"command":"stop","sequence_id":")" + sid + R"("}})"; }
|
||||
std::string OrcaPrinterAgent::build_pushall(const std::string& sid)
|
||||
{ return R"({"pushing":{"command":"pushall","sequence_id":")" + sid + R"(","version":1,"push_target":1}})"; }
|
||||
std::string OrcaPrinterAgent::build_get_version(const std::string& sid)
|
||||
{ return R"({"info":{"command":"get_version","sequence_id":")" + sid + R"("}})"; }
|
||||
std::string OrcaPrinterAgent::build_get_capabilities(const std::string& sid)
|
||||
{ return R"({"info":{"command":"get_capabilities","sequence_id":")" + sid + R"("}})"; }
|
||||
|
||||
void OrcaPrinterAgent::emit_connect_sequence(const std::string& dev_id,
|
||||
std::function<void(const std::string&)> subscribe,
|
||||
std::function<void(const std::string&)> request)
|
||||
{
|
||||
subscribe(dev_id);
|
||||
request(build_pushing_start(seq(1)));
|
||||
request(build_pushall(seq(2)));
|
||||
request(build_get_version(seq(3)));
|
||||
request(build_get_capabilities(seq(4)));
|
||||
}
|
||||
|
||||
void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation)
|
||||
{
|
||||
// Called from both connect paths with whichever epoch that path captured; the two
|
||||
// counters are independent, so matching either one means the caller is still live.
|
||||
if (!conn || (generation != m_lan_generation.load() && generation != m_cloud_generation.load()))
|
||||
return;
|
||||
emit_connect_sequence(
|
||||
dev_id, [conn](const std::string& id) { conn->subscribe(id); },
|
||||
[conn, dev_id](const std::string& body) { conn->send_request(dev_id, body); }); // dev_id captured BY VALUE
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: connect_printer requested dev_id=" << dev_id
|
||||
<< " dev_ip=" << dev_ip << " username=" << (username.empty() ? "<default>" : username)
|
||||
<< " password_present=" << (!password.empty()) << " use_ssl=" << use_ssl;
|
||||
(void) use_ssl; // OrcaSonar LAN is plaintext ws://
|
||||
if (dev_id.empty() || dev_ip.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip";
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
std::string host, port;
|
||||
if (!parse_lan_endpoint(dev_ip, host, port)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected unparsable LAN endpoint dev_ip=" << dev_ip;
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
}
|
||||
disconnect_printer();
|
||||
const uint64_t gen = ++m_lan_generation;
|
||||
|
||||
OrcaMqttConnection::Config cfg;
|
||||
cfg.url = "ws://" + host + ":" + port + "/mqtt";
|
||||
cfg.use_tls = false;
|
||||
cfg.username = username.empty() ? std::string("orcasonar") : username;
|
||||
cfg.password = password;
|
||||
cfg.client_id = make_lan_client_id(dev_id);
|
||||
cfg.keepalive_seconds = 60;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen
|
||||
<< " host=" << host << " port=" << port << " url=" << cfg.url
|
||||
<< " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty())
|
||||
<< " client_id=" << cfg.client_id;
|
||||
|
||||
OrcaMqttConnection* conn = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
m_lan_dev_id = dev_id;
|
||||
m_lan_url = cfg.url;
|
||||
m_current_connection = LAN;
|
||||
lan_mqtt_connection = std::make_unique<OrcaMqttConnection>();
|
||||
conn = lan_mqtt_connection.get();
|
||||
}
|
||||
|
||||
if (m_lan_connect_thread.joinable())
|
||||
m_lan_connect_thread.join(); // disconnect_printer() above already stopped the old conn, so this is fast
|
||||
m_lan_connect_thread = std::thread([this, conn, cfg, dev_id, gen] {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen
|
||||
<< " dev_id=" << dev_id << " url=" << cfg.url;
|
||||
if (gen != m_lan_generation.load()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker abandoned before start generation=" << gen
|
||||
<< " current_generation=" << m_lan_generation.load();
|
||||
return; // superseded before we ran: never raise a socket nobody will tear down
|
||||
}
|
||||
const bool ok = conn->start(cfg, make_lan_message_handler(gen), [this, gen, dev_id, conn](bool connected, bool initial) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT state callback connected=" << connected
|
||||
<< " initial=" << initial << " generation=" << gen
|
||||
<< " current_generation=" << m_lan_generation.load()
|
||||
<< " connack_rc=" << conn->last_connack_rc();
|
||||
if (gen != m_lan_generation.load()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring stale LAN MQTT state callback generation=" << gen;
|
||||
return;
|
||||
}
|
||||
if (connected && !initial) {
|
||||
on_connected(dev_id, conn, gen);
|
||||
dispatch_local_connect(ConnectStatusOk, dev_id, "0");
|
||||
} else if (!connected && !initial) {
|
||||
dispatch_local_connect(ConnectStatusLost, dev_id, "connection_lost");
|
||||
}
|
||||
});
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok
|
||||
<< " generation=" << gen << " current_generation=" << m_lan_generation.load()
|
||||
<< " connected=" << conn->is_connected() << " running=" << conn->is_running()
|
||||
<< " connack_rc=" << conn->last_connack_rc();
|
||||
if (ok && gen == m_lan_generation.load()) {
|
||||
on_connected(dev_id, conn, gen);
|
||||
dispatch_local_connect(ConnectStatusOk, dev_id, "0");
|
||||
} else if (!ok && gen == m_lan_generation.load() && !conn->is_running()) {
|
||||
// A refusal with rc 4/5 terminates the transport. Network errors keep
|
||||
// retrying in OrcaMqttConnection, so leave the UI in its connecting state.
|
||||
const int rc = conn->last_connack_rc();
|
||||
const std::string reason = rc >= 0 ? std::to_string(rc) : "initial_connect_failed";
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT connection terminated before readiness"
|
||||
<< " generation=" << gen << " connack_rc=" << rc
|
||||
<< " reason=" << reason;
|
||||
dispatch_local_connect(ConnectStatusFailed, dev_id, reason);
|
||||
} else if (!ok && gen == m_lan_generation.load()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT initial attempt failed but worker is retrying"
|
||||
<< " generation=" << gen << " connack_rc=" << conn->last_connack_rc();
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker exiting generation=" << gen
|
||||
<< " current_generation=" << m_lan_generation.load();
|
||||
});
|
||||
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::disconnect_printer()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: disconnect_printer requested";
|
||||
++m_lan_generation; // fence stale worker callbacks
|
||||
std::unique_ptr<OrcaMqttConnection> doomed;
|
||||
std::string prev_dev;
|
||||
{
|
||||
std::lock_guard<std::mutex> l(state_mutex);
|
||||
doomed = std::move(lan_mqtt_connection);
|
||||
prev_dev = m_lan_dev_id;
|
||||
m_lan_dev_id.clear();
|
||||
if (m_current_connection == LAN)
|
||||
m_current_connection = NONE;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load()
|
||||
<< " previous_dev_id=" << prev_dev << " had_connection=" << (doomed ? "yes" : "no")
|
||||
<< " connected=" << (doomed && doomed->is_connected() ? "yes" : "no");
|
||||
// Tell the printer to stop pushing and drop the report topic before the socket
|
||||
// goes away (§3.2/§5.4: deselect issues pushing.stop on both transports).
|
||||
if (doomed && !prev_dev.empty() && doomed->is_connected()) {
|
||||
doomed->send_request(prev_dev, build_pushing_stop(seq(5)));
|
||||
doomed->unsubscribe(prev_dev);
|
||||
}
|
||||
if (doomed)
|
||||
doomed->stop(); // joins the OrcaMqttConnection worker; OUTSIDE state_mutex
|
||||
if (m_lan_connect_thread.joinable())
|
||||
m_lan_connect_thread.join(); // start() has returned (doomed->stop above); the thread's raw conn ptr
|
||||
// is still valid here because `doomed` is not destroyed until we return
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag)
|
||||
int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/)
|
||||
{ return route_send(/*is_lan=*/true, dev_id, json_str); }
|
||||
|
||||
int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id
|
||||
<< " payload_bytes=" << json_str.size();
|
||||
if (dev_id.empty())
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
|
||||
if (!conn)
|
||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||
return conn->send_request(dev_id, json_str) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Certificates - All Stubs
|
||||
// ============================================================================
|
||||
|
||||
int OrcaPrinterAgent::check_cert()
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
int OrcaPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only)
|
||||
{
|
||||
}
|
||||
void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) {}
|
||||
|
||||
// ============================================================================
|
||||
// Discovery - Stub
|
||||
// Discovery
|
||||
// ============================================================================
|
||||
|
||||
bool OrcaPrinterAgent::start_discovery(bool start, bool sending)
|
||||
bool OrcaPrinterAgent::start_discovery(bool start, bool /*sending*/)
|
||||
{
|
||||
if (start) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (!m_discovery) {
|
||||
m_discovery = std::make_unique<OrcaSonarDiscovery>([this](const std::string& machine_alive) {
|
||||
OnMsgArrivedFn ssdp_fn;
|
||||
QueueOnMainFn queue_fn;
|
||||
{
|
||||
std::lock_guard<std::mutex> callback_lock(state_mutex);
|
||||
ssdp_fn = on_ssdp_msg_fn;
|
||||
queue_fn = queue_on_main_fn;
|
||||
}
|
||||
if (!ssdp_fn)
|
||||
return;
|
||||
if (queue_fn)
|
||||
queue_fn([ssdp_fn, machine_alive] { ssdp_fn(machine_alive); });
|
||||
else
|
||||
ssdp_fn(machine_alive);
|
||||
});
|
||||
}
|
||||
m_discovery->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The discovery thread invokes the callback, which takes state_mutex. Move the
|
||||
// owner out first, then join without holding that mutex.
|
||||
std::unique_ptr<OrcaSonarDiscovery> discovery;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
discovery = std::move(m_discovery);
|
||||
}
|
||||
if (discovery)
|
||||
discovery->stop();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -125,26 +654,20 @@ bool OrcaPrinterAgent::start_discovery(bool start, bool sending)
|
||||
// Binding - All Stubs
|
||||
// ============================================================================
|
||||
|
||||
int OrcaPrinterAgent::ping_bind(std::string ping_code)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
int OrcaPrinterAgent::ping_bind(std::string ping_code) { return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::bind(
|
||||
std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
int OrcaPrinterAgent::bind(std::string dev_ip,
|
||||
std::string dev_id,
|
||||
std::string dev_model,
|
||||
std::string sec_link,
|
||||
std::string timezone,
|
||||
bool improved,
|
||||
OnUpdateStatusFn update_fn)
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::unbind(std::string dev_id)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
int OrcaPrinterAgent::unbind(std::string dev_id) { return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::request_bind_ticket(std::string* ticket)
|
||||
{
|
||||
@@ -181,7 +704,7 @@ std::string OrcaPrinterAgent::get_user_selected_machine()
|
||||
|
||||
int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||
{
|
||||
std::shared_ptr<ICloudServiceAgent> cloud;
|
||||
auto* cloud = get_orca_cloud_agent();
|
||||
std::string previous;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
@@ -191,97 +714,73 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
|
||||
}
|
||||
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>");
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id
|
||||
<< " cloud=" << (cloud ? "set" : "<null>");
|
||||
if (!cloud) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no cloud agent";
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca 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;
|
||||
}
|
||||
// Bump ONCE at the top for any change (select or deselect) so a deselect also
|
||||
// fences an in-flight configure thread started by the previous selection. This is
|
||||
// the CLOUD epoch only — a cloud selection must not fence a live LAN session.
|
||||
const uint64_t gen = ++m_cloud_generation;
|
||||
|
||||
auto* conn = cloud->get_mqtt_connection();
|
||||
if (!previous.empty() && conn && conn->is_connected())
|
||||
conn->send_request(previous, build_pushing_stop(seq(5)));
|
||||
cloud->teardown_selected_printer_mqtt();
|
||||
|
||||
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;
|
||||
m_current_connection = dev_id.empty() ? NONE : CLOUD;
|
||||
}
|
||||
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);
|
||||
|
||||
if (m_cloud_connect_thread.joinable())
|
||||
m_cloud_connect_thread.join(); // teardown_selected_printer_mqtt() above stopped the old cloud conn
|
||||
if (!dev_id.empty()) {
|
||||
m_cloud_connect_thread = std::thread([this, cloud, dev_id, gen] {
|
||||
if (gen != m_cloud_generation.load())
|
||||
return; // superseded before we ran: do not raise a socket nobody tears down
|
||||
if (cloud->configure_selected_printer_mqtt(dev_id) == BAMBU_NETWORK_SUCCESS && gen == m_cloud_generation.load())
|
||||
on_connected(dev_id, cloud->get_mqtt_connection(), gen);
|
||||
});
|
||||
} else {
|
||||
// Deselect: the joined thread may have raised a fresh socket between the
|
||||
// teardown above and the join. stop() is not sticky, so tear down again.
|
||||
cloud->teardown_selected_printer_mqtt();
|
||||
}
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Agent Information
|
||||
// ============================================================================
|
||||
AgentInfo OrcaPrinterAgent::get_agent_info_static()
|
||||
{
|
||||
return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"};
|
||||
}
|
||||
{ return AgentInfo{ORCA_PRINTER_AGENT_ID, "Orca", OrcaPrinterAgent_VERSION, "Orca Printer Communication Protocol Agent"}; }
|
||||
|
||||
// ============================================================================
|
||||
// Print Job Operations - All Stubs
|
||||
// ============================================================================
|
||||
|
||||
int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::start_local_print_with_record(PrintParams params,
|
||||
int OrcaPrinterAgent::start_local_print_with_record(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn)
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
{ return BAMBU_NETWORK_SUCCESS; }
|
||||
|
||||
// ============================================================================
|
||||
// Callback Registration
|
||||
@@ -327,6 +826,7 @@ int OrcaPrinterAgent::set_on_local_connect_fn(OnLocalConnectedFn fn)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
on_local_connect_fn = fn;
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_local_connect_fn: callback=" << (fn ? "set" : "clear");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -334,6 +834,7 @@ int OrcaPrinterAgent::set_on_local_message_fn(OnMessageFn fn)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
on_local_message_fn = fn;
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_on_local_message_fn: callback=" << (fn ? "set" : "clear");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "OrcaMqttConnection.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -79,16 +85,95 @@ public:
|
||||
int set_on_local_message_fn(OnMessageFn fn) override;
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) 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; }
|
||||
|
||||
protected:
|
||||
// Forward one inbound printer message to on_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);
|
||||
|
||||
// LAN has a separate callback in the existing IPrinterAgent contract because the
|
||||
// GUI parses local reports with the "lan" dialect and looks up local machines.
|
||||
void deliver_to_local_sink(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;
|
||||
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);
|
||||
enum CurrentConn {
|
||||
NONE,
|
||||
CLOUD,
|
||||
LAN
|
||||
};
|
||||
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
|
||||
|
||||
OrcaCloudServiceAgent* get_orca_cloud_agent();
|
||||
|
||||
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
|
||||
|
||||
// Route one command payload to device/<dev_id>/request on the LAN or the cloud
|
||||
// per-printer 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;
|
||||
|
||||
Reference in New Issue
Block a user