mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-21 16:02:37 +00:00
Bunch of dedupes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]")
|
||||
|
||||
Reference in New Issue
Block a user