fix: LAN paths and camera stream

This commit is contained in:
Ian Chua
2026-09-04 16:18:43 +08:00
parent 972031cf06
commit 2f82cfe40f
7 changed files with 575 additions and 35 deletions

View File

@@ -3046,6 +3046,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
} }
} catch (...) {} } 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 { try {
if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) { if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) {
ptr->ParseFirmwareSwitch(j); 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<bool>();
};
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<int>();
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) { static bool is_hex_digit(char c) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0; return std::isxdigit(static_cast<unsigned char>(c)) != 0;
} }

View File

@@ -947,6 +947,7 @@ public:
/*for parse new info*/ /*for parse new info*/
bool check_enable_np(const json& print) const; bool check_enable_np(const json& print) const;
void parse_new_info(json print); 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; 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; 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; int get_flag_bits(int num, int start, int count = 1, int base = 10) const;

View File

@@ -52,10 +52,13 @@ void WebMediaController::Play()
"refreshCameraFrame();" "refreshCameraFrame();"
"setInterval(refreshCameraFrame,200);" "setInterval(refreshCameraFrame,200);"
"</script></body></html>"; "</script></body></html>";
} else {
html += " src=\"" + url + "\"></body></html>";
}
m_webview->SetPage(html, url); m_webview->SetPage(html, url);
} else {
// Load MJPEG streams as the top-level document. Some embedded WebView
// backends buffer a multipart stream when it is used as an <img> resource,
// which introduces noticeable live-view latency.
m_webview->LoadURL(url);
}
} }
void WebMediaController::Stop() void WebMediaController::Stop()

View File

@@ -125,6 +125,9 @@ void OrcaMqttConnection::stop() {
{ {
std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> lock(mutex);
connected = false; connected = false;
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
if (!initial_completed) { if (!initial_completed) {
initial_completed = true; initial_completed = true;
initial_result = false; initial_result = false;
@@ -173,6 +176,11 @@ bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
} }
{ {
std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> 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); subscriptions.insert(topic);
pending_unsubscriptions.erase(topic); pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic); pending_subscriptions.insert(topic);
@@ -190,8 +198,21 @@ bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
{ {
std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> lock(mutex);
subscriptions.erase(topic); subscriptions.erase(topic);
acknowledged_subscriptions.erase(topic);
pending_subscriptions.erase(topic); pending_subscriptions.erase(topic);
pending_unsubscriptions.insert(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 BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic
<< " total_subscriptions=" << subscriptions.size() << " total_subscriptions=" << subscriptions.size()
<< " connected=" << connected.load(); << " connected=" << connected.load();
@@ -207,6 +228,9 @@ void OrcaMqttConnection::clear_subscriptions() {
subscriptions.clear(); subscriptions.clear();
pending_subscriptions.clear(); pending_subscriptions.clear();
pending_unsubscriptions.clear(); pending_unsubscriptions.clear();
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
} }
bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) { 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; << " dev_id=" << dev_id;
return false; return false;
} }
const std::string report = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
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 { try {
// ws_write() serialises the write via write_mutex; do not lock it here. // 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 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"; 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<std::mutex> lock(mutex);
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
}
notify_state(true); notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions"; 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<std::mutex> lock(mutex); std::lock_guard<std::mutex> lock(mutex);
topics.assign(subscriptions.begin(), subscriptions.end()); 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(); BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << topics.size();
for (const std::string& topic : topics) { for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++; const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << 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)); 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) { for (const std::string& topic : subscribe_topics) {
const uint16_t packet_id = next_packet_id++; const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic
<< " packet_id=" << packet_id; << " packet_id=" << packet_id;
ws_write(conn, make_subscribe_packet(packet_id, topic, 1)); 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(); << " bytes=" << packet.size();
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status. if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) { if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
std::ostringstream result_codes; std::ostringstream result_codes;
for (size_t index = 4; index < packet.size(); ++index) { for (size_t index = 4; index < packet.size(); ++index) {
if (index != 4) if (index != 4)
@@ -624,9 +681,51 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index])); 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=" BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id="
<< ((static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) | << packet_id
static_cast<unsigned int>(static_cast<uint8_t>(packet[3])))
<< " result_codes=" << result_codes.str(); << " 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<uint8_t>(packet[4]);
std::string topic;
std::deque<std::pair<std::string, std::string>> requests;
{
std::lock_guard<std::mutex> lock(mutex);
auto pending = pending_subscribe_packets.find(packet_id);
if (pending != pending_subscribe_packets.end()) {
topic = pending->second;
pending_subscribe_packets.erase(pending);
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (report_topic(it->first) == topic) {
requests.push_back(std::move(*it));
it = pending_requests.erase(it);
} else {
++it;
}
}
if (result == 0 || result == 1) {
acknowledged_subscriptions.insert(topic);
}
}
}
if (topic.empty()) {
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<unsigned int>(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<unsigned int>(result) << std::dec
<< " dropped_requests=" << requests.size();
}
} }
return; return;
} }

