mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
fix: shim layer for any compatibiliity changes
This commit is contained in:
@@ -22,11 +22,13 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <boost/property_tree/ptree.hpp>
|
#include <boost/property_tree/ptree.hpp>
|
||||||
@@ -218,38 +220,6 @@ bool probe_orcasonar_device_xml(const std::string& location, std::string& device
|
|||||||
std::string body;
|
std::string body;
|
||||||
return fetch_orcasonar_body(location, body) && parse_orcasonar_device_xml(body, device_id, device_name, model_id);
|
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
|
||||||
@@ -531,50 +501,142 @@ OrcaMqttConnection* OrcaPrinterAgent::get_appropriate_mqtt_connection(bool is_la
|
|||||||
return cloud ? cloud->get_mqtt_connection() : nullptr;
|
return cloud ? cloud->get_mqtt_connection() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload)
|
// ============================================================================
|
||||||
|
// Orca-dialect -> Bambu-dialect compatibility shim for inbound printer reports.
|
||||||
|
//
|
||||||
|
// OrcaSonar and the bridge adapter speak the "Orca Protocol" JSON dialect.
|
||||||
|
// MachineObject::parse_json (DeviceManager.cpp) only understands the Bambu
|
||||||
|
// dialect. Until the Orca Protocol is formally specified, this function is the
|
||||||
|
// ONE place where an inbound Orca-dialect report is rewritten into the Bambu
|
||||||
|
// shape parse_json already handles.
|
||||||
|
//
|
||||||
|
// Rules of this seam:
|
||||||
|
// 1. parse_json and the rest of DeviceManager are NOT modified to accommodate
|
||||||
|
// the Orca dialect - every such accommodation is a rule inside this
|
||||||
|
// function.
|
||||||
|
// 2. Each rule documents its Orca-dialect source, the Bambu-dialect target
|
||||||
|
// parse_json expects, and the rewrite between them. Rules are independent
|
||||||
|
// and can be removed one at a time as parse_json gains native support.
|
||||||
|
// 3. When parse_json reads the Orca dialect directly, this function and the
|
||||||
|
// single call in deliver_to_sink can be deleted, state included. Nothing
|
||||||
|
// else should need to change.
|
||||||
|
//
|
||||||
|
// It runs on every inbound report, so it stays cheap for payloads it does not
|
||||||
|
// touch (substring pre-check, no re-serialize unless something changed) and
|
||||||
|
// never throws (non-throwing parse, every field access guarded).
|
||||||
|
// ============================================================================
|
||||||
|
std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, const std::string& payload)
|
||||||
{
|
{
|
||||||
OnMessageFn fn;
|
if (payload.find("get_capabilities") == std::string::npos && payload.find("push_status") == std::string::npos)
|
||||||
QueueOnMainFn q;
|
return payload;
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> l(state_mutex);
|
nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false);
|
||||||
fn = on_message_fn;
|
if (!envelope.is_object())
|
||||||
q = queue_on_main_fn;
|
return payload;
|
||||||
|
|
||||||
|
// Shim-local state, kept here (not on the class) so deleting this function
|
||||||
|
// removes its storage too. Process-wide and keyed by the globally-unique
|
||||||
|
// dev_id, with its own mutex; C++11 makes the one-time init thread-safe.
|
||||||
|
static std::unordered_map<std::string, double> nozzle_diameter_cache;
|
||||||
|
static std::mutex nozzle_diameter_cache_mutex;
|
||||||
|
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
// ---- Rule: nozzle geometry ---------------------------------------------
|
||||||
|
// Orca dialect : info.capabilities.topology.tools[i].nozzle.diameter_mm,
|
||||||
|
// delivered once in the get_capabilities reply; push_status
|
||||||
|
// frames carry no nozzle geometry at all.
|
||||||
|
// Bambu dialect: parse_json runs DevNozzleSystemParser::ParseV1_0 only for a
|
||||||
|
// push_status frame that holds BOTH print.nozzle_diameter and
|
||||||
|
// print.nozzle_type.
|
||||||
|
// Rewrite : cache the diameter from the capabilities reply per device,
|
||||||
|
// then stamp print.nozzle_diameter + a neutral print.nozzle_type
|
||||||
|
// ("N/A" -> NozzleType::ntUndefine, as MoonrakerPrinterAgent
|
||||||
|
// does; Klipper has no Bambu nozzle type) onto later push_status
|
||||||
|
// frames that carry no real nozzle data.
|
||||||
|
const auto info_it = envelope.find("info");
|
||||||
|
const bool is_capabilities_reply = info_it != envelope.end() && info_it->is_object() &&
|
||||||
|
info_it->value("command", "") == "get_capabilities";
|
||||||
|
|
||||||
|
if (is_capabilities_reply) {
|
||||||
|
double nozzle_dia = 0.0;
|
||||||
|
const auto caps_it = info_it->find("capabilities");
|
||||||
|
if (caps_it != info_it->end() && caps_it->is_object()) {
|
||||||
|
const auto topology_it = caps_it->find("topology");
|
||||||
|
if (topology_it != caps_it->end() && topology_it->is_object()) {
|
||||||
|
const auto tools_it = topology_it->find("tools");
|
||||||
|
if (tools_it != topology_it->end() && tools_it->is_array()) {
|
||||||
|
// First tool with a usable diameter wins: ParseV1_0 keeps a
|
||||||
|
// single nozzle (id 0).
|
||||||
|
for (const auto& tool : *tools_it) {
|
||||||
|
if (!tool.is_object())
|
||||||
|
continue;
|
||||||
|
const auto nozzle_it = tool.find("nozzle");
|
||||||
|
if (nozzle_it == tool.end() || !nozzle_it->is_object())
|
||||||
|
continue;
|
||||||
|
const auto dia_it = nozzle_it->find("diameter_mm");
|
||||||
|
if (dia_it != nozzle_it->end() && dia_it->is_number() && dia_it->get<double>() > 0.0) {
|
||||||
|
nozzle_dia = dia_it->get<double>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nozzle_dia > 0.0) {
|
||||||
|
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
|
||||||
|
nozzle_diameter_cache[dev_id] = nozzle_dia;
|
||||||
|
}
|
||||||
|
// The capabilities reply itself is forwarded unchanged.
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering cloud message dev_id=" << dev_id << " payload_bytes=" << payload.size()
|
else {
|
||||||
<< " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
|
const auto print_it = envelope.find("print");
|
||||||
if (!fn) {
|
if (print_it != envelope.end() && print_it->is_object() &&
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set"
|
print_it->value("command", "") == "push_status" && !print_it->contains("nozzle_diameter")) {
|
||||||
<< " dev_id=" << dev_id;
|
|
||||||
return;
|
double nozzle_dia = 0.0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
|
||||||
|
const auto it = nozzle_diameter_cache.find(dev_id);
|
||||||
|
if (it != nozzle_diameter_cache.end())
|
||||||
|
nozzle_dia = it->second;
|
||||||
|
}
|
||||||
|
if (nozzle_dia > 0.0) {
|
||||||
|
(*print_it)["nozzle_diameter"] = nozzle_dia;
|
||||||
|
(*print_it)["nozzle_type"] = "N/A";
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (q)
|
// ----------------------------------------------------------------------
|
||||||
q([fn, dev_id, payload] { fn(dev_id, payload); });
|
|
||||||
else
|
return modified ? envelope.dump() : payload;
|
||||||
fn(dev_id, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrcaPrinterAgent::deliver_to_local_sink(const std::string& dev_id, const std::string& payload)
|
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
|
||||||
{
|
{
|
||||||
parse_ipcam_info(dev_id, payload);
|
parse_ipcam_info(dev_id, payload);
|
||||||
|
std::string merged_payload = merge_capabilities(dev_id, payload);
|
||||||
|
|
||||||
OnMessageFn fn;
|
OnMessageFn fn;
|
||||||
QueueOnMainFn q;
|
QueueOnMainFn q;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> l(state_mutex);
|
std::lock_guard<std::mutex> l(state_mutex);
|
||||||
fn = on_local_message_fn;
|
fn = local ? on_local_message_fn : on_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 << " payload_bytes=" << payload.size()
|
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering " << (local ? "local" : "cloud") << " report payload dev_id=" << dev_id
|
||||||
|
<< " payload=" << merged_payload
|
||||||
<< " 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 " << (local ? "local" : "cloud") << " message because on_message_fn is not set"
|
||||||
<< " dev_id=" << dev_id;
|
<< " dev_id=" << dev_id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (q)
|
if (q)
|
||||||
q([fn, dev_id, payload] { fn(dev_id, payload); });
|
q([fn, dev_id, merged_payload] { fn(dev_id, merged_payload); });
|
||||||
else
|
else
|
||||||
fn(dev_id, payload);
|
fn(dev_id, merged_payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& message)
|
void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_id, const std::string& message)
|
||||||
@@ -603,7 +665,7 @@ std::function<void(const std::string&, const std::string&)> OrcaPrinterAgent::ma
|
|||||||
{
|
{
|
||||||
return [this, generation](const std::string& id, const std::string& payload) {
|
return [this, generation](const std::string& id, const std::string& payload) {
|
||||||
if (generation == m_lan_generation.load())
|
if (generation == m_lan_generation.load())
|
||||||
deliver_to_local_sink(id, payload);
|
deliver_to_sink(id, payload, true);
|
||||||
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() << " dev_id=" << id;
|
<< " current_generation=" << m_lan_generation.load() << " dev_id=" << id;
|
||||||
@@ -627,7 +689,7 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
|
|||||||
// standard sink. message_arrive_fn self-marshals to the UI thread via CallAfter,
|
// standard sink. message_arrive_fn self-marshals to the UI thread via CallAfter,
|
||||||
// so being called from the MQTT worker thread is fine.
|
// so being called from the MQTT worker thread is fine.
|
||||||
const int callback_result = get_orca_cloud_agent()->set_printer_status_callback(
|
const int callback_result = get_orca_cloud_agent()->set_printer_status_callback(
|
||||||
[this](std::string dev_id, std::string payload) { deliver_to_sink(dev_id, payload); });
|
[this](std::string dev_id, std::string payload) { deliver_to_sink(dev_id, payload, false); });
|
||||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result;
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,17 +127,21 @@ public:
|
|||||||
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
|
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Forward one inbound printer message to on_message_fn (marshalled onto the UI
|
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
|
||||||
// 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, bool local);
|
||||||
|
|
||||||
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
|
// 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.
|
// 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);
|
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
|
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
|
||||||
// GUI parses local reports with the "lan" dialect and looks up local machines.
|
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
|
||||||
void deliver_to_local_sink(const std::string& dev_id, const std::string& payload);
|
// already handles, so parse_json needs no Orca-specific changes. Self-contained
|
||||||
|
// (its cache is a function-local static) and deletable together with its call site
|
||||||
|
// once parse_json reads the Orca dialect natively. See the definition for the
|
||||||
|
// per-rule detail. Returns the payload unchanged when no rule applies.
|
||||||
|
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
|
||||||
|
|
||||||
// Report the asynchronous LAN connection state using the same callback contract as
|
// Report the asynchronous LAN connection state using the same callback contract as
|
||||||
// the other printer agents. The transport result cannot be returned by
|
// the other printer agents. The transport result cannot be returned by
|
||||||
|
|||||||
@@ -28,11 +28,45 @@ TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaP
|
|||||||
Probe agent("/tmp");
|
Probe agent("/tmp");
|
||||||
std::string got_id, got_payload;
|
std::string got_id, got_payload;
|
||||||
agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); });
|
agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); });
|
||||||
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})");
|
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})", /*local=*/false);
|
||||||
CHECK(got_id == "dev-1");
|
CHECK(got_id == "dev-1");
|
||||||
CHECK(got_payload.find("push_status") != std::string::npos);
|
CHECK(got_payload.find("push_status") != std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto push_status frames", "[OrcaPrinterAgent]") {
|
||||||
|
Probe agent("/tmp");
|
||||||
|
std::string last_payload;
|
||||||
|
agent.set_on_message_fn([&](std::string, std::string p){ last_payload = std::move(p); });
|
||||||
|
|
||||||
|
// Before any capabilities reply, a push_status frame is forwarded untouched.
|
||||||
|
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/false);
|
||||||
|
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
|
||||||
|
|
||||||
|
// The get_capabilities reply is forwarded verbatim; its topology nozzle diameter
|
||||||
|
// is cached for the device.
|
||||||
|
agent.deliver_to_sink(
|
||||||
|
"dev-1",
|
||||||
|
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})",
|
||||||
|
/*local=*/false);
|
||||||
|
CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos);
|
||||||
|
CHECK(last_payload.find("\"print\"") == std::string::npos);
|
||||||
|
|
||||||
|
// Later push_status frames for that device get the cached diameter plus a neutral
|
||||||
|
// nozzle_type, so MachineObject::parse_json's legacy nozzle parser can run.
|
||||||
|
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":20}})", /*local=*/false);
|
||||||
|
CHECK(last_payload.find("\"nozzle_diameter\":0.4") != std::string::npos);
|
||||||
|
CHECK(last_payload.find("\"nozzle_type\":\"N/A\"") != std::string::npos);
|
||||||
|
|
||||||
|
// A different device is unaffected.
|
||||||
|
agent.deliver_to_sink("dev-2", R"({"print":{"command":"push_status"}})", /*local=*/false);
|
||||||
|
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
|
||||||
|
|
||||||
|
// A frame that already carries real nozzle data is not overridden.
|
||||||
|
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false);
|
||||||
|
CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos);
|
||||||
|
CHECK(last_payload.find("N/A") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
|
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
|
||||||
std::string h, p;
|
std::string h, p;
|
||||||
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));
|
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));
|
||||||
|
|||||||
Reference in New Issue
Block a user