mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
feat(orca-agent): integrate OrcaSonar AMS synchronization
- Translate slicer AMS commands to OrcaSonar’s canonical write format - Gate AMS operations using printer capabilities - Refresh capabilities when filament topology becomes available - Apply virtual-slot removals only from full status snapshots - Add synchronization tests and document the integration
This commit is contained in:
@@ -5495,14 +5495,14 @@ TEST_CASE("filament_id_by_type resolves a modifier type to its generic family",
|
||||
pla.is_visible = true;
|
||||
pla.is_compatible = true;
|
||||
pla.filament_id = "GFL99";
|
||||
pla.config.option<ConfigOptionString>("filament_type")->value = "PLA";
|
||||
pla.config.option<ConfigOptionStrings>("filament_type")->values = {"PLA"};
|
||||
|
||||
Preset &petg = add_inmemory_preset(bundle.filaments, "Generic PETG @System");
|
||||
petg.is_system = true;
|
||||
petg.is_visible = true;
|
||||
petg.is_compatible = true;
|
||||
petg.filament_id = "GFG99";
|
||||
petg.config.option<ConfigOptionString>("filament_type")->value = "PETG";
|
||||
petg.config.option<ConfigOptionStrings>("filament_type")->values = {"PETG"};
|
||||
|
||||
CHECK(bundle.filaments.filament_id_by_type("PETG Basic") == "GFG99");
|
||||
CHECK(bundle.filaments.filament_id_by_type("PLA") == "GFL99");
|
||||
|
||||
@@ -88,6 +88,92 @@ TEST_CASE("An orphan deputy virtual tray is dropped, not indexed", "[DeviceManag
|
||||
CHECK(machine.vt_slot.empty());
|
||||
}
|
||||
|
||||
// OrcaSonar emits a virtual slot only when the layout has it, so it counts as
|
||||
// present even with no filament: the user's configuration wins.
|
||||
TEST_CASE("A configured vir_slot is present even with no material", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"255"},{"id":"254"}]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].is_exists);
|
||||
CHECK(machine.vt_slot[1].is_exists);
|
||||
}
|
||||
|
||||
// A full vir_slot snapshot is authoritative: an id it omits is removed, so a
|
||||
// tool-count shrink does not leave a stale deputy tray.
|
||||
TEST_CASE("A populated vir_slot prunes virtual trays it omits", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"255"},{"id":"254"}]}})", false);
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"255"}]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
}
|
||||
|
||||
// A delta frame (msg=1) is not authoritative for removals: an entry it omits
|
||||
// stays held, and an entry it names is updated in place (spec §7.3).
|
||||
TEST_CASE("A delta vir_slot does not prune virtual trays it omits", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[{"id":"255"},{"id":"254"}]}})", false);
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
|
||||
// Delta names only the deputy: the main must survive, the deputy updates.
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":1,"vir_slot":[{"id":"254","tag_uid":"ABCDEF0123456789"}]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
CHECK(machine.vt_slot[1].tag_uid == "ABCDEF0123456789");
|
||||
}
|
||||
|
||||
// An empty delta is not authoritative and must preserve the known layout.
|
||||
TEST_CASE("An empty delta vir_slot keeps the virtual trays", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":0,"vir_slot":[{"id":"255"},{"id":"254"}]}})", false);
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","msg":1,"vir_slot":[]}})", false);
|
||||
|
||||
REQUIRE(machine.vt_slot.size() == 2);
|
||||
CHECK(machine.vt_slot[0].id == "255");
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
CHECK(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
TEST_CASE("Capability flags parse without a DeviceManager", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"flags":{"support_tunnel_mqtt":true}}}})", false);
|
||||
|
||||
CHECK(machine.is_support_tunnel_mqtt);
|
||||
}
|
||||
|
||||
// A filament setting landing on the virtual tray also marks it present.
|
||||
TEST_CASE("A virtual tray setting marks the tray present", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"255"}]}})", false);
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
machine.vt_slot[0].is_exists = false;
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","ams_id":255,"tray_id":255,"tray_color":"FF0000FF","tray_type":"PLA","tray_info_idx":"GFA00","nozzle_temp_min":190,"nozzle_temp_max":230}})", false);
|
||||
|
||||
CHECK(machine.vt_slot[0].is_exists);
|
||||
}
|
||||
|
||||
// acks that target the virtual tray must survive an emptied vt_slot.
|
||||
TEST_CASE("Virtual tray acks are safe with no virtual tray", "[DeviceManager]")
|
||||
{
|
||||
|
||||
@@ -394,6 +394,20 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","selector":"external"}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
|
||||
// A lane can resolve to an external slot server-side (§7.8), so a device
|
||||
// that declares only external still admits a canonical lane write.
|
||||
Slic3r::register_ams_ops("dev-c4", {"external"});
|
||||
unsupported = false;
|
||||
canon("dev-c4", R"({"print":{"command":"ams_change_filament","selector":"lane","lane":1}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
unsupported = false;
|
||||
canon("dev-c4", R"({"print":{"command":"ams_change_filament","selector":"external"}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
// Other selectors still gate on their own token.
|
||||
unsupported = false;
|
||||
canon("dev-c4", R"({"print":{"command":"ams_change_filament","selector":"unload"}})", &unsupported);
|
||||
CHECK(unsupported);
|
||||
|
||||
// A device with no capabilities record is never gated (server backstops).
|
||||
unsupported = false;
|
||||
canon("dev-c3", R"({"print":{"command":"ams_user_setting","ams_id":0}})", &unsupported);
|
||||
@@ -441,3 +455,72 @@ TEST_CASE("an AMS tray selection sends the tray's ams_id and slot_id", "[OrcaPri
|
||||
CHECK(body["print"]["ams_id"] == 0);
|
||||
CHECK(body["print"]["slot_id"] == 3);
|
||||
}
|
||||
|
||||
// A filament frame can arrive after the get_capabilities reply was missed (the
|
||||
// topology resolved late, or Klipper restarted). While the device is still
|
||||
// unconfirmed, one filament frame must re-ask, throttled, and stop once a reply
|
||||
// arrives so the session can leave FilamentSyncMode::none.
|
||||
TEST_CASE("a filament frame re-requests capabilities while the topology is unconfirmed", "[OrcaPrinterAgent]") {
|
||||
struct RefreshProbe : OrcaPrinterAgent {
|
||||
using OrcaPrinterAgent::OrcaPrinterAgent;
|
||||
using OrcaPrinterAgent::deliver_to_sink;
|
||||
std::vector<std::string> refreshes;
|
||||
void request_filament_capabilities(const std::string& dev_id, bool /*local*/) override {
|
||||
refreshes.push_back(dev_id);
|
||||
}
|
||||
} agent("/tmp");
|
||||
agent.set_on_message_fn([](std::string, std::string) {});
|
||||
|
||||
const std::string frame = R"({"print":{"command":"push_status","vir_slot":[{"id":"255"}]}})";
|
||||
agent.deliver_to_sink("dev-refresh-1", frame, /*local=*/true);
|
||||
REQUIRE(agent.refreshes.size() == 1);
|
||||
|
||||
// Throttled: a second frame immediately after does not ask again.
|
||||
agent.deliver_to_sink("dev-refresh-1", frame, /*local=*/true);
|
||||
CHECK(agent.refreshes.size() == 1);
|
||||
|
||||
// A capability reply confirms the topology: no further re-request.
|
||||
agent.deliver_to_sink("dev-refresh-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"filament_slots":true}}}}})",
|
||||
/*local=*/true);
|
||||
agent.deliver_to_sink("dev-refresh-1", frame, /*local=*/true);
|
||||
CHECK(agent.refreshes.size() == 1);
|
||||
|
||||
// A status without filament state never triggers a request.
|
||||
agent.deliver_to_sink("dev-refresh-2", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/true);
|
||||
CHECK(agent.refreshes.size() == 1);
|
||||
}
|
||||
|
||||
// Deselecting a cloud device forgets its declaration, so a later session starts
|
||||
// from "no reply yet" instead of a stale one. With no record an undeclared op is
|
||||
// admitted again, which is how a freshly selected device starts.
|
||||
TEST_CASE("deselecting a cloud device forgets its capabilities", "[OrcaPrinterAgent]") {
|
||||
OrcaPrinterAgent agent("/tmp");
|
||||
Slic3r::register_ams_ops("dev-cloud-clear", {"change_filament"});
|
||||
Slic3r::register_filament_slots("dev-cloud-clear", true);
|
||||
CHECK_FALSE(Slic3r::ams_op_supported("dev-cloud-clear", "external"));
|
||||
|
||||
agent.set_user_selected_machine("dev-cloud-clear");
|
||||
agent.set_user_selected_machine("");
|
||||
|
||||
CHECK(Slic3r::ams_op_supported("dev-cloud-clear", "external"));
|
||||
CHECK_FALSE(Slic3r::has_filament_slots("dev-cloud-clear"));
|
||||
}
|
||||
|
||||
// A LAN feed for the same device id must keep its declaration when the cloud
|
||||
// selection is cleared: clearing it would strand an independently live session.
|
||||
// why hidden: spawns the LAN connect worker, like the other connect tests.
|
||||
TEST_CASE("deselecting a cloud device keeps a live LAN declaration", "[OrcaPrinterAgent][.integration]") {
|
||||
OrcaPrinterAgent agent("/tmp");
|
||||
Slic3r::register_ams_ops("dev-lan-keep", {"change_filament"});
|
||||
Slic3r::register_filament_slots("dev-lan-keep", true);
|
||||
|
||||
REQUIRE(agent.connect_printer("dev-lan-keep", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
agent.set_user_selected_machine("dev-lan-keep");
|
||||
agent.set_user_selected_machine("");
|
||||
|
||||
CHECK_FALSE(Slic3r::ams_op_supported("dev-lan-keep", "external"));
|
||||
CHECK(Slic3r::has_filament_slots("dev-lan-keep"));
|
||||
|
||||
agent.disconnect_printer();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user