View File

@@ -7,13 +7,15 @@
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include <set> #include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility>
#include <vector> #include <vector>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -130,6 +132,12 @@ private:
std::set<std::string> subscriptions; std::set<std::string> subscriptions;
std::set<std::string> pending_subscriptions; std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions; std::set<std::string> pending_unsubscriptions;
// Requests for a subscribed device wait until the corresponding SUBACK is
// received. Otherwise an immediate pushall response can be published by
// the broker before this client is actually subscribed to the report topic.
std::set<std::string> acknowledged_subscriptions;
std::map<uint16_t, std::string> pending_subscribe_packets;
std::deque<std::pair<std::string, std::string>> pending_requests;
std::atomic<uint16_t> next_packet_id{1}; std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1}; std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence

View File

@@ -1,4 +1,5 @@
#include "OrcaPrinterAgent.hpp" #include "OrcaPrinterAgent.hpp"
#include "IPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp" #include "NetworkAgentFactory.hpp"
#include "OrcaCloudServiceAgent.hpp" #include "OrcaCloudServiceAgent.hpp"
#include <algorithm> #include <algorithm>
@@ -9,6 +10,8 @@
#include <cctype> #include <cctype>
#include <cstdio> #include <cstdio>
#include <condition_variable> #include <condition_variable>
#include <cmath>
#include <limits>
#include <mutex> #include <mutex>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <random> #include <random>
@@ -213,6 +216,16 @@ private:
OrcaPrinterAgent::OrcaPrinterAgent(std::string log_dir) : log_dir(std::move(log_dir)) {} 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() OrcaPrinterAgent::~OrcaPrinterAgent()
{ {
start_discovery(false, false); 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) void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const std::string& payload)
{ {
parse_ipcam_info(dev_id, payload);
OnMessageFn fn; OnMessageFn fn;
QueueOnMainFn q; QueueOnMainFn q;
{ {
@@ -380,12 +395,141 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
} }
// ============================================================================ // ============================================================================
// Communication - All Stubs // Communication
// ============================================================================ // ============================================================================
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); } { 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<char>(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) bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port)
{ {
std::string s = dev_ip; 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; 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<int>::max())
return false;
result = static_cast<int>(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<std::string>() == "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<int>() == 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<std::string>();
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<std::string>();
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<std::mutex> 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<int>(m_camera_stream_mode)
<< " url=" << m_camera_url;
}
std::string OrcaPrinterAgent::lan_connection_target() const std::string OrcaPrinterAgent::lan_connection_target() const
{ {
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> 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; << " client_id=" << cfg.client_id;
OrcaMqttConnection* conn = nullptr; OrcaMqttConnection* conn = nullptr;
CurrentConn previous_connection;
{ {
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> l(state_mutex);
previous_connection = m_current_connection;
m_lan_dev_id = dev_id; m_lan_dev_id = dev_id;
m_lan_url = cfg.url; m_lan_url = cfg.url;
m_camera_stream_mode = CameraStreamMode::none;
m_camera_url.clear();
m_current_connection = LAN; m_current_connection = LAN;
lan_mqtt_connection = std::make_unique<OrcaMqttConnection>(); lan_mqtt_connection = std::make_unique<OrcaMqttConnection>();
conn = lan_mqtt_connection.get(); 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()) 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.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 ++m_lan_generation; // fence stale worker callbacks
std::unique_ptr<OrcaMqttConnection> doomed; std::unique_ptr<OrcaMqttConnection> doomed;
std::string prev_dev; std::string prev_dev;
CurrentConn previous_connection;
CurrentConn current_connection;
{ {
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> l(state_mutex);
previous_connection = m_current_connection;
doomed = std::move(lan_mqtt_connection); doomed = std::move(lan_mqtt_connection);
prev_dev = m_lan_dev_id; prev_dev = m_lan_dev_id;
m_lan_dev_id.clear(); m_lan_dev_id.clear();
if (m_current_connection == LAN) if (m_current_connection == LAN) {
m_current_connection = NONE; 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() BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN disconnect generation=" << m_lan_generation.load()
<< " previous_dev_id=" << prev_dev << " had_connection=" << (doomed ? "yes" : "no") << " 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 // 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). // goes away (§3.2/§5.4: deselect issues pushing.stop on both transports).
if (doomed && !prev_dev.empty() && doomed->is_connected()) { 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) int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str)
{ {
std::string command = "<unparsed>";
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<std::string>();
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 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()) if (dev_id.empty())
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan); OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
if (!conn) if (!conn)
return BAMBU_NETWORK_ERR_INVALID_HANDLE; 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(); auto* cloud = get_orca_cloud_agent();
std::string previous; std::string previous;
CurrentConn previous_connection;
CurrentConn current_connection;
{ {
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
if (dev_id == selected_machine) { previous_connection = m_current_connection;
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id; // 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; return BAMBU_NETWORK_SUCCESS;
} }
previous = selected_machine; previous = selected_machine;
selected_machine = dev_id; 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 BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: previous=" << previous << " new=" << dev_id
<< " cloud=" << (cloud ? "set" : "<null>"); << " cloud=" << (cloud ? "set" : "<null>") << " transport="
<< connection_type_name(previous_connection) << "->"
<< connection_type_name(current_connection);
if (!cloud) { if (!cloud) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent"; BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::set_user_selected_machine: no Orca cloud agent";
return BAMBU_NETWORK_SUCCESS; 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))); conn->send_request(previous, build_pushing_stop(seq(5)));
cloud->teardown_selected_printer_mqtt(); cloud->teardown_selected_printer_mqtt();
{
std::lock_guard<std::mutex> lock(state_mutex);
m_current_connection = dev_id.empty() ? NONE : CLOUD;
}
if (m_cloud_connect_thread.joinable()) if (m_cloud_connect_thread.joinable())
m_cloud_connect_thread.join(); // teardown_selected_printer_mqtt() above stopped the old cloud conn m_cloud_connect_thread.join(); // teardown_selected_printer_mqtt() above stopped the old cloud conn
if (!dev_id.empty()) { if (!dev_id.empty()) {
@@ -845,4 +1133,20 @@ int OrcaPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
CameraStreamMode OrcaPrinterAgent::get_camera_stream_mode() const
{
std::lock_guard<std::mutex> 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<std::mutex> lock(state_mutex);
if (m_current_connection != LAN)
return {};
return m_camera_url;
}
} // namespace Slic3r } // namespace Slic3r

View File

@@ -18,12 +18,13 @@ namespace Slic3r {
class OrcaCloudServiceAgent; class OrcaCloudServiceAgent;
/** /**
* OrcaPrinterAgent - Stub implementation for printer operations. * OrcaPrinterAgent - OrcaSonar MQTT printer agent.
* *
* All printer-related operations are currently stubs that return success. * LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* Actual printer connectivity requires the BBL SDK or future Orca implementation. * MQTT connection selected by route_send() differs.
*/ */
class OrcaPrinterAgent : public IPrinterAgent { class OrcaPrinterAgent : public IPrinterAgent
{
public: public:
explicit OrcaPrinterAgent(std::string log_dir); explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override; ~OrcaPrinterAgent() override;
@@ -50,12 +51,21 @@ public:
// Binding // Binding
int ping_bind(std::string ping_code) override; int ping_bind(std::string ping_code) override;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) 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 unbind(std::string dev_id) override;
int request_bind_ticket(std::string* ticket) override; int request_bind_ticket(std::string* ticket) override;
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override; int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override; int set_server_callback(OnServerErrFn fn) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Machine Selection // Machine Selection
std::string get_user_selected_machine() override; std::string get_user_selected_machine() override;
int set_user_selected_machine(std::string dev_id) override; int set_user_selected_machine(std::string dev_id) override;
@@ -85,8 +95,28 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn 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). // Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id) { void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {}); emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
} }
@@ -100,6 +130,10 @@ protected:
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler. // 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); 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 // 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. // 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); void deliver_to_local_sink(const std::string& dev_id, const std::string& payload);
@@ -140,11 +174,12 @@ private:
std::string log_dir; std::string log_dir;
std::string selected_machine; std::string selected_machine;
enum CurrentConn { enum CurrentConn { NONE, CLOUD, LAN };
NONE, static const char* connection_type_name(CurrentConn connection);
CLOUD,
LAN // 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; CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
@@ -166,10 +201,13 @@ private:
std::string m_lan_dev_id; // guarded by state_mutex 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_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(); OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true); OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the cloud // 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. // per-printer connection. The uniform send path for both send_message* overrides.