fix: shim layer for any compatibiliity changes

This commit is contained in:
Ian Chua
2026-09-10 21:55:31 +08:00
parent 3fe043651b
commit 3fc7fd99d6
3 changed files with 163 additions and 63 deletions

View File

@@ -22,11 +22,13 @@
#include <limits>
#include <mutex>
#include <nlohmann/json.hpp>
#include <nlohmann/json_fwd.hpp>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#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;
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
class OrcaPrinterAgent::OrcaSonarDiscovery
@@ -531,50 +501,142 @@ OrcaMqttConnection* OrcaPrinterAgent::get_appropriate_mqtt_connection(bool is_la
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;
QueueOnMainFn q;
{
std::lock_guard<std::mutex> l(state_mutex);
fn = on_message_fn;
q = queue_on_main_fn;
if (payload.find("get_capabilities") == std::string::npos && payload.find("push_status") == std::string::npos)
return payload;
nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false);
if (!envelope.is_object())
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()
<< " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
if (!fn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping cloud message because on_message_fn is not set"
<< " dev_id=" << dev_id;
return;
else {
const auto print_it = envelope.find("print");
if (print_it != envelope.end() && print_it->is_object() &&
print_it->value("command", "") == "push_status" && !print_it->contains("nozzle_diameter")) {
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
fn(dev_id, payload);
// ----------------------------------------------------------------------
return modified ? envelope.dump() : 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);
std::string merged_payload = merge_capabilities(dev_id, payload);
OnMessageFn fn;
QueueOnMainFn q;
{
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;
}
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");
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;
return;
}
if (q)
q([fn, dev_id, payload] { fn(dev_id, payload); });
q([fn, dev_id, merged_payload] { fn(dev_id, merged_payload); });
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)
@@ -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) {
if (generation == m_lan_generation.load())
deliver_to_local_sink(id, payload);
deliver_to_sink(id, payload, true);
else
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: dropping stale LAN message generation=" << generation
<< " 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,
// so being called from the MQTT worker thread is fine.
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;
}

View File

@@ -127,17 +127,21 @@ public:
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
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.
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
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// LAN has a separate callback in the existing IPrinterAgent contract because the
// GUI parses local reports with the "lan" dialect and looks up local machines.
void deliver_to_local_sink(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// 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
// the other printer agents. The transport result cannot be returned by

View File

@@ -28,11 +28,45 @@ TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaP
Probe agent("/tmp");
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.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_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]") {
std::string h, p;
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));