mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Initial implementation of filament mapping on OrcaSonar side
This commit is contained in:
@@ -5482,3 +5482,35 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
||||
}
|
||||
}
|
||||
|
||||
// OrcaSonar and the Qidi Box report a material type but no slicer preset id.
|
||||
// The Orca agent's AMS sync path fills one with filament_id_by_type, whose
|
||||
// modifier stripping ("PETG Basic" -> the PETG family) this locks in.
|
||||
TEST_CASE("filament_id_by_type resolves a modifier type to its generic family", "[Preset][Filament]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "Generic PLA @System");
|
||||
pla.is_system = true;
|
||||
pla.is_visible = true;
|
||||
pla.is_compatible = true;
|
||||
pla.filament_id = "GFL99";
|
||||
pla.config.option<ConfigOptionString>("filament_type")->value = "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";
|
||||
|
||||
CHECK(bundle.filaments.filament_id_by_type("PETG Basic") == "GFG99");
|
||||
CHECK(bundle.filaments.filament_id_by_type("PLA") == "GFL99");
|
||||
|
||||
// The strict overload reports no match instead of binding a fallback.
|
||||
std::string out = "unchanged";
|
||||
CHECK_FALSE(bundle.filaments.filament_id_by_type("Kevlar", out));
|
||||
CHECK(out == "unchanged");
|
||||
CHECK(bundle.filaments.filament_id_by_type("PETG Basic", out));
|
||||
CHECK(out == "GFG99");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_dev_mapping.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
|
||||
// ambiguity in the Windows COM headers.
|
||||
// why: wx/timer.h must precede DeviceManager.hpp because
|
||||
// DeviceErrorDialog.hpp uses wxTimerEvent.
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
// DeviceManager's push_status contract for the OrcaSonar virtual tray: an
|
||||
// authoritative empty clears, a populated key re-enables, and a frame that
|
||||
// omits the key leaves both the trays and the support flag alone.
|
||||
|
||||
// Contract: an authoritative empty vir_slot ([] = "known, no virtual slots")
|
||||
// clears the seeded virtual trays. The consumers were guarded so an empty
|
||||
// vector is safe.
|
||||
TEST_CASE("An empty vir_slot clears the virtual trays", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
REQUIRE(machine.vt_slot[0].id == "255");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
CHECK_FALSE(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// A frame that omits vir_slot must leave the seeded virtual tray untouched.
|
||||
TEST_CASE("A missing vir_slot keeps the virtual trays", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.vt_slot.size() == 1);
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status"}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.size() == 1);
|
||||
CHECK(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// Repopulating after a clear must not rely on the constructor's seed, and must
|
||||
// re-enable virtual-tray support even though the clear turned the flag off.
|
||||
TEST_CASE("Virtual trays repopulate after an authoritative clear", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
REQUIRE_FALSE(machine.ams_support_virtual_tray);
|
||||
|
||||
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].id == "255");
|
||||
CHECK(machine.vt_slot[1].id == "254");
|
||||
CHECK(machine.ams_support_virtual_tray);
|
||||
}
|
||||
|
||||
// A deputy with no main is not an index-1 write into an empty vector.
|
||||
TEST_CASE("An orphan deputy virtual tray is dropped, not indexed", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[{"id":"254"}]}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
}
|
||||
|
||||
// acks that target the virtual tray must survive an emptied vt_slot.
|
||||
TEST_CASE("Virtual tray acks are safe with no virtual tray", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"push_status","vir_slot":[]}})", false);
|
||||
REQUIRE(machine.vt_slot.empty());
|
||||
|
||||
machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","ams_id":255,"tray_id":255}})", false);
|
||||
machine.parse_json("lan", R"({"print":{"command":"extrusion_cali_set","tray_id":255,"k_value":0.02}})", false);
|
||||
|
||||
CHECK(machine.vt_slot.empty());
|
||||
}
|
||||
|
||||
// Only Bambu's own agent treats a non-zero tag_uid as an RFID lock; agent-managed
|
||||
// printers send it as metadata and must keep their trays editable.
|
||||
TEST_CASE("Only the BBL agent is RFID-locking", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.printer_agent_id = "bbl";
|
||||
CHECK(machine.is_bbl_agent());
|
||||
|
||||
machine.printer_agent_id = "orca";
|
||||
CHECK_FALSE(machine.is_bbl_agent());
|
||||
|
||||
machine.printer_agent_id = "";
|
||||
CHECK(machine.is_bbl_agent());
|
||||
}
|
||||
|
||||
// An OPCP error ack carries result "fail" and a reason; a success ack (or one
|
||||
// without result) must not be mistaken for a failure.
|
||||
TEST_CASE("AMS filament setting acks expose OPCP failures", "[DeviceManager]")
|
||||
{
|
||||
std::string reason;
|
||||
|
||||
const json failure = json::parse(R"({"result":"fail","errno":-19,"reason":"unknown slot"})");
|
||||
CHECK(MachineObject::ams_filament_ack_failed(failure, reason));
|
||||
CHECK(reason == "unknown slot");
|
||||
|
||||
reason = "stale";
|
||||
const json success = json::parse(R"({"result":"success","errno":0})");
|
||||
CHECK_FALSE(MachineObject::ams_filament_ack_failed(success, reason));
|
||||
CHECK(reason.empty());
|
||||
|
||||
const json plain = json::parse(R"({"ams_id":1,"tray_id":2})");
|
||||
CHECK_FALSE(MachineObject::ams_filament_ack_failed(plain, reason));
|
||||
}
|
||||
|
||||
// An ack that targets a tray but omits tray_id must not abort the frame: it
|
||||
// falls back to slot 0 instead of an unguarded get on a missing key.
|
||||
TEST_CASE("An AMS filament ack without a tray_id targets slot 0", "[DeviceManager]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
const json ams = json::parse(R"({"ams":{"tray_exist_bits":"1","ams":[
|
||||
{ "id": "0", "info": "00000001", "tray": [ { "id": "0" } ] } ]}})");
|
||||
DevFilaSystemParser::ParseV1_0(ams, &machine, machine.GetFilaSystem().get(), false);
|
||||
REQUIRE(machine.GetFilaSystem()->GetAmsTray("0", "0") != nullptr);
|
||||
|
||||
CHECK_NOTHROW(machine.parse_json("lan", R"({"print":{"command":"ams_filament_setting","ams_id":0,"tray_color":"FF0000FF","tray_type":"PLA","tray_info_idx":"GFA00","nozzle_temp_min":190,"nozzle_temp_max":230}})", false));
|
||||
|
||||
const DevAmsTray* tray = machine.GetFilaSystem()->GetAmsTray("0", "0");
|
||||
REQUIRE(tray != nullptr);
|
||||
CHECK(tray->color == "FF0000FF");
|
||||
}
|
||||
@@ -139,3 +139,5 @@ TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Prog
|
||||
REQUIRE(machine.mc_print_percent == 0);
|
||||
CHECK(current_subtask->task_progress == 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -127,12 +127,28 @@ TEST_CASE("filament sync follows the printer's AMS capability", "[OrcaPrinterAge
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
|
||||
// filament_slots alone enables sync: a standalone printer with no material
|
||||
// system still has slots (REQ-FMS-001).
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false,"filament_slots":true}}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// Older payloads without features.fms fall back to a non-empty ams_ops.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// A reply without a protocol block is not a capabilities answer: it is
|
||||
// ignored, so a transient malformed reply cannot gate every write. The
|
||||
// ams_ops fallback above still holds.
|
||||
agent.deliver_to_sink("dev-ams-1",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{}}})",
|
||||
/*local=*/true);
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
|
||||
|
||||
// Disconnecting forgets the declaration rather than letting it go stale.
|
||||
agent.disconnect_printer();
|
||||
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
|
||||
}
|
||||
@@ -155,46 +171,21 @@ TEST_CASE("a capability reply without ams_ops gates AMS writes", "[OrcaPrinterAg
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
// The subscription refresh is self-triggered: a pushed frame whose print block
|
||||
// carries a CHANGED topology_state.material_hash (spec REQ-STS-007 §7.7) is
|
||||
// the doorbell; repeat hashes, temperature-only frames and hash-less blocks
|
||||
// are not (no per-frame polling regression).
|
||||
TEST_CASE("a material_hash change is the filament-sync doorbell", "[OrcaPrinterAgent][.integration]") {
|
||||
// A malformed get_capabilities reply must be ignored, not read as "no
|
||||
// capabilities": doing so would set ops_known with an empty set and gate every
|
||||
// AMS write until a good reply arrives.
|
||||
TEST_CASE("a malformed capability reply does not gate AMS writes", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
agent.deliver_to_sink("dev-malformed",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"ams_ops":["change_filament"]}}}})",
|
||||
/*local=*/true);
|
||||
CHECK(Slic3r::ams_op_supported("dev-malformed", "change_filament"));
|
||||
|
||||
const std::string frame_a = R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:aaaaaaaaaaaaaaaa","units":[]}}})";
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1",
|
||||
R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:bbbbbbbbbbbbbbbb","units":[]}}})"));
|
||||
// Topology block without the doorbell token (older/foreign payload): stays quiet.
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1",
|
||||
R"({"print":{"command":"push_status","topology_state":{"units":[]}}})"));
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})"));
|
||||
// Not the active LAN device: never a doorbell.
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-2", frame_a));
|
||||
|
||||
agent.disconnect_printer();
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
|
||||
}
|
||||
|
||||
// After a failed lane_data fetch there is no retry timer: any next LAN frame
|
||||
// re-arms the refresh, because a changing printer keeps pushing.
|
||||
TEST_CASE("a failed filament fetch retries on the next LAN frame", "[OrcaPrinterAgent][.integration]") {
|
||||
Probe agent("/tmp");
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
|
||||
const std::string telemetry = R"({"print":{"command":"push_status","mc_percent":10}})";
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
agent.set_filament_failed_for_test(true);
|
||||
CHECK(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
|
||||
agent.disconnect_printer();
|
||||
// disconnect clears the latch; the next connect eager-fetches instead.
|
||||
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
|
||||
agent.disconnect_printer();
|
||||
agent.deliver_to_sink("dev-malformed",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{}}})",
|
||||
/*local=*/true);
|
||||
// The malformed reply changed nothing, rather than clearing the op set.
|
||||
CHECK(Slic3r::ams_op_supported("dev-malformed", "change_filament"));
|
||||
}
|
||||
|
||||
TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
|
||||
@@ -320,10 +311,11 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
auto out = nlohmann::json::parse(canon("dev-c1",
|
||||
R"({"print":{"command":"ams_change_filament","sequence_id":"1","target":5,"slot_id":1,"ams_id":1,"curr_temp":210,"tar_temp":220}})"));
|
||||
CHECK(out["print"]["selector"] == "lane");
|
||||
CHECK(out["print"]["lane"] == 5);
|
||||
// Coordinates are the resolver's own form; target (a BBL tray id) is dropped.
|
||||
CHECK(out["print"]["ams_id"] == 1);
|
||||
CHECK(out["print"]["slot_id"] == 1);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("target"));
|
||||
CHECK(!out["print"].contains("slot_id"));
|
||||
CHECK(!out["print"].contains("ams_id"));
|
||||
CHECK(out["print"]["tar_temp"] == 220);
|
||||
|
||||
// External-spool selection ("254" arrives hacked to 255 with slot_id=0):
|
||||
@@ -331,17 +323,48 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":255,"target":255,"slot_id":0}})"));
|
||||
CHECK(out["print"]["selector"] == "external");
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("ams_id"));
|
||||
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":0,"target":255,"slot_id":255}})"));
|
||||
CHECK(out["print"]["selector"] == "unload");
|
||||
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":1,"slot_id":2}})"));
|
||||
CHECK(out["print"]["lane"] == 6);
|
||||
// A coordinate-less body must not fabricate a flat lane from a BBL tray id;
|
||||
// the server validates the address and answers -19.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","target":6}})"));
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("target"));
|
||||
CHECK(out["print"]["selector"] == "lane");
|
||||
|
||||
// Box coordinates are forwarded unchanged: a fabricated flat lane would be
|
||||
// the BBL tray id, which is not the layout lane for wide/sparse boxes.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["lane"] == 6);
|
||||
CHECK(out["print"]["ams_id"] == 1);
|
||||
CHECK(out["print"]["slot_id"] == 2);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("tray_id"));
|
||||
CHECK(out["print"]["tray_type"] == "PLA");
|
||||
|
||||
// Wide-box dual form: both addressings resolve to one slot server-side.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":5,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["ams_id"] == 1);
|
||||
CHECK(out["print"]["slot_id"] == 5);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":2,"slot_id":1,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["ams_id"] == 2);
|
||||
CHECK(out["print"]["slot_id"] == 1);
|
||||
|
||||
// A lane-only body must pass through untouched, never become lane = -5.
|
||||
const std::string lane_only = R"({"print":{"command":"ams_filament_setting","lane":5,"tray_type":"PLA"}})";
|
||||
CHECK(canon("dev-c1", lane_only) == lane_only);
|
||||
|
||||
// External/direct spool (Bambu 254/255): forwarded as the canonical
|
||||
// ams_id address, never a fabricated lane (REQ-FMS-001).
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":255,"slot_id":0,"tray_id":0,"tray_type":"PLA"}})"));
|
||||
CHECK(out["print"]["ams_id"] == 255);
|
||||
CHECK(out["print"]["slot_id"] == 0);
|
||||
CHECK(!out["print"].contains("lane"));
|
||||
CHECK(!out["print"].contains("tray_id"));
|
||||
|
||||
// Legacy RFID call shape (ams_id+slot_id, no tray_id) flattens to tray_id.
|
||||
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_get_rfid","ams_id":1,"slot_id":2}})"));
|
||||
CHECK(out["print"]["tray_id"] == 6);
|
||||
@@ -363,9 +386,40 @@ TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical Orc
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":1}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
// A selector-carrying canonical body is gated on its selector's op too.
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","selector":"lane","lane":1}})", &unsupported);
|
||||
CHECK(!unsupported);
|
||||
unsupported = false;
|
||||
canon("dev-c2", R"({"print":{"command":"ams_change_filament","selector":"external"}})", &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);
|
||||
CHECK(!unsupported);
|
||||
}
|
||||
|
||||
// filament_setting is advertised by filament_slots alone (OPCP §7.8), so a
|
||||
// standalone printer with no ams_ops can still write slots, while the material
|
||||
// writes stay gated.
|
||||
TEST_CASE("a filament_slots reply admits ams_filament_setting without ams_ops", "[OrcaPrinterAgent]") {
|
||||
Probe agent("/tmp");
|
||||
agent.deliver_to_sink("dev-slots",
|
||||
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false,"filament_slots":true}}}}})",
|
||||
/*local=*/true);
|
||||
|
||||
bool unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload(
|
||||
"dev-slots",
|
||||
R"({"print":{"command":"ams_filament_setting","ams_id":255,"slot_id":0,"tray_id":0,"tray_type":"PLA"}})",
|
||||
&unsupported);
|
||||
CHECK_FALSE(unsupported);
|
||||
|
||||
unsupported = false;
|
||||
OrcaPrinterAgent::canonicalize_ams_payload(
|
||||
"dev-slots",
|
||||
R"({"print":{"command":"ams_change_filament","target":1,"slot_id":1,"ams_id":0}})",
|
||||
&unsupported);
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
@@ -209,6 +209,14 @@ TEST_CASE("unit: AMS capability registry reports only declared material systems"
|
||||
// Later replies overwrite: a removed material system must clear the flag.
|
||||
register_ams_capability("cap-true", false);
|
||||
CHECK_FALSE(has_ams_capability("cap-true"));
|
||||
|
||||
// filament_slots is a separate connector flag: no record reads false, and
|
||||
// a later reply clears it.
|
||||
CHECK_FALSE(has_filament_slots("cap-slot-none"));
|
||||
register_filament_slots("cap-slots", true);
|
||||
CHECK(has_filament_slots("cap-slots"));
|
||||
register_filament_slots("cap-slots", false);
|
||||
CHECK_FALSE(has_filament_slots("cap-slots"));
|
||||
}
|
||||
|
||||
// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space.
|
||||
|
||||
Reference in New Issue
Block a user