From 2f82cfe40feb38c54a2616e9e667ac3ecdf6765a Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 4 Sep 2026 16:18:43 +0800 Subject: [PATCH] fix: LAN paths and camera stream --- src/slic3r/GUI/DeviceManager.cpp | 87 +++++++ src/slic3r/GUI/DeviceManager.hpp | 1 + src/slic3r/GUI/WebMediaController.cpp | 7 +- src/slic3r/Utils/OrcaMqttConnection.cpp | 103 +++++++- src/slic3r/Utils/OrcaMqttConnection.hpp | 10 +- src/slic3r/Utils/OrcaPrinterAgent.cpp | 330 +++++++++++++++++++++++- src/slic3r/Utils/OrcaPrinterAgent.hpp | 72 ++++-- 7 files changed, 575 insertions(+), 35 deletions(-) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 741ed788fc..6c155fe158 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3046,6 +3046,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } } catch (...) {} + try { + if (j.contains("info")) + parse_new_info2(j["info"]); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "parse_json: failed to parse OrcaSonar capability info"; + } + try { if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) { ptr->ParseFirmwareSwitch(j); @@ -5432,6 +5439,86 @@ void MachineObject::parse_new_info(json print) } } +void MachineObject::parse_new_info2(const json& info) +{ + if (!info.is_object() || info.value("command", "") != "get_capabilities") + return; + + const auto capabilities_it = info.find("capabilities"); + if (capabilities_it == info.end() || !capabilities_it->is_object()) + return; + const auto flags_it = capabilities_it->find("flags"); + if (flags_it == capabilities_it->end() || !flags_it->is_object()) + return; + + const json& flags = *flags_it; + BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump(); + + auto parse_bool = [&flags](const char* name, bool& target) { + const auto it = flags.find(name); + if (it != flags.end() && it->is_boolean()) + target = it->get(); + }; + + parse_bool("support_send_to_sd", is_support_send_to_sdcard); + parse_bool("support_filament_backup", is_support_filament_backup); + parse_bool("support_update_remain", is_support_update_remain); + parse_bool("support_auto_recovery_step_loss", is_support_auto_recovery_step_loss); + parse_bool("support_ams_humidity", is_support_ams_humidity); + parse_bool("support_prompt_sound", is_support_prompt_sound); + parse_bool("support_filament_tangle_detect", is_support_filament_tangle_detect); + parse_bool("support_1080dpi", is_support_1080dpi); + parse_bool("support_cloud_print_only", is_support_cloud_print_only); + parse_bool("support_command_ams_switch", is_support_command_ams_switch); + parse_bool("support_mqtt_alive", is_support_mqtt_alive); + parse_bool("support_motor_noise_cali", is_support_motor_noise_cali); + parse_bool("support_timelapse", is_support_timelapse); + parse_bool("support_user_preset", is_support_user_preset); + parse_bool("support_refresh_nozzle", is_support_refresh_nozzle); + parse_bool("support_flow_calibration", is_support_flow_calibration); + parse_bool("support_build_plate_marker_detect", is_support_build_plate_marker_detect); + parse_bool("support_nozzle_blob_detect", is_support_nozzle_blob_detection); + + if (!m_manager->IsMultiMachineEnabled() && !is_support_agora) + parse_bool("support_tunnel_mqtt", is_support_tunnel_mqtt); + + const auto bed_leveling_it = flags.find("support_bed_leveling"); + if (bed_leveling_it != flags.end() && bed_leveling_it->is_number_integer()) + is_support_bed_leveling = bed_leveling_it->get(); + + auto copy_bool = [&flags](json& target, const char* name) { + const auto it = flags.find(name); + if (it != flags.end() && it->is_boolean()) + target[name] = *it; + }; + + // The capability manifest uses an object for this range, while the legacy + // DeviceCore parser consumes a boolean plus a two-element range array. + json device_config; + copy_bool(device_config, "support_chamber"); + copy_bool(device_config, "support_first_layer_inspect"); + copy_bool(device_config, "support_ai_monitoring"); + copy_bool(device_config, "support_lidar_calibration"); + const auto chamber_edit_it = flags.find("support_chamber_temp_edit"); + if (chamber_edit_it != flags.end() && chamber_edit_it->is_boolean()) { + device_config["support_chamber_temp_edit"] = *chamber_edit_it; + } else if (chamber_edit_it != flags.end() && chamber_edit_it->is_object()) { + const auto min_it = chamber_edit_it->find("min"); + const auto max_it = chamber_edit_it->find("max"); + if (min_it != chamber_edit_it->end() && max_it != chamber_edit_it->end() && min_it->is_number() && max_it->is_number()) { + device_config["support_chamber_temp_edit"] = true; + device_config["support_chamber_temp_edit_range"] = {*min_it, *max_it}; + } + } + + json fan_config; + copy_bool(fan_config, "support_aux_fan"); + copy_bool(fan_config, "support_chamber_fan"); + + m_config->ParseConfig(device_config); + m_fan->ParseV2_0(fan_config); +} + static bool is_hex_digit(char c) { return std::isxdigit(static_cast(c)) != 0; } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 8788c2288d..6e4e7d7183 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -947,6 +947,7 @@ public: /*for parse new info*/ bool check_enable_np(const json& print) const; void parse_new_info(json print); + void parse_new_info2(const json& info); int get_flag_bits(std::string str, int start, int count = 1) const; uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const; int get_flag_bits(int num, int start, int count = 1, int base = 10) const; diff --git a/src/slic3r/GUI/WebMediaController.cpp b/src/slic3r/GUI/WebMediaController.cpp index 13d820c98f..378d83b650 100644 --- a/src/slic3r/GUI/WebMediaController.cpp +++ b/src/slic3r/GUI/WebMediaController.cpp @@ -52,10 +52,13 @@ void WebMediaController::Play() "refreshCameraFrame();" "setInterval(refreshCameraFrame,200);" ""; + m_webview->SetPage(html, url); } else { - html += " src=\"" + url + "\">"; + // Load MJPEG streams as the top-level document. Some embedded WebView + // backends buffer a multipart stream when it is used as an resource, + // which introduces noticeable live-view latency. + m_webview->LoadURL(url); } - m_webview->SetPage(html, url); } void WebMediaController::Stop() diff --git a/src/slic3r/Utils/OrcaMqttConnection.cpp b/src/slic3r/Utils/OrcaMqttConnection.cpp index 5ba0279fc2..2173231310 100644 --- a/src/slic3r/Utils/OrcaMqttConnection.cpp +++ b/src/slic3r/Utils/OrcaMqttConnection.cpp @@ -125,6 +125,9 @@ void OrcaMqttConnection::stop() { { std::lock_guard lock(mutex); connected = false; + acknowledged_subscriptions.clear(); + pending_subscribe_packets.clear(); + pending_requests.clear(); if (!initial_completed) { initial_completed = true; initial_result = false; @@ -173,6 +176,11 @@ bool OrcaMqttConnection::subscribe(const std::string& dev_id) { } { std::lock_guard lock(mutex); + if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe already queued or active topic=" << topic + << " acknowledged=" << (acknowledged_subscriptions.count(topic) != 0); + return true; + } subscriptions.insert(topic); pending_unsubscriptions.erase(topic); pending_subscriptions.insert(topic); @@ -190,8 +198,21 @@ bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) { { std::lock_guard lock(mutex); subscriptions.erase(topic); + acknowledged_subscriptions.erase(topic); pending_subscriptions.erase(topic); pending_unsubscriptions.insert(topic); + for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) { + if (it->second == topic) + it = pending_subscribe_packets.erase(it); + else + ++it; + } + for (auto it = pending_requests.begin(); it != pending_requests.end();) { + if (it->first == dev_id) + it = pending_requests.erase(it); + else + ++it; + } BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic << " total_subscriptions=" << subscriptions.size() << " connected=" << connected.load(); @@ -207,6 +228,9 @@ void OrcaMqttConnection::clear_subscriptions() { subscriptions.clear(); pending_subscriptions.clear(); pending_unsubscriptions.clear(); + acknowledged_subscriptions.clear(); + pending_subscribe_packets.clear(); + pending_requests.clear(); } bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) { @@ -450,6 +474,17 @@ bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::stri << " dev_id=" << dev_id; return false; } + const std::string report = report_topic(dev_id); + { + std::lock_guard lock(mutex); + if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) { + pending_requests.emplace_back(dev_id, payload); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT request queued until SUBACK" + << " dev_id=" << dev_id << " payload_bytes=" << payload.size() + << " pending_requests=" << pending_requests.size(); + return true; + } + } 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 @@ -531,6 +566,14 @@ void OrcaMqttConnection::connect_and_read() { } m_connection_stage = "reading MQTT messages"; + // The subscription acknowledgement belongs to this MQTT session. Clear + // the previous session's state before notifying the owner, because the + // reconnect callback immediately queues the printer's initial requests. + { + std::lock_guard lock(mutex); + acknowledged_subscriptions.clear(); + pending_subscribe_packets.clear(); + } 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"; @@ -572,10 +615,18 @@ void OrcaMqttConnection::send_current_subscriptions(Connection& conn) { { std::lock_guard lock(mutex); topics.assign(subscriptions.begin(), subscriptions.end()); + acknowledged_subscriptions.clear(); + pending_subscribe_packets.clear(); + for (const std::string& topic : topics) + pending_subscriptions.erase(topic); } 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++; + { + std::lock_guard lock(mutex); + pending_subscribe_packets[packet_id] = topic; + } BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << packet_id; ws_write(conn, make_subscribe_packet(packet_id, topic, 1)); } @@ -593,6 +644,10 @@ void OrcaMqttConnection::send_pending_subscriptions(Connection& conn) { } for (const std::string& topic : subscribe_topics) { const uint16_t packet_id = next_packet_id++; + { + std::lock_guard lock(mutex); + pending_subscribe_packets[packet_id] = topic; + } 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)); @@ -617,6 +672,8 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) { << " bytes=" << packet.size(); if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status. if (packet_type == 9 && packet.size() >= 5) { + const uint16_t packet_id = (static_cast(static_cast(packet[2])) << 8) | + static_cast(static_cast(packet[3])); std::ostringstream result_codes; for (size_t index = 4; index < packet.size(); ++index) { if (index != 4) @@ -624,9 +681,51 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) { result_codes << "0x" << std::hex << static_cast(static_cast(packet[index])); } BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id=" - << ((static_cast(static_cast(packet[2])) << 8) | - static_cast(static_cast(packet[3]))) + << packet_id << " result_codes=" << result_codes.str(); + + // Each production SUBSCRIBE packet currently contains one topic. + // MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80 + // means the subscription was rejected. + const uint8_t result = static_cast(packet[4]); + std::string topic; + std::deque> requests; + { + std::lock_guard lock(mutex); + auto pending = pending_subscribe_packets.find(packet_id); + if (pending != pending_subscribe_packets.end()) { + topic = pending->second; + pending_subscribe_packets.erase(pending); + for (auto it = pending_requests.begin(); it != pending_requests.end();) { + if (report_topic(it->first) == topic) { + requests.push_back(std::move(*it)); + it = pending_requests.erase(it); + } else { + ++it; + } + } + if (result == 0 || result == 1) { + acknowledged_subscriptions.insert(topic); + } + } + } + if (topic.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: SUBACK has no pending topic packet_id=" << packet_id; + } else if (result == 0 || result == 1) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: report subscription active topic=" << topic + << " granted_qos=" << static_cast(result) + << " releasing_requests=" << requests.size(); + for (const auto& request : requests) { + if (!send_request(request.first, request.second)) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: queued MQTT request could not be sent" + << " after SUBACK dev_id=" << request.first; + } + } + } else { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: report subscription rejected topic=" << topic + << " result_code=0x" << std::hex << static_cast(result) << std::dec + << " dropped_requests=" << requests.size(); + } } return; } diff --git a/src/slic3r/Utils/OrcaMqttConnection.hpp b/src/slic3r/Utils/OrcaMqttConnection.hpp index cac1892a68..6131ee86f1 100644 --- a/src/slic3r/Utils/OrcaMqttConnection.hpp +++ b/src/slic3r/Utils/OrcaMqttConnection.hpp @@ -7,13 +7,15 @@ #include #include #include +#include #include +#include #include #include -#include #include #include #include +#include #include #include #include @@ -130,6 +132,12 @@ private: std::set subscriptions; std::set pending_subscriptions; std::set pending_unsubscriptions; + // Requests for a subscribed device wait until the corresponding SUBACK is + // received. Otherwise an immediate pushall response can be published by + // the broker before this client is actually subscribed to the report topic. + std::set acknowledged_subscriptions; + std::map pending_subscribe_packets; + std::deque> pending_requests; std::atomic next_packet_id{1}; std::atomic m_last_connack_rc{-1}; uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 0b2c38ae2a..6c7fed7eab 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,4 +1,5 @@ #include "OrcaPrinterAgent.hpp" +#include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" #include "OrcaCloudServiceAgent.hpp" #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -213,6 +216,16 @@ private: OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) {} +const char* OrcaPrinterAgent::connection_type_name(CurrentConn connection) +{ + switch (connection) { + case LAN: return "LAN"; + case CLOUD: return "cloud"; + case NONE: return "none"; + } + return "unknown"; +} + OrcaPrinterAgent::~OrcaPrinterAgent() { start_discovery(false, false); @@ -300,6 +313,8 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const std::string& payload) { + parse_ipcam_info(dev_id, payload); + OnMessageFn fn; QueueOnMainFn q; { @@ -380,12 +395,141 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr cloud } // ============================================================================ -// Communication - All Stubs +// Communication // ============================================================================ 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); } +int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + int tray_number = 0; + if (!parse_nonnegative_command_id(tray_id, tray_number)) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid RFID tray id=" << tray_id; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + nlohmann::json j; + j["print"]["command"] = "ams_get_rfid"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + j["print"]["tray_id"] = tray_number; + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_ams_calibrate(std::string /*dev_id*/, int /*ams_id*/, int /*sequence_id*/, bool /*lan_mode*/) +{ + // OrcaSonar has no ams_calibrate command. Do not send the Bambu M620 C + // dialect through the vendor-neutral OrcaSonar gcode_line command. + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: AMS calibration is not part of the OrcaSonar API"; + return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; +} + +int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + int tray_number = 0; + if (!parse_nonnegative_command_id(tray_id, tray_number)) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid AMS target tray id=" << tray_id; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + nlohmann::json j; + j["print"]["command"] = "ams_change_filament"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + j["print"]["target"] = tray_number; + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_start_camera(std::string /*dev_id*/) +{ + // OrcaSonar exposes camera.ipcam_* controls, not the legacy start_camera + // operation used by the Bambu agent. + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: camera start is not part of the OrcaSonar API"; + return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; +} + +int OrcaPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) +{ + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = "G90\n"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) +{ + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = "G29\n"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, + int sequence_id, bool lan_mode) +{ + nlohmann::json j; + j["print"]["sequence_id"] = std::to_string(sequence_id); + if (supports_mqtt_homing) { + j["print"]["command"] = "back_to_center"; + } else { + // Preserve the existing safety behavior: never home Z/Y during a print. + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = is_printing ? "G28 X\n" : "G28\n"; + } + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, + int sequence_id, bool lan_mode) +{ + nlohmann::json j; + j["print"]["command"] = "set_bed_temp"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + j["print"]["temp"] = temp; + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) +{ + nlohmann::json j; + j["print"]["command"] = "set_nozzle_temp"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + j["print"]["extruder_index"] = 0; + j["print"]["target_temp"] = temp; + return route_send(lan_mode, dev_id, j.dump()); +} + +int OrcaPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, + int /*speed*/, bool is_core_xy, bool /*supports_mqtt_axis_control*/, + int sequence_id, bool lan_mode) +{ + std::transform(axis.begin(), axis.end(), axis.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + if (axis != "X" && axis != "Y" && axis != "Z" && axis != "E") { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control axis=" << axis; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + const double requested_distance = input_val * unit; + if (!std::isfinite(requested_distance) || requested_distance == 0.0) { + BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val + << " unit=" << unit; + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + } + + int direction = requested_distance > 0.0 ? 1 : -1; + if (!is_core_xy && (axis == "Y" || axis == "Z")) + direction = -direction; + + nlohmann::json j; + j["print"]["command"] = "xyz_ctrl"; + j["print"]["sequence_id"] = std::to_string(sequence_id); + j["print"]["axis"] = axis; + j["print"]["dir"] = direction; + j["print"]["distance"] = requested_distance < 0.0 ? -requested_distance : requested_distance; + return route_send(lan_mode, dev_id, j.dump()); +} + bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port) { std::string s = dev_ip; @@ -420,6 +564,97 @@ std::string OrcaPrinterAgent::make_lan_client_id(const std::string& dev_id) return "orcaslicer-lan-" + dev_id + "-" + suffix; } +bool OrcaPrinterAgent::parse_nonnegative_command_id(const std::string& value, int& result) +{ + if (value.empty()) + return false; + try { + std::size_t consumed = 0; + const long long parsed = std::stoll(value, &consumed); + if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits::max()) + return false; + result = static_cast(parsed); + return true; + } catch (const std::exception&) { + return false; + } +} + +void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::string& payload) +{ + const nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false); + if (!envelope.is_object()) + return; + + const auto print_it = envelope.find("print"); + if (print_it == envelope.end() || !print_it->is_object()) + return; + + const nlohmann::json& print = *print_it; + const auto command_it = print.find("command"); + const bool is_push_status = command_it != print.end() && command_it->is_string() && command_it->get() == "push_status"; + bool is_full_snapshot = false; + if (is_push_status) { + const auto msg_it = print.find("msg"); + is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get() == 0); + } + + CameraStreamMode stream_mode = CameraStreamMode::none; + std::string stream_url; + bool has_camera_update = false; + const auto ipcam_it = print.find("ipcam"); + if (ipcam_it != print.end() && ipcam_it->is_object()) { + const auto stream_modes_it = ipcam_it->find("stream_mode"); + if (stream_modes_it != ipcam_it->end() && stream_modes_it->is_array()) { + has_camera_update = true; + for (const auto& stream : *stream_modes_it) { + if (!stream.is_object()) + continue; + const auto mode_it = stream.find("mode"); + const auto url_it = stream.find("url"); + if (mode_it == stream.end() || url_it == stream.end() || !mode_it->is_string() || !url_it->is_string()) + continue; + + const std::string mode = mode_it->get(); + if (mode == "rtsp") + stream_mode = CameraStreamMode::rtsp; + else if (mode == "http") + stream_mode = CameraStreamMode::http; + else if (mode == "http_snapshot") + stream_mode = CameraStreamMode::http_snapshot; + else + continue; + + stream_url = url_it->get(); + break; // OrcaSonar orders entries by preference. + } + } + else if (is_full_snapshot) { + has_camera_update = true; + } + } else if (is_full_snapshot) { + // A full push_status without ipcam means the printer has no camera + // stream information. Diff reports omit unchanged domains. + has_camera_update = true; + } + + if (!has_camera_update) + return; + + std::lock_guard lock(state_mutex); + if (m_current_connection != LAN || m_lan_dev_id != dev_id) { + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring camera update for inactive LAN printer dev_id=" << dev_id; + return; + } + + m_camera_stream_mode = stream_mode; + m_camera_url = std::move(stream_url); + BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id + << " transport=LAN" + << " mode=" << static_cast(m_camera_stream_mode) + << " url=" << m_camera_url; +} + std::string OrcaPrinterAgent::lan_connection_target() const { std::lock_guard l(state_mutex); @@ -493,14 +728,20 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st << " client_id=" << cfg.client_id; OrcaMqttConnection* conn = nullptr; + CurrentConn previous_connection; { std::lock_guard l(state_mutex); + previous_connection = m_current_connection; m_lan_dev_id = dev_id; m_lan_url = cfg.url; + m_camera_stream_mode = CameraStreamMode::none; + m_camera_url.clear(); m_current_connection = LAN; lan_mqtt_connection = std::make_unique(); conn = lan_mqtt_connection.get(); } + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: selected LAN printer dev_id=" << dev_id + << " transport=" << connection_type_name(previous_connection) << "->LAN"; if (m_lan_connect_thread.joinable()) m_lan_connect_thread.join(); // disconnect_printer() above already stopped the old conn, so this is fast @@ -561,17 +802,26 @@ int OrcaPrinterAgent::disconnect_printer() ++m_lan_generation; // fence stale worker callbacks std::unique_ptr doomed; std::string prev_dev; + CurrentConn previous_connection; + CurrentConn current_connection; { std::lock_guard l(state_mutex); + previous_connection = m_current_connection; doomed = std::move(lan_mqtt_connection); prev_dev = m_lan_dev_id; m_lan_dev_id.clear(); - if (m_current_connection == LAN) + if (m_current_connection == LAN) { m_current_connection = NONE; + m_camera_stream_mode = CameraStreamMode::none; + m_camera_url.clear(); + } + current_connection = m_current_connection; } 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"); + << " connected=" << (doomed && doomed->is_connected() ? "yes" : "no") + << " transport=" << connection_type_name(previous_connection) << "->" + << connection_type_name(current_connection); // 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()) { @@ -591,14 +841,34 @@ int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string js int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str) { + std::string command = ""; + try { + const nlohmann::json envelope = nlohmann::json::parse(json_str); + for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) { + const auto namespace_it = envelope.find(namespace_name); + if (namespace_it != envelope.end() && namespace_it->is_object()) { + const auto command_it = namespace_it->find("command"); + if (command_it != namespace_it->end() && command_it->is_string()) { + command = std::string(namespace_name) + "." + command_it->get(); + break; + } + } + } + } catch (const std::exception&) { + // Preserve the transport's existing behavior for malformed payloads; + // the printer will report the protocol error asynchronously. + } BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send is_lan=" << is_lan << " dev_id=" << dev_id - << " payload_bytes=" << json_str.size(); + << " command=" << command << " 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; + const bool queued = conn->send_request(dev_id, json_str); + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued + << " is_lan=" << is_lan << " dev_id=" << dev_id; + return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } // ============================================================================ @@ -706,17 +976,40 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) { auto* cloud = get_orca_cloud_agent(); std::string previous; + CurrentConn previous_connection; + CurrentConn current_connection; { std::lock_guard lock(state_mutex); - if (dev_id == selected_machine) { - BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id; + previous_connection = m_current_connection; + // An empty cloud selection must not clear an independently active LAN + // selection. Conversely, selecting a cloud machine with the same id + // while LAN is active is still a transport switch and must proceed. + const bool same_selection = dev_id == selected_machine; + const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD; + if (same_selection && same_transport) { + BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id + << " transport=" << connection_type_name(m_current_connection); return BAMBU_NETWORK_SUCCESS; } previous = selected_machine; selected_machine = dev_id; + if (dev_id.empty()) { + if (m_current_connection == CLOUD) { + m_current_connection = NONE; + m_camera_stream_mode = CameraStreamMode::none; + m_camera_url.clear(); + } + } else { + m_current_connection = CLOUD; + m_camera_stream_mode = CameraStreamMode::none; + m_camera_url.clear(); + } + current_connection = m_current_connection; } BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id - << " cloud=" << (cloud ? "set" : ""); + << " cloud=" << (cloud ? "set" : "") << " transport=" + << connection_type_name(previous_connection) << "->" + << connection_type_name(current_connection); if (!cloud) { BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; return BAMBU_NETWORK_SUCCESS; @@ -732,11 +1025,6 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) conn->send_request(previous, build_pushing_stop(seq(5))); cloud->teardown_selected_printer_mqtt(); - { - std::lock_guard lock(state_mutex); - m_current_connection = dev_id.empty() ? NONE : CLOUD; - } - 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()) { @@ -845,4 +1133,20 @@ int OrcaPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn) return BAMBU_NETWORK_SUCCESS; } +CameraStreamMode OrcaPrinterAgent::get_camera_stream_mode() const +{ + std::lock_guard lock(state_mutex); + if (m_current_connection == CLOUD) + return CameraStreamMode::webrtc; + return m_camera_stream_mode; +} + +std::string OrcaPrinterAgent::get_camera_url() const +{ + std::lock_guard lock(state_mutex); + if (m_current_connection != LAN) + return {}; + return m_camera_url; +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index 666fd26fe3..ea7128ff5a 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -18,12 +18,13 @@ namespace Slic3r { class OrcaCloudServiceAgent; /** - * OrcaPrinterAgent - Stub implementation for printer operations. + * OrcaPrinterAgent - OrcaSonar MQTT printer agent. * - * All printer-related operations are currently stubs that return success. - * Actual printer connectivity requires the BBL SDK or future Orca implementation. + * LAN and cloud commands use the same OrcaSonar protocol payloads; only the + * MQTT connection selected by route_send() differs. */ -class OrcaPrinterAgent : public IPrinterAgent { +class OrcaPrinterAgent : public IPrinterAgent +{ public: explicit OrcaPrinterAgent(std::string log_dir); ~OrcaPrinterAgent() override; @@ -50,12 +51,21 @@ public: // Binding int ping_bind(std::string ping_code) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - int 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) override; + int 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) override; int unbind(std::string dev_id) override; int request_bind_ticket(std::string* ticket) override; int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; + CameraStreamMode get_camera_stream_mode() const override; + std::string get_camera_url() const override; + // Machine Selection std::string get_user_selected_machine() override; int set_user_selected_machine(std::string dev_id) override; @@ -85,9 +95,29 @@ public: int set_on_local_message_fn(OnMessageFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override; + int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; + int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override; + int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override; + int command_start_camera(std::string dev_id) override; + int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override; + int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override; + int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override; + int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override; + int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override; + int command_axis_control(std::string dev_id, + std::string axis, + double unit, + double input_val, + int speed, + bool is_core_xy, + bool supports_mqtt_axis_control, + int sequence_id, + bool lan_mode) override; + // Test-only: drive emit_connect_sequence directly (no socket). - void run_connect_sequence_for_test(const std::string& dev_id) { - emit_connect_sequence(dev_id, [](const std::string&){}, [](const std::string&){}); + 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. @@ -100,6 +130,10 @@ protected: // 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); + // Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they + // are forwarded to the GUI. The getters below then read this agent-owned state. + void parse_ipcam_info(const std::string& dev_id, const std::string& payload); + // 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); @@ -114,10 +148,10 @@ protected: std::function 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 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; + 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); @@ -127,7 +161,7 @@ protected: virtual void emit_connect_sequence(const std::string& dev_id, std::function subscribe, std::function request); - static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band + 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); @@ -140,11 +174,12 @@ private: std::string log_dir; std::string selected_machine; - enum CurrentConn { - NONE, - CLOUD, - LAN - }; + enum CurrentConn { NONE, CLOUD, LAN }; + static const char* connection_type_name(CurrentConn connection); + + // The transport for the printer currently selected by the UI. LAN and + // cloud sessions have separate connection objects, so this is selection + // state rather than an inference from whichever socket happens to exist. CurrentConn m_current_connection = NONE; std::shared_ptr m_cloud_agent; @@ -164,12 +199,15 @@ private: std::unique_ptr 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 + 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 + CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex + std::string m_camera_url; // guarded by state_mutex OrcaCloudServiceAgent* get_orca_cloud_agent(); OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true); + static bool parse_nonnegative_command_id(const std::string& value, int& result); // Route one command payload to device//request on the LAN or the cloud // per-printer connection. The uniform send path for both send_message* overrides.