feat: LAN impl for Orca Printer Agent

This commit is contained in:
Ian Chua
2026-09-08 19:13:48 +08:00
parent a4376c77c3
commit 01c553bad9
2 changed files with 244 additions and 149 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ DevStorage::SdcardState Slic3r::DevStorage::set_sdcard_state(int state)
if (system) if (system)
{ {
try { try {
if (print_json.contains("sdcard")) { if (print_json.contains("sdcard") || print_json.contains("support_send_to_sd")) {
if (print_json["sdcard"].get<bool>()) if (print_json["sdcard"].get<bool>() || print_json["support_send_to_sd"].get<bool>())
system->m_sdcard_state = DevStorage::SdcardState::HAS_SDCARD_NORMAL; system->m_sdcard_state = DevStorage::SdcardState::HAS_SDCARD_NORMAL;
else else
system->m_sdcard_state = DevStorage::SdcardState::NO_SDCARD; system->m_sdcard_state = DevStorage::SdcardState::NO_SDCARD;
+242 -147
View File
@@ -22,10 +22,14 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <random> #include <random>
#include <set> #include <set>
#include <sstream>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
namespace Slic3r { namespace Slic3r {
const std::string OrcaPrinterAgent_VERSION = "0.0.1"; const std::string OrcaPrinterAgent_VERSION = "0.0.1";
@@ -56,19 +60,15 @@ std::string resolve_local_gcode_path(const PrintParams& params)
// SDCARD_PRINT_FILE splits its argument on whitespace, so collapse anything unsafe. // SDCARD_PRINT_FILE splits its argument on whitespace, so collapse anything unsafe.
std::string remote_gcode_name(const PrintParams& params) std::string remote_gcode_name(const PrintParams& params)
{ {
std::string name = params.project_name.empty() std::string name = params.project_name.empty() ? fs::path(resolve_local_gcode_path(params)).filename().string() :
? fs::path(resolve_local_gcode_path(params)).filename().string() fs::path(params.project_name).filename().string();
: fs::path(params.project_name).filename().string();
if (boost::iends_with(name, ".3mf")) // "model.gcode.3mf" -> "model.gcode" if (boost::iends_with(name, ".3mf")) // "model.gcode.3mf" -> "model.gcode"
name.erase(name.size() - 4); name.erase(name.size() - 4);
std::replace_if( std::replace_if(
name.begin(), name.end(), name.begin(), name.end(),
[](unsigned char c) { [](unsigned char c) { return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\'; }, '_');
return std::isspace(c) != 0 || c == ';' || c == '#' || c == '*' || c == '/' || c == '\\';
},
'_');
if (name.empty()) if (name.empty())
name = "orca_print"; name = "orca_print";
@@ -109,66 +109,154 @@ std::string next_gcode_file_sequence_id()
return std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); return std::to_string(counter.fetch_add(1, std::memory_order_relaxed));
} }
// Ask an OrcaSonar instance at host:port for its MQTT device id. Every command static constexpr const char* ORCASONAR_FALLBACK = "orcasonar";
// topic is keyed on it (device/<id>/request), so a manual "connect by IP" has to
// learn it from the printer instead of inventing one from the address. OrcaSonar's bool fetch_orcasonar_body(const std::string& url, std::string& body)
// landing page (GET /) returns "OrcaSonar running\ndevice_id=<id>\nmqtt=<addr>\n";
// /upnp/device.xml carries the same id as <UDN>uuid:<id></UDN> and is the fallback.
bool probe_orcasonar_device_id(const std::string& host, const std::string& port, std::string& device_id)
{ {
const std::string origin = "http://" + host + ":" + port; bool ok = false;
Http::get(url)
.timeout_connect(4)
.timeout_max(6)
.on_complete([&](std::string b, unsigned status) {
if (status == 200) {
body = std::move(b);
ok = true;
}
})
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url << " failed status=" << status << " err=" << err;
})
.perform_sync();
return ok;
}
auto fetch = [](const std::string& url, std::string& body) { std::string extract_line_value(const std::string& body, const std::string& key)
bool ok = false; {
Http::get(url) const auto pos = body.find(key);
.timeout_connect(4) if (pos == std::string::npos)
.timeout_max(6) return {};
.on_complete([&](std::string b, unsigned status) { const auto value_start = pos + key.size();
if (status == 200) { const auto value_end = body.find('\n', value_start);
body = std::move(b); std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start);
ok = true; boost::trim(value);
} return value;
}) }
.on_error([&](std::string, std::string err, unsigned status) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: identity probe " << url
<< " failed status=" << status << " err=" << err;
})
.perform_sync();
return ok;
};
auto extract = [](const std::string& body, const std::string& start_token, char end_char) -> std::string { // Manual binding uses the OrcaSonar landing page as its sole identity source.
const auto pos = body.find(start_token); // The page returns device_id and may return device_name/model_id.
if (pos == std::string::npos) bool probe_orcasonar_landing_page(const std::string& host,
return {}; const std::string& port,
const auto value_start = pos + start_token.size(); std::string& device_id,
const auto value_end = body.find(end_char, value_start); std::string& device_name,
std::string value = body.substr(value_start, value_end == std::string::npos ? std::string::npos : value_end - value_start); std::string& model_id)
boost::trim(value); {
return value; device_name = ORCASONAR_FALLBACK;
}; model_id = ORCASONAR_FALLBACK;
std::string body; std::string body;
if (fetch(origin + "/", body)) { if (fetch_orcasonar_body("http://" + host + ":" + port + "/", body)) {
std::string id = extract(body, "device_id=", '\n'); device_id = extract_line_value(body, "device_id=");
if (!id.empty()) { const std::string name = extract_line_value(body, "device_name=");
device_id = std::move(id); const std::string model = extract_line_value(body, "model_id=");
if (!name.empty())
device_name = name;
if (!model.empty())
model_id = model;
if (!device_id.empty())
return true; return true;
}
}
body.clear();
if (fetch(origin + "/upnp/device.xml", body)) {
std::string id = extract(body, "uuid:", '<');
if (!id.empty()) {
device_id = std::move(id);
return true;
}
} }
return false; return false;
} }
// SSDP discovery uses the LOCATION URL's UPnP device description as its sole
// identity source. OrcaSonar maps device_id/device_name/model_id to UDN,
// friendlyName, and modelNumber respectively.
bool parse_orcasonar_device_xml(const std::string& body,
std::string& device_id,
std::string& device_name,
std::string& model_id)
{
device_name = ORCASONAR_FALLBACK;
model_id = ORCASONAR_FALLBACK;
try {
boost::property_tree::ptree tree;
std::istringstream stream(body);
boost::property_tree::read_xml(stream, tree, boost::property_tree::xml_parser::trim_whitespace);
const auto device = tree.get_child_optional("root.device");
if (!device)
return false;
std::string udn = device->get<std::string>("UDN", "");
boost::trim(udn);
if (boost::istarts_with(udn, "uuid:"))
device_id = udn.substr(5);
else
device_id = device->get<std::string>("device_id", "");
boost::trim(device_id);
if (device_id.empty())
return false;
device_name = device->get<std::string>("friendlyName", "");
if (device_name.empty())
device_name = device->get<std::string>("device_name", ORCASONAR_FALLBACK);
model_id = device->get<std::string>("modelNumber", "");
if (model_id.empty())
model_id = device->get<std::string>("model_id", ORCASONAR_FALLBACK);
boost::trim(device_name);
boost::trim(model_id);
if (device_name.empty())
device_name = ORCASONAR_FALLBACK;
if (model_id.empty())
model_id = ORCASONAR_FALLBACK;
return true;
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: failed to parse OrcaSonar device.xml: " << error.what();
return false;
}
}
bool probe_orcasonar_device_xml(const std::string& location,
std::string& device_id,
std::string& device_name,
std::string& model_id)
{
std::string body;
return fetch_orcasonar_body(location, body) && parse_orcasonar_device_xml(body, device_id, device_name, model_id);
}
// TEMP MOCK: OrcaSonar's push_status doesn't report storage state yet, so
// DevStorage::ParseV1_0() decodes NO_SDCARD and SelectMachineDialog blocks
// printing with "No SD card". Force the storage-present markers into every
// forwarded status document so the printer reports HAS_SDCARD_NORMAL.
// Remove once the firmware reports real storage state.
std::string force_sdcard_present(const std::string& payload)
{
nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false);
if (!envelope.is_object())
return payload;
const auto print_it = envelope.find("print");
if (print_it == envelope.end() || !print_it->is_object())
return payload;
// DevStorage::ParseV1_0() reads print.sdcard as a bool -> HAS_SDCARD_NORMAL.
(*print_it)["sdcard"] = true;
// MachineObject::parse_home_flag() runs afterwards and re-derives the state
// from bits 8-9 of print.home_flag; rewrite them to 01 so it doesn't clobber
// the mock back to NO_SDCARD.
const auto home_flag_it = print_it->find("home_flag");
if (home_flag_it != print_it->end() && home_flag_it->is_number_integer()) {
int flag = home_flag_it->get<int>();
flag = (flag & ~(0x3 << 8)) | (0x1 << 8);
*home_flag_it = flag;
}
return envelope.dump();
}
} // namespace } // namespace
class OrcaPrinterAgent::OrcaSonarDiscovery class OrcaPrinterAgent::OrcaSonarDiscovery
@@ -246,8 +334,7 @@ private:
const std::size_t type_start = lower_usn.find(device_type, uuid_prefix.size()); const std::size_t type_start = lower_usn.find(device_type, uuid_prefix.size());
if (type_start == std::string::npos) if (type_start == std::string::npos)
return false; return false;
const std::string device_id = trim_ascii(usn.substr(uuid_prefix.size(), type_start - uuid_prefix.size())); if (host.empty())
if (device_id.empty() || host.empty())
return false; return false;
const std::string lower_location = lower_ascii(location); const std::string lower_location = lower_ascii(location);
@@ -274,17 +361,34 @@ private:
if (port.empty() || port.find_first_not_of("0123456789") != std::string::npos) if (port.empty() || port.find_first_not_of("0123456789") != std::string::npos)
return false; return false;
// SSDP LOCATION commonly advertises the device's mDNS name (for
// example, http://orcasonar-123.local:8280/upnp/device.xml). The
// discovery response already gives us the sender's reachable address,
// so use that address for the HTTP probe instead of requiring the
// platform HTTP client to resolve .local. Keep the advertised path so
// this remains compatible with non-default device-description URLs.
const std::string location_path = authority_end == std::string::npos ? "/" : location.substr(authority_end);
const std::string probe_host = host.find(':') == std::string::npos ? host : "[" + host + "]";
const std::string probe_url = location.substr(0, scheme_end + 3) + probe_host + ":" + port + location_path;
std::string device_id;
std::string device_name;
std::string model_id;
if (!probe_orcasonar_device_xml(probe_url, device_id, device_name, model_id))
return false;
nlohmann::json machine; nlohmann::json machine;
machine["dev_name"] = device_id; machine["dev_name"] = device_name;
machine["dev_id"] = device_id; machine["dev_id"] = device_id;
machine["dev_type"] = model_id;
machine["connection_name"] = device_id;
machine["dev_ip"] = host + ":" + port; machine["dev_ip"] = host + ":" + port;
machine["dev_type"] = "orcasonar";
machine["dev_signal"] = "0"; machine["dev_signal"] = "0";
machine["connect_type"] = "lan"; machine["connect_type"] = "lan";
machine["bind_state"] = "free"; machine["bind_state"] = "free";
machine["sec_link"] = "secure"; machine["sec_link"] = "secure";
machine["ssdp_version"] = "v1"; machine["ssdp_version"] = "v1";
machine["connection_name"] = device_id;
json = machine.dump(); json = machine.dump();
return true; return true;
} }
@@ -443,10 +547,8 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str
fn = on_message_fn; fn = on_message_fn;
q = queue_on_main_fn; q = queue_on_main_fn;
} }
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id << " payload_bytes=" << payload.size()
<< " payload_bytes=" << payload.size() << " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
<< " callback=" << (fn ? "set" : "null")
<< " queue_on_main=" << (q ? "set" : "null");
if (!fn) { if (!fn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set" BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set"
<< " dev_id=" << dev_id; << " dev_id=" << dev_id;
@@ -469,10 +571,8 @@ void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const st
fn = on_local_message_fn; fn = on_local_message_fn;
q = queue_on_main_fn; q = queue_on_main_fn;
} }
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering LAN message dev_id=" << dev_id BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering LAN message dev_id=" << dev_id << " payload_bytes=" << payload.size()
<< " payload_bytes=" << payload.size() << " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
<< " callback=" << (fn ? "set" : "null")
<< " queue_on_main=" << (q ? "set" : "null");
if (!fn) { if (!fn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping LAN message because on_local_message_fn is not set" BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping LAN message because on_local_message_fn is not set"
<< " dev_id=" << dev_id; << " dev_id=" << dev_id;
@@ -494,10 +594,8 @@ void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_
queue = queue_on_main_fn; queue = queue_on_main_fn;
} }
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message
<< " dev_id=" << dev_id << " message=" << message << " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null");
<< " callback=" << (callback ? "set" : "null")
<< " queue_on_main=" << (queue ? "set" : "null");
if (!callback) if (!callback)
return; return;
@@ -515,8 +613,7 @@ std::function<void(const std::string&, const std::string&)> OrcaPrinterAgent::ma
deliver_to_local_sink(id, payload); deliver_to_local_sink(id, payload);
else else
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation
<< " current_generation=" << m_lan_generation.load() << " current_generation=" << m_lan_generation.load() << " dev_id=" << id;
<< " dev_id=" << id;
}; };
} }
@@ -612,8 +709,7 @@ int OrcaPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id,
return route_send(lan_mode, dev_id, j.dump()); 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 OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
int sequence_id, bool lan_mode)
{ {
nlohmann::json j; nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id); j["print"]["sequence_id"] = std::to_string(sequence_id);
@@ -627,8 +723,7 @@ int OrcaPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool
return route_send(lan_mode, dev_id, j.dump()); 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 OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, int sequence_id, bool lan_mode)
int sequence_id, bool lan_mode)
{ {
nlohmann::json j; nlohmann::json j;
j["print"]["command"] = "set_bed_temp"; j["print"]["command"] = "set_bed_temp";
@@ -647,9 +742,15 @@ int OrcaPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int seque
return route_send(lan_mode, dev_id, j.dump()); 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 OrcaPrinterAgent::command_axis_control(std::string dev_id,
int /*speed*/, bool is_core_xy, bool /*supports_mqtt_axis_control*/, std::string axis,
int sequence_id, bool lan_mode) 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)); }); 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") { if (axis != "X" && axis != "Y" && axis != "Z" && axis != "E") {
@@ -659,8 +760,7 @@ int OrcaPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
const double requested_distance = input_val * unit; const double requested_distance = input_val * unit;
if (!std::isfinite(requested_distance) || requested_distance == 0.0) { if (!std::isfinite(requested_distance) || requested_distance == 0.0) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid axis control distance input=" << input_val << " unit=" << unit;
<< " unit=" << unit;
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
} }
@@ -716,7 +816,7 @@ bool OrcaPrinterAgent::parse_nonnegative_command_id(const std::string& value, in
if (value.empty()) if (value.empty())
return false; return false;
try { try {
std::size_t consumed = 0; std::size_t consumed = 0;
const long long parsed = std::stoll(value, &consumed); const long long parsed = std::stoll(value, &consumed);
if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits<int>::max()) if (consumed != value.size() || parsed < 0 || parsed > std::numeric_limits<int>::max())
return false; return false;
@@ -738,18 +838,18 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st
return; return;
const nlohmann::json& print = *print_it; const nlohmann::json& print = *print_it;
const auto command_it = print.find("command"); 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"; 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; bool is_full_snapshot = false;
if (is_push_status) { if (is_push_status) {
const auto msg_it = print.find("msg"); const auto msg_it = print.find("msg");
is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get<int>() == 0); is_full_snapshot = msg_it == print.end() || (msg_it->is_number_integer() && msg_it->get<int>() == 0);
} }
CameraStreamMode stream_mode = CameraStreamMode::none; CameraStreamMode stream_mode = CameraStreamMode::none;
std::string stream_url; std::string stream_url;
bool has_camera_update = false; bool has_camera_update = false;
const auto ipcam_it = print.find("ipcam"); const auto ipcam_it = print.find("ipcam");
if (ipcam_it != print.end() && ipcam_it->is_object()) { if (ipcam_it != print.end() && ipcam_it->is_object()) {
const auto stream_modes_it = ipcam_it->find("stream_mode"); const auto stream_modes_it = ipcam_it->find("stream_mode");
if (stream_modes_it != ipcam_it->end() && stream_modes_it->is_array()) { if (stream_modes_it != ipcam_it->end() && stream_modes_it->is_array()) {
@@ -758,7 +858,7 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st
if (!stream.is_object()) if (!stream.is_object())
continue; continue;
const auto mode_it = stream.find("mode"); const auto mode_it = stream.find("mode");
const auto url_it = stream.find("url"); const auto url_it = stream.find("url");
if (mode_it == stream.end() || url_it == stream.end() || !mode_it->is_string() || !url_it->is_string()) if (mode_it == stream.end() || url_it == stream.end() || !mode_it->is_string() || !url_it->is_string())
continue; continue;
@@ -775,8 +875,7 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st
stream_url = url_it->get<std::string>(); stream_url = url_it->get<std::string>();
break; // OrcaSonar orders entries by preference. break; // OrcaSonar orders entries by preference.
} }
} } else if (is_full_snapshot) {
else if (is_full_snapshot) {
has_camera_update = true; has_camera_update = true;
} }
} else if (is_full_snapshot) { } else if (is_full_snapshot) {
@@ -795,11 +894,9 @@ void OrcaPrinterAgent::parse_ipcam_info(const std::string& dev_id, const std::st
} }
m_camera_stream_mode = stream_mode; m_camera_stream_mode = stream_mode;
m_camera_url = std::move(stream_url); m_camera_url = std::move(stream_url);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: updated camera state dev_id=" << dev_id << " transport=LAN"
<< " transport=LAN" << " mode=" << static_cast<int>(m_camera_stream_mode) << " url=" << m_camera_url;
<< " 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
@@ -845,9 +942,9 @@ void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnectio
int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) 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 BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: connect_printer requested dev_id=" << dev_id << " dev_ip=" << dev_ip
<< " dev_ip=" << dev_ip << " username=" << (username.empty() ? "<default>" : username) << " username=" << (username.empty() ? "<default>" : username) << " password_present=" << (!password.empty())
<< " password_present=" << (!password.empty()) << " use_ssl=" << use_ssl; << " use_ssl=" << use_ssl;
(void) use_ssl; // OrcaSonar LAN is plaintext ws:// (void) use_ssl; // OrcaSonar LAN is plaintext ws://
if (dev_id.empty() || dev_ip.empty()) { if (dev_id.empty() || dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip"; BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: connect_printer rejected missing dev_id or dev_ip";
@@ -869,16 +966,15 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st
cfg.client_id = make_lan_client_id(dev_id); cfg.client_id = make_lan_client_id(dev_id);
cfg.keepalive_seconds = 60; cfg.keepalive_seconds = 60;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection prepared generation=" << gen << " host=" << host << " port=" << port
<< " host=" << host << " port=" << port << " url=" << cfg.url << " url=" << cfg.url << " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty())
<< " mqtt_username=" << cfg.username << " password_present=" << (!cfg.password.empty())
<< " client_id=" << cfg.client_id; << " client_id=" << cfg.client_id;
OrcaMqttConnection* conn = nullptr; OrcaMqttConnection* conn = nullptr;
CurrentConn previous_connection; CurrentConn previous_connection;
{ {
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> l(state_mutex);
previous_connection = m_current_connection; 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_stream_mode = CameraStreamMode::none;
@@ -893,17 +989,16 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st
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
m_lan_connect_thread = std::thread([this, conn, cfg, dev_id, gen] { m_lan_connect_thread = std::thread([this, conn, cfg, dev_id, gen] {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker started generation=" << gen << " dev_id=" << dev_id
<< " dev_id=" << dev_id << " url=" << cfg.url; << " url=" << cfg.url;
if (gen != m_lan_generation.load()) { if (gen != m_lan_generation.load()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker abandoned before start generation=" << gen BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connect worker abandoned before start generation=" << gen
<< " current_generation=" << m_lan_generation.load(); << " current_generation=" << m_lan_generation.load();
return; // superseded before we ran: never raise a socket nobody will tear down 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) { 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 BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT state callback connected=" << connected << " initial=" << initial
<< " initial=" << initial << " generation=" << gen << " generation=" << gen << " current_generation=" << m_lan_generation.load()
<< " current_generation=" << m_lan_generation.load()
<< " connack_rc=" << conn->last_connack_rc(); << " connack_rc=" << conn->last_connack_rc();
if (gen != m_lan_generation.load()) { if (gen != m_lan_generation.load()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring stale LAN MQTT state callback generation=" << gen; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: ignoring stale LAN MQTT state callback generation=" << gen;
@@ -916,21 +1011,19 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st
dispatch_local_connect(ConnectStatusLost, dev_id, "connection_lost"); dispatch_local_connect(ConnectStatusLost, dev_id, "connection_lost");
} }
}); });
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN MQTT start returned ok=" << ok << " generation=" << gen
<< " generation=" << gen << " current_generation=" << m_lan_generation.load() << " current_generation=" << m_lan_generation.load() << " connected=" << conn->is_connected()
<< " connected=" << conn->is_connected() << " running=" << conn->is_running() << " running=" << conn->is_running() << " connack_rc=" << conn->last_connack_rc();
<< " connack_rc=" << conn->last_connack_rc();
if (ok && gen == m_lan_generation.load()) { if (ok && gen == m_lan_generation.load()) {
on_connected(dev_id, conn, gen); on_connected(dev_id, conn, gen);
dispatch_local_connect(ConnectStatusOk, dev_id, "0"); dispatch_local_connect(ConnectStatusOk, dev_id, "0");
} else if (!ok && gen == m_lan_generation.load() && !conn->is_running()) { } else if (!ok && gen == m_lan_generation.load() && !conn->is_running()) {
// A refusal with rc 4/5 terminates the transport. Network errors keep // A refusal with rc 4/5 terminates the transport. Network errors keep
// retrying in OrcaMqttConnection, so leave the UI in its connecting state. // retrying in OrcaMqttConnection, so leave the UI in its connecting state.
const int rc = conn->last_connack_rc(); const int rc = conn->last_connack_rc();
const std::string reason = rc >= 0 ? std::to_string(rc) : "initial_connect_failed"; 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" BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT connection terminated before readiness"
<< " generation=" << gen << " connack_rc=" << rc << " generation=" << gen << " connack_rc=" << rc << " reason=" << reason;
<< " reason=" << reason;
dispatch_local_connect(ConnectStatusFailed, dev_id, reason); dispatch_local_connect(ConnectStatusFailed, dev_id, reason);
} else if (!ok && gen == m_lan_generation.load()) { } else if (!ok && gen == m_lan_generation.load()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT initial attempt failed but worker is retrying" BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: LAN MQTT initial attempt failed but worker is retrying"
@@ -954,8 +1047,8 @@ int OrcaPrinterAgent::disconnect_printer()
{ {
std::lock_guard<std::mutex> l(state_mutex); std::lock_guard<std::mutex> l(state_mutex);
previous_connection = m_current_connection; 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;
@@ -964,8 +1057,8 @@ int OrcaPrinterAgent::disconnect_printer()
} }
current_connection = m_current_connection; 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
<< " previous_dev_id=" << prev_dev << " had_connection=" << (doomed ? "yes" : "no") << " 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) << "->" << " transport=" << connection_type_name(previous_connection) << "->"
<< connection_type_name(current_connection); << connection_type_name(current_connection);
@@ -1005,16 +1098,16 @@ int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const s
// Preserve the transport's existing behavior for malformed payloads; // Preserve the transport's existing behavior for malformed payloads;
// the printer will report the protocol error asynchronously. // 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 << " command=" << command
<< " command=" << command << " payload_bytes=" << json_str.size(); << " 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;
const bool queued = conn->send_request(dev_id, json_str); const bool queued = conn->send_request(dev_id, json_str);
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan
<< " is_lan=" << is_lan << " dev_id=" << dev_id; << " dev_id=" << dev_id;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
} }
@@ -1087,20 +1180,21 @@ int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string /*sec_link*/,
} }
std::string device_id; std::string device_id;
if (!probe_orcasonar_device_id(host, port, device_id) || device_id.empty()) { std::string device_name;
std::string model_id;
if (!probe_orcasonar_landing_page(host, port, device_id, device_name, model_id) || device_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: no OrcaSonar reachable at " << host << ":" << port; BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::bind_detect: no OrcaSonar reachable at " << host << ":" << port;
return BAMBU_NETWORK_ERR_INVALID_HANDLE; return BAMBU_NETWORK_ERR_INVALID_HANDLE;
} }
detect.dev_id = device_id; detect.dev_id = device_id;
detect.dev_name = device_id; detect.dev_name = device_name;
detect.model_id = ""; // unknown; DeviceManager::insert_local_device defaults it detect.model_id = model_id;
detect.version = ""; detect.version = "";
detect.connect_type = "lan"; // required by MachineObject::is_lan_mode_printer() detect.connect_type = "lan"; // required by MachineObject::is_lan_mode_printer()
detect.bind_state = "free"; // required by MachineObject::is_avaliable() detect.bind_state = "free"; // required by MachineObject::is_avaliable()
detect.result_msg = ""; detect.result_msg = "";
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::bind_detect: found OrcaSonar dev_id=" << device_id << " at " << host << ":" << port;
<< " at " << host << ":" << port;
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -1161,7 +1255,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
// selection. Conversely, selecting a cloud machine with the same id // selection. Conversely, selecting a cloud machine with the same id
// while LAN is active is still a transport switch and must proceed. // while LAN is active is still a transport switch and must proceed.
const bool same_selection = dev_id == selected_machine; const bool same_selection = dev_id == selected_machine;
const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD; const bool same_transport = dev_id.empty() ? m_current_connection != CLOUD : m_current_connection == CLOUD;
if (same_selection && same_transport) { if (same_selection && same_transport) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_user_selected_machine: unchanged dev_id=" << dev_id
<< " transport=" << connection_type_name(m_current_connection); << " transport=" << connection_type_name(m_current_connection);
@@ -1183,8 +1277,7 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
current_connection = m_current_connection; 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>") << " transport=" << " cloud=" << (cloud ? "set" : "<null>") << " transport=" << connection_type_name(previous_connection) << "->"
<< connection_type_name(previous_connection) << "->"
<< connection_type_name(current_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";
@@ -1240,13 +1333,16 @@ int OrcaPrinterAgent::start_local_print_with_record(PrintParams params,
// Upload one G-code file to the printer's `gcodes` root over OrcaSonar's // Upload one G-code file to the printer's `gcodes` root over OrcaSonar's
// Moonraker-compatible HTTP facade. No print is started here (print=false); the // Moonraker-compatible HTTP facade. No print is started here (print=false); the
// caller issues print.gcode_file over MQTT separately (start_sdcard_print). // caller issues print.gcode_file over MQTT separately (start_sdcard_print).
int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn /*wait_fn*/) int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn /*wait_fn*/)
{ {
if (update_fn) if (update_fn)
update_fn(PrintingStageCreate, 0, "Preparing..."); update_fn(PrintingStageCreate, 0, "Preparing...");
const std::string local_path = resolve_local_gcode_path(params); const std::string local_path = resolve_local_gcode_path(params);
const fs::path source(local_path); const fs::path source(local_path);
boost::system::error_code ec; boost::system::error_code ec;
if (!fs::exists(source, ec) || !fs::is_regular_file(source, ec)) { if (!fs::exists(source, ec) || !fs::is_regular_file(source, ec)) {
BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path;
@@ -1265,7 +1361,7 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta
std::string host, port, origin; std::string host, port, origin;
if (parse_lan_endpoint(params.dev_ip, host, port)) if (parse_lan_endpoint(params.dev_ip, host, port))
origin = "http://" + host + ":" + port; origin = "http://" + host;
else else
origin = http_origin_from_lan_ws(lan_connection_target()); origin = http_origin_from_lan_ws(lan_connection_target());
if (origin.empty()) { if (origin.empty()) {
@@ -1274,14 +1370,14 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta
} }
const std::string upload_name = remote_gcode_name(params); const std::string upload_name = remote_gcode_name(params);
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: uploading G-code " << local_path << " -> " << origin << "/server/files/upload as "
<< "/server/files/upload as " << upload_name << " (" << file_size << " bytes)"; << upload_name << " (" << file_size << " bytes)";
if (update_fn) if (update_fn)
update_fn(PrintingStageUpload, 0, "Uploading..."); update_fn(PrintingStageUpload, 0, "Uploading...");
bool canceled = false; bool canceled = false;
long http_status = 0; long http_status = 0;
std::string http_error; std::string http_error;
std::string response_body; std::string response_body;
@@ -1322,8 +1418,8 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateSta
// OrcaSonar's Moonraker facade returns 201 Created on a successful save. // OrcaSonar's Moonraker facade returns 201 Created on a successful save.
if (http_status != 200 && http_status != 201) { if (http_status != 200 && http_status != 201) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: G-code upload failed http_status=" << http_status << " error=" << http_error
<< " error=" << http_error << " body=" << response_body; << " body=" << response_body;
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
} }
@@ -1363,8 +1459,7 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
// dst_file, when set, names a file already on the printer (print-from-SD flow); // dst_file, when set, names a file already on the printer (print-from-SD flow);
// otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`. // otherwise start what start_send_gcode_to_sdcard just uploaded to `gcodes`.
const std::string target = params.dst_file.empty() ? remote_gcode_name(params) const std::string target = params.dst_file.empty() ? remote_gcode_name(params) : fs::path(params.dst_file).filename().string();
: fs::path(params.dst_file).filename().string();
nlohmann::json j; nlohmann::json j;
j["print"]["command"] = "gcode_file"; j["print"]["command"] = "gcode_file";
@@ -1375,10 +1470,10 @@ int OrcaPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn up
update_fn(PrintingStageSending, 0, "Starting print..."); update_fn(PrintingStageSending, 0, "Starting print...");
const bool is_lan = params.connection_type == "lan"; const bool is_lan = params.connection_type == "lan";
const int rc = route_send(is_lan, params.dev_id, j.dump()); const int rc = route_send(is_lan, params.dev_id, j.dump());
if (rc != BAMBU_NETWORK_SUCCESS) { if (rc != BAMBU_NETWORK_SUCCESS) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: start_sdcard_print publish failed rc=" << rc << " dev_id=" << params.dev_id
<< " dev_id=" << params.dev_id << " param=" << target; << " param=" << target;
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
} }