Bunch of dedupes

This commit is contained in:
Lam Wei Lun
2026-09-21 18:43:08 +08:00
parent 6fdf697bc7
commit 5a4fcafb5b
7 changed files with 232 additions and 244 deletions
+87 -12
View File
@@ -1,5 +1,6 @@
#include "AmsPayload.hpp"
#include "Http.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -20,6 +21,7 @@
#include <map>
#include <mutex>
#include <sstream>
#include <utility>
namespace Slic3r {
@@ -203,6 +205,69 @@ bool parse_moonraker_lane_data(const nlohmann::json& body,
return true;
}
LaneDataFetch read_moonraker_lane_data(const std::string& origin,
const std::string& api_key,
std::vector<AmsTrayData>& trays,
int& max_lane_index)
{
trays.clear();
max_lane_index = 0;
std::string base = origin;
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return LaneDataFetch::unknown;
unsigned http_status = 0;
std::string response_body;
std::string http_error;
auto http = Http::get(base + "/server/database/item?namespace=lane_data");
if (!api_key.empty())
http.header("X-Api-Key", api_key);
http.timeout_connect(5)
.timeout_max(10)
.on_complete([&](std::string body, unsigned status) {
http_status = status;
if (status == 200)
response_body = std::move(body);
else
http_error = "HTTP error: " + std::to_string(status);
})
.on_error([&](std::string, std::string err, unsigned status) {
// Http routes every >=400 response through here, so a 404 arrives as
// a real HTTP status, not a transport failure (REQ-STS-007 §7.7).
http_status = status;
http_error = err;
if (status > 0)
http_error += " (HTTP " + std::to_string(status) + ")";
})
.perform_sync();
if (http_status == 404) {
BOOST_LOG_TRIVIAL(info) << "AmsPayload: lane_data not served yet (unknown topology)";
return LaneDataFetch::unknown;
}
if (http_status != 200) {
BOOST_LOG_TRIVIAL(info) << "AmsPayload: lane_data fetch failed: " << http_error;
return LaneDataFetch::error;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "AmsPayload: invalid lane_data JSON";
return LaneDataFetch::error;
}
const bool empty_value = json.is_object() && json.contains("result") && json["result"].is_object() &&
json["result"].contains("value") && json["result"]["value"].is_object() &&
json["result"]["value"].empty();
if (empty_value)
return LaneDataFetch::none;
if (!parse_moonraker_lane_data(json, trays, max_lane_index))
return LaneDataFetch::error;
return LaneDataFetch::synced;
}
void resolve_tray_info_idx(std::vector<AmsTrayData>& trays)
{
auto* bundle = GUI::wxGetApp().preset_bundle;
@@ -315,10 +380,18 @@ nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
// Last ams_count rendered per device: clear_ams_payload_for_device walks the
// same unit set to mark them all absent.
static std::mutex g_ams_state_mutex;
static std::map<std::string, int> g_ams_last_count;
static std::map<std::string, std::vector<std::string>> g_ams_ops;
static std::map<std::string, bool> g_ams_capability;
static std::mutex g_ams_state_mutex;
static std::map<std::string, int> g_ams_last_count;
// One device's declaration from its get_capabilities reply. ops_known separates
// "no reply yet" (never gate) from "answered without ops" (gate every write).
struct AmsDeviceCaps
{
std::vector<std::string> ops;
bool ops_known = false;
bool has_ams = false;
};
static std::map<std::string, AmsDeviceCaps> g_ams_caps;
static void remember_ams_count(const std::string& dev_id, int ams_count)
{
@@ -333,16 +406,18 @@ void register_ams_ops(const std::string& dev_id, const std::vector<std::string>&
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_ops[dev_id] = ops;
AmsDeviceCaps& caps = g_ams_caps[dev_id];
caps.ops = ops;
caps.ops_known = true;
}
bool ams_op_supported(const std::string& dev_id, const std::string& op)
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_ops.find(dev_id);
if (it == g_ams_ops.end())
return true; // no OrcaSonar capabilities seen: never gate
return std::find(it->second.begin(), it->second.end(), op) != it->second.end();
auto it = g_ams_caps.find(dev_id);
if (it == g_ams_caps.end() || !it->second.ops_known)
return true; // no capability reply yet: never gate
return std::find(it->second.ops.begin(), it->second.ops.end(), op) != it->second.ops.end();
}
void register_ams_capability(const std::string& dev_id, bool has_ams)
@@ -350,14 +425,14 @@ void register_ams_capability(const std::string& dev_id, bool has_ams)
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_capability[dev_id] = has_ams;
g_ams_caps[dev_id].has_ams = has_ams;
}
bool has_ams_capability(const std::string& dev_id)
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_capability.find(dev_id);
return it != g_ams_capability.end() && it->second;
auto it = g_ams_caps.find(dev_id);
return it != g_ams_caps.end() && it->second.has_ams;
}
void build_ams_payload_for_device(const std::string& dev_id,
+24 -3
View File
@@ -39,6 +39,26 @@ bool parse_moonraker_lane_data(const nlohmann::json& body,
std::vector<AmsTrayData>& trays,
int& max_lane_index);
// Outcome of one lane_data read. synced/none/unknown mirror OrcaSonar's
// REQ-STS-007 tri-state; error covers transport failure and unparsable bodies.
enum class LaneDataFetch { synced, none, unknown, error };
// Read and parse the Moonraker `lane_data` namespace from origin (OrcaSonar's
// façade or a real Moonraker — both emit the same shape). A non-empty api_key is
// sent as X-Api-Key. trays/max_lane_index are only populated on synced. The
// tri-state is preserved: 404 is unknown, an empty value object is none.
LaneDataFetch read_moonraker_lane_data(const std::string& origin,
const std::string& api_key,
std::vector<AmsTrayData>& trays,
int& max_lane_index);
// AMS units a flat lane range renders into (4 lanes per unit, rounding up);
// 0 when there are no lanes. max_lane_index is the highest index, or -1.
inline int ams_count_for_lanes(int max_lane_index)
{
return max_lane_index < 0 ? 0 : (max_lane_index + 4) / 4;
}
// Fill each tray's tray_info_idx from the loaded preset bundle (falling back to
// the generic family map). Reads GUI preset state, so it MUST run on the main
// thread. Ids already set by a vendor-aware resolver are left untouched.
@@ -76,9 +96,10 @@ void build_ams_payload_for_device(const std::string& dev_id,
void clear_ams_payload_for_device(const std::string& dev_id, const QueueOnMainFn& queue_fn);
// Process-wide canonical AMS write capability (OrcaSonar REQ-STS-008), parsed
// from the info.get_capabilities reply. Devices with no record (Bambu, cloud
// profiles) report every op supported: gating only ever applies to OrcaSonar
// printers that answered.
// from the info.get_capabilities reply. A device with no record (no reply yet;
// non-OrcaSonar agents never register) reports every op supported: gating only
// applies to OrcaSonar printers that answered. An answer without ams_ops
// registers an empty op set, so it gates every write.
void register_ams_ops(const std::string& dev_id, const std::vector<std::string>& ops);
bool ams_op_supported(const std::string& dev_id, const std::string& op);
+19 -70
View File
@@ -564,29 +564,20 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
std::vector<AmsTrayData> trays;
int max_lane_index = 0;
// Try Moonraker filament data (more generic, supports any filament changer
// software that reports lane data to Moonraker like AFC and recent Happy
// Hare as of Feb 15, 2026)
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
<< (max_lane_index + 1) << " lanes";
int ams_count = (max_lane_index + 4) / 4;
build_ams_payload(ams_count, max_lane_index, trays);
return true;
// Try Moonraker lane data first (generic; covers AFC and recent Happy Hare),
// then the Happy Hare object query.
const bool moonraker_lanes = fetch_moonraker_filament_data(trays, max_lane_index);
const bool happy_hare = !moonraker_lanes && fetch_hh_filament_info(trays, max_lane_index);
if (!moonraker_lanes && !happy_hare) {
// No MMU detected - normal for printers without one, not an error.
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No MMU system detected (neither HH nor Moonraker)";
return false;
}
// Attempt Happy Hare first (more widely adopted, supports more filament changers)
if (fetch_hh_filament_info(trays, max_lane_index)) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
<< (max_lane_index + 1) << " gates";
int ams_count = (max_lane_index + 4) / 4;
build_ams_payload(ams_count, max_lane_index, trays);
return true;
}
// No MMU detected - this is normal for printers without MMU, not an error
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: No MMU system detected (neither HH nor Moonraker)";
return false;
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected "
<< (moonraker_lanes ? "Moonraker filament system" : "Happy Hare MMU") << " with "
<< (max_lane_index + 1) << (moonraker_lanes ? " lanes" : " gates");
build_ams_payload(ams_count_for_lanes(max_lane_index), max_lane_index, trays);
return true;
}
CameraStreamMode MoonrakerPrinterAgent::get_camera_stream_mode() const
@@ -667,54 +658,12 @@ int MoonrakerPrinterAgent::safe_array_int(const nlohmann::json& arr, int idx)
// Fetch filament info from moonraker database
bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index)
{
// Fetch lane data from Moonraker database
std::string url = join_url(device_info.base_url, "/server/database/item?namespace=lane_data");
std::string response_body;
bool success = false;
std::string http_error;
auto http = Http::get(url);
if (!device_info.api_key.empty()) {
http.header("X-Api-Key", device_info.api_key);
}
http.timeout_connect(5)
.timeout_max(10)
.on_complete([&](std::string body, unsigned status) {
if (status == 200) {
response_body = body;
success = true;
} else {
http_error = "HTTP error: " + std::to_string(status);
}
})
.on_error([&](std::string body, std::string err, unsigned status) {
http_error = err;
if (status > 0) {
http_error += " (HTTP " + std::to_string(status) + ")";
}
})
.perform_sync();
if (!success) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Failed to fetch lane data: " << http_error;
return false;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Invalid JSON response";
return false;
}
// Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } }
if (!parse_moonraker_lane_data(json, trays, max_lane_index)) {
return false;
}
// tray_info_idx is resolved later, on the main thread, inside
// build_ams_payload_for_device.
return true;
// Shared with OrcaPrinterAgent's lane_data read; only the synced outcome
// matters here, since a missing namespace or an empty one both fall through
// to the Happy Hare query. tray_info_idx is resolved later, on the main
// thread, inside build_ams_payload_for_device.
return read_moonraker_lane_data(device_info.base_url, device_info.api_key, trays, max_lane_index) ==
LaneDataFetch::synced;
}
// Fetch filament info from Happy Hare MMU
+65 -145
View File
@@ -595,42 +595,6 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
nozzle_diameter_cache[dev_id] = nozzle_dia;
}
// The capabilities reply itself is forwarded unchanged. Its declared
// ams_ops (OPCP §7.8) gate every AMS control, and protocol.features.fms
// declares whether a material system actually exists.
{
const auto caps_it = info_it->find("capabilities");
if (caps_it != info_it->end() && caps_it->is_object()) {
const auto proto_it = caps_it->find("protocol");
if (proto_it != caps_it->end() && proto_it->is_object()) {
const auto ops_it = proto_it->find("ams_ops");
std::vector<std::string> ops;
if (ops_it != proto_it->end() && ops_it->is_array()) {
for (const auto& op : *ops_it)
if (op.is_string())
ops.push_back(op.get<std::string>());
register_ams_ops(dev_id, ops);
}
// features.fms is the authoritative "has a material system"
// flag (topology.material_units non-empty). Payloads without
// it fall back to a non-empty ams_ops.
bool has_ams = false;
bool fms_known = false;
const auto features_it = proto_it->find("features");
if (features_it != proto_it->end() && features_it->is_object()) {
const auto fms_it = features_it->find("fms");
if (fms_it != features_it->end() && fms_it->is_boolean()) {
has_ams = fms_it->get<bool>();
fms_known = true;
}
}
if (!fms_known)
has_ams = !ops.empty();
register_ams_capability(dev_id, has_ams);
}
}
}
} else {
const auto print_it = envelope.find("print");
if (print_it != envelope.end() && print_it->is_object() && print_it->value("command", "") == "push_status" &&
@@ -654,6 +618,57 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
return modified ? envelope.dump() : payload;
}
// One get_capabilities reply's AMS declaration. Not a dialect shim: it feeds
// get_filament_sync_mode() and the ams_ops write gate, so it must outlive
// merge_capabilities.
void OrcaPrinterAgent::register_ams_capabilities(const std::string& dev_id, const std::string& payload)
{
if (dev_id.empty() || payload.find("get_capabilities") == std::string::npos)
return;
const nlohmann::json envelope = nlohmann::json::parse(payload, nullptr, false);
if (!envelope.is_object())
return;
const auto info_it = envelope.find("info");
if (info_it == envelope.end() || !info_it->is_object() || info_it->value("command", "") != "get_capabilities")
return;
const auto caps_it = info_it->find("capabilities");
if (caps_it == info_it->end() || !caps_it->is_object())
return;
const auto proto_it = caps_it->find("protocol");
if (proto_it == caps_it->end() || !proto_it->is_object())
return;
// ams_ops is the write-op union the resolved drivers implement. Register it
// on every reply — empty when the key is absent — so "answered without ops"
// gates every AMS write (OPCP §7.8) and a later reply clears stale ops.
std::vector<std::string> ops;
const auto ops_it = proto_it->find("ams_ops");
if (ops_it != proto_it->end() && ops_it->is_array()) {
for (const auto& op : *ops_it)
if (op.is_string())
ops.push_back(op.get<std::string>());
}
register_ams_ops(dev_id, ops);
// features.fms is the authoritative "has a material system" flag
// (topology.material_units non-empty). Payloads without it fall back to a
// non-empty ams_ops.
bool has_ams = false;
bool fms_known = false;
const auto features_it = proto_it->find("features");
if (features_it != proto_it->end() && features_it->is_object()) {
const auto fms_it = features_it->find("fms");
if (fms_it != features_it->end() && fms_it->is_boolean()) {
has_ams = fms_it->get<bool>();
fms_known = true;
}
}
if (!fms_known)
has_ams = !ops.empty();
register_ams_capability(dev_id, has_ams);
}
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
{
// Subscription doorbell, on the raw payload before the UI marshal so the
@@ -662,6 +677,7 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str
request_filament_refresh(dev_id);
parse_ipcam_info(dev_id, payload);
register_ams_capabilities(dev_id, payload);
std::string merged_payload = merge_capabilities(dev_id, payload);
OnMessageFn fn;
@@ -776,14 +792,6 @@ int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string t
return route_send(lan_mode, dev_id, j.dump());
}
int OrcaPrinterAgent::command_ams_calibrate(std::string /*dev_id*/, int /*ams_id*/, int /*sequence_id*/, bool /*lan_mode*/)
{
// OrcaSonar has no ams_calibrate command. Do not send the Bambu M620 C
// dialect through the vendor-neutral OrcaSonar gcode_line command.
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: AMS calibration is not part of the OrcaSonar API";
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
int tray_number = 0;
@@ -801,46 +809,6 @@ int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tr
return route_send(lan_mode, dev_id, j.dump());
}
int OrcaPrinterAgent::command_start_camera(std::string /*dev_id*/)
{
// OrcaSonar exposes camera.ipcam_* controls, not the legacy start_camera
// operation used by the Bambu agent.
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: camera start is not part of the OrcaSonar API";
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
}
int OrcaPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G90\n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return route_send(lan_mode, dev_id, j.dump());
}
int OrcaPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G29\n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
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 sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_homing) {
j["print"]["command"] = "back_to_center";
} else {
// Preserve the existing safety behavior: never home Z/Y during a print.
j["print"]["command"] = "gcode_line";
j["print"]["param"] = is_printing ? "G28 X\n" : "G28\n";
}
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 sequence_id, bool lan_mode)
{
nlohmann::json j;
@@ -908,13 +876,6 @@ FilamentSyncMode OrcaPrinterAgent::get_filament_sync_mode() const
return FilamentSyncMode::none;
}
bool OrcaPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{
if (sync_mode != get_filament_sync_mode())
return false;
return fetch_lane_data(dev_id) == LaneDataState::synced;
}
// Read the lane_data projection once and apply the REQ-STS-007 tri-state to
// DevFilaSystem. Only LaneDataState::error arms the retry latch: 404 means
// "not knowable yet" (pre-bootstrap or acknowledged-unknown topology) and {}
@@ -939,69 +900,28 @@ OrcaPrinterAgent::LaneDataState OrcaPrinterAgent::fetch_lane_data(const std::str
// Moonraker-compatible façade. Called on the refresh worker (subscription
// mode), so the payload mutation is marshalled onto the main thread through
// queue_fn; a GUI-thread caller reads DevFilaSystem inline.
const std::string url = origin + "/server/database/item?namespace=lane_data";
std::string response_body;
unsigned http_status = 0;
std::string http_error;
auto http = Http::get(url);
if (!api_key.empty())
http.header("X-Api-Key", api_key);
http.timeout_connect(5)
.timeout_max(10)
.on_complete([&](std::string body, unsigned status) {
http_status = status;
if (status == 200) {
response_body = std::move(body);
} else {
http_error = "HTTP error: " + std::to_string(status);
}
})
.on_error([&](std::string, std::string err, unsigned status) {
http_status = status;
http_error = err;
if (status > 0)
http_error += " (HTTP " + std::to_string(status) + ")";
})
.perform_sync();
// Http routes every >=400 response through on_error(), so a 404 arrives here
// with an empty err and the status in http_status. It is a real HTTP
// response, not a transport failure: the namespace is not served / topology
// unknown (REQ-STS-007 §7.7). Never latch a retry for it.
if (http_status == 404) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::fetch_lane_data: lane_data not served yet (unknown topology)";
return LaneDataState::unknown;
}
if (http_status != 200) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::fetch_lane_data: lane_data fetch failed: " << http_error;
return LaneDataState::error;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::fetch_lane_data: invalid lane_data JSON";
return LaneDataState::error;
}
const bool empty_value = json.is_object() && json.contains("result") && json["result"].is_object() &&
json["result"].contains("value") && json["result"]["value"].is_object() && json["result"]["value"].empty();
if (empty_value) {
std::vector<AmsTrayData> trays;
int max_lane_index = 0;
switch (read_moonraker_lane_data(origin, api_key, trays, max_lane_index)) {
case LaneDataFetch::synced:
break;
case LaneDataFetch::none:
// Authoritative empty: flush stale trays so a removed AMS does not
// linger in the device panel.
clear_ams_payload_for_device(dev_id, queue_fn);
return LaneDataState::none;
case LaneDataFetch::unknown:
// 404: not knowable yet (pre-bootstrap or acknowledged-unknown topology).
// Never latched; the next doorbell or reconnect retries.
return LaneDataState::unknown;
case LaneDataFetch::error:
return LaneDataState::error;
}
std::vector<AmsTrayData> trays;
int max_lane_index = 0;
if (!parse_moonraker_lane_data(json, trays, max_lane_index))
return LaneDataState::error;
const int ams_count = (max_lane_index + 4) / 4;
// printer_type stays unset: push_status already carries the OrcaSonar printer
// type, and overwriting it here would clear it. build_ams_payload_for_device
// marshals the DevFilaSystem mutation through queue_fn when set.
build_ams_payload_for_device(dev_id, std::nullopt, ams_count, max_lane_index, trays, queue_fn);
build_ams_payload_for_device(dev_id, std::nullopt, ams_count_for_lanes(max_lane_index), max_lane_index, trays, queue_fn);
return LaneDataState::synced;
}
+6 -8
View File
@@ -80,12 +80,7 @@ public:
int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
@@ -104,7 +99,6 @@ public:
// topology_state doorbell that keep DevFilaSystem fresh are LAN-only, and
// cloud printers get their AMS view from the mirrored push_status.
FilamentSyncMode get_filament_sync_mode() const override;
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
@@ -114,8 +108,6 @@ public:
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
// Test-only: observe the subscription doorbell policy without spawning the refresh worker.
bool filament_doorbell_needed_for_test(const std::string& dev_id, const std::string& payload)
@@ -149,6 +141,12 @@ protected:
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
// Register one get_capabilities reply's AMS declaration: ams_ops (empty when
// the reply omits it, so "answered without ops" gates every write) and
// protocol.features.fms. Kept separate from merge_capabilities so dropping
// that shim cannot silently drop the capability registry.
void register_ams_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
// connect_printer(), which only starts the worker.
+18 -6
View File
@@ -133,16 +133,28 @@ TEST_CASE("filament sync follows the printer's AMS capability", "[OrcaPrinterAge
/*local=*/true);
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
// The pull contract (Sidebar's blocking path) is gone: a pull-mode fetch is
// refused by the mode guard, and a subscription fetch for a device that is
// not the active printer is refused before any HTTP.
CHECK_FALSE(agent.fetch_filament_info("dev-ams-1", Slic3r::FilamentSyncMode::pull));
CHECK_FALSE(agent.fetch_filament_info("dev-ams-2", Slic3r::FilamentSyncMode::subscription));
agent.disconnect_printer();
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
}
// OrcaSonar's contract: an absent ams_ops key means "no AMS controls" (app.go
// omits it when no driver declares a write op; Qidi is the shipped example).
// Answering get_capabilities without it must gate every AMS write client-side,
// not fall back to the base default's "no record -> never gate".
TEST_CASE("a capability reply without ams_ops gates AMS writes", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
agent.deliver_to_sink("dev-noops",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":true}}}}})",
/*local=*/true);
bool unsupported = false;
OrcaPrinterAgent::canonicalize_ams_payload(
"dev-noops",
R"({"print":{"command":"ams_change_filament","target":0,"slot_id":0,"ams_id":0}})",
&unsupported);
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
+13
View File
@@ -184,6 +184,19 @@ TEST_CASE("unit: AMS payload sets exist bits beyond 31 lanes", "[unit][moonraker
CHECK_FALSE(u8[1].contains("tray_slot_placeholder")); // slot 33 present
}
// why: a flat lane range renders into 4-slot AMS units; both agents and the
// builder share this one formula so they cannot drift.
TEST_CASE("unit: AMS unit count rounds lane ranges up to 4-slot units", "[unit][moonraker]")
{
CHECK(ams_count_for_lanes(-1) == 0);
CHECK(ams_count_for_lanes(0) == 1);
CHECK(ams_count_for_lanes(3) == 1);
CHECK(ams_count_for_lanes(4) == 2);
CHECK(ams_count_for_lanes(6) == 2);
CHECK(ams_count_for_lanes(7) == 2);
CHECK(ams_count_for_lanes(8) == 3);
}
// why: the sync mode keys off the printer's declared material system; a device
// with no capability record must never read as AMS-capable.
TEST_CASE("unit: AMS capability registry reports only declared material systems", "[unit][moonraker]